Skip to content

Commit 541e5b6

Browse files
authored
refactor: Billing refactor (#992)
* 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. ---------
1 parent 2f06d6f commit 541e5b6

File tree

27 files changed

+2652
-1304
lines changed

27 files changed

+2652
-1304
lines changed

clients/tfchain-client-go/contract_events.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,33 @@ type NodeExtraFeeSet struct {
242242
ExtraFee types.U64 `json:"extra_fee"`
243243
Topics []types.Hash
244244
}
245+
246+
type RentWaived struct {
247+
Phase types.Phase
248+
ContractID types.U64 `json:"contract_id"`
249+
Topics []types.Hash
250+
}
251+
252+
type ContractGracePeriodElapsed struct {
253+
Phase types.Phase
254+
ContractID types.U64 `json:"contract_id"`
255+
GracePeriod types.U64 `json:"grace_period"`
256+
Topics []types.Hash
257+
}
258+
259+
type ContractPaymentOverdrawn struct {
260+
Phase types.Phase
261+
ContractID types.U64 `json:"contract_id"`
262+
Timestamp types.U64 `json:"timestamp"`
263+
PartiallyBilledAmount types.U128 `json:"partially_billed_amount"`
264+
Overdraft types.U128 `json:"overdraft"`
265+
Topics []types.Hash
266+
}
267+
268+
type RewardDistributed struct {
269+
Phase types.Phase
270+
ContractID types.U64 `json:"contract_id"`
271+
StandardRewards types.U128 `json:"standard_rewards"`
272+
AdditionalRewards types.U128 `json:"additional_rewards"`
273+
Topics []types.Hash
274+
}

clients/tfchain-client-go/events.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,10 @@ type EventRecords struct {
389389
SmartContractModule_ServiceContractBilled []ServiceContractBilled //nolint:stylecheck,golint
390390
SmartContractModule_BillingFrequencyChanged []BillingFrequencyChanged //nolint:stylecheck,golint
391391
SmartContractModule_NodeExtraFeeSet []NodeExtraFeeSet //nolint:stylecheck,golint
392+
SmartContractModule_RentWaived []RentWaived //nolint:stylecheck,golint
393+
SmartContractModule_ContractGracePeriodElapsed []ContractGracePeriodElapsed //nolint:stylecheck,golint
394+
SmartContractModule_ContractPaymentOverdrawn []ContractPaymentOverdrawn //nolint:stylecheck,golint
395+
SmartContractModule_RewardDistributed []RewardDistributed //nolint:stylecheck,golint
392396

393397
// farm events
394398
TfgridModule_FarmStored []FarmStored //nolint:stylecheck,golint

clients/tfchain-client-go/utils.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ var smartContractModuleErrors = []string{
127127
"WrongAuthority",
128128
"UnauthorizedToChangeSolutionProviderId",
129129
"UnauthorizedToSetExtraFee",
130+
"RewardDistributionError",
131+
"ContractPaymentStateNotExists",
130132
}
131133

132134
// https://github.com/threefoldtech/tfchain/blob/development/substrate-node/pallets/pallet-tfgrid/src/lib.rs#L442
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# 23. Billing Refactor in pallet-smart-contract Module
2+
3+
Date: 2024-08-18
4+
5+
## Status
6+
7+
Accepted
8+
9+
## Context
10+
11+
The billing logic within the pallet-smart-contract module faced several issues, including critical bugs, inefficiencies in handling validators, and inadequate fund reservation mechanisms.
12+
The goal of this refactor was to address these issues while enhancing the overall reliability and robustness of the billing process.
13+
14+
## Decision
15+
16+
The following architectural decisions were made to improve the billing logic:
17+
18+
### Refactoring Billing Logic
19+
20+
- Enhanced Tracking:
21+
Improved tracking mechanisms were introduced for contract payments, especially during grace periods and when handling overdue payments.
22+
The new ContractPaymentState was introduced to accurately manage contract payment states and resolve liquidity issues.
23+
- Overdraft Handling:
24+
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.
25+
- Fund Reservation:
26+
The use of balance locks/freezes was replaced with a more reliable reservation system, reducing issues related to fund availability for reward distribution.
27+
28+
### Improved Off-Chain Worker Logic
29+
30+
- Runtime Verification:
31+
The is_next_block_author function was removed, with verification now occurring at runtime.
32+
This change ensures transaction fees are reliably waived for validators and still prevents duplicate transactions.
33+
34+
### New Events Introduced
35+
36+
- ContractGracePeriodElapsed: Indicates that a contract's grace period has elapsed.
37+
- ContractPaymentOverdrawn: Indicates that a contract's payment is overdrawn.
38+
- RewardDistributed: Indicates that rewards have been distributed.
39+
40+
### Certified vs DIY Capacity
41+
42+
The system now correctly charges certified capacity at a higher rate (25% more) than DIY capacity.
43+
44+
## Consequences
45+
46+
- Increased Robustness: The billing process is now more robust and less prone to errors, particularly in scenarios involving partial payments and fund reservations.
47+
- Better Visibility: The introduction of new events and improved logging provides better visibility into the billing and payment processes, aiding in debugging and monitoring.
48+
- Backward Compatibility: While significant refactoring has been done, efforts were made to ensure backward compatibility where possible, especially concerning contract migration.

substrate-node/node/src/benchmarking.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use sp_core::{Encode, Pair};
1111
use sp_inherents::{InherentData, InherentDataProvider};
1212
use sp_keyring::Sr25519Keyring;
1313
use sp_runtime::{OpaqueExtrinsic, SaturatedConversion};
14-
use tfchain_runtime as runtime;
14+
use tfchain_runtime::{self as runtime, pallet_smart_contract};
1515

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

@@ -132,6 +132,7 @@ pub fn create_benchmark_extrinsic(
132132
frame_system::CheckNonce::<runtime::Runtime>::from(nonce),
133133
frame_system::CheckWeight::<runtime::Runtime>::new(),
134134
pallet_transaction_payment::ChargeTransactionPayment::<runtime::Runtime>::from(0),
135+
pallet_smart_contract::types::ContractIdProvides::<runtime::Runtime>::new(),
135136
);
136137

137138
let raw_payload = runtime::SignedPayload::from_raw(
@@ -146,6 +147,7 @@ pub fn create_benchmark_extrinsic(
146147
(),
147148
(),
148149
(),
150+
(),
149151
),
150152
);
151153
let signature = raw_payload.using_encoded(|e| sender.sign(e));

substrate-node/pallets/pallet-burning/src/weights.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
//! Autogenerated weights for pallet_burning
33
//!
44
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
5-
//! DATE: 2024-06-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
5+
//! DATE: 2024-08-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
66
//! WORST CASE MAP SIZE: `1000000`
7-
//! HOSTNAME: `4b80713dc969`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
7+
//! HOSTNAME: `66e77d0da08f`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
88
//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
99
1010
// Executed Command:
@@ -45,8 +45,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
4545
// Proof Size summary in bytes:
4646
// Measured: `109`
4747
// Estimated: `1594`
48-
// Minimum execution time: 26_841_000 picoseconds.
49-
Weight::from_parts(27_372_000, 1594)
48+
// Minimum execution time: 26_780_000 picoseconds.
49+
Weight::from_parts(27_291_000, 1594)
5050
.saturating_add(T::DbWeight::get().reads(1_u64))
5151
.saturating_add(T::DbWeight::get().writes(1_u64))
5252
}
@@ -60,8 +60,8 @@ impl WeightInfo for () {
6060
// Proof Size summary in bytes:
6161
// Measured: `109`
6262
// Estimated: `1594`
63-
// Minimum execution time: 26_841_000 picoseconds.
64-
Weight::from_parts(27_372_000, 1594)
63+
// Minimum execution time: 26_780_000 picoseconds.
64+
Weight::from_parts(27_291_000, 1594)
6565
.saturating_add(RocksDbWeight::get().reads(1_u64))
6666
.saturating_add(RocksDbWeight::get().writes(1_u64))
6767
}

substrate-node/pallets/pallet-dao/src/weights.rs

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
//! Autogenerated weights for pallet_dao
33
//!
44
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
5-
//! DATE: 2024-06-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
5+
//! DATE: 2024-08-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
66
//! WORST CASE MAP SIZE: `1000000`
7-
//! HOSTNAME: `4b80713dc969`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
7+
//! HOSTNAME: `66e77d0da08f`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
88
//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
99
1010
// Executed Command:
@@ -58,8 +58,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
5858
// Proof Size summary in bytes:
5959
// Measured: `208`
6060
// Estimated: `4687`
61-
// Minimum execution time: 19_196_000 picoseconds.
62-
Weight::from_parts(19_487_000, 4687)
61+
// Minimum execution time: 19_277_000 picoseconds.
62+
Weight::from_parts(20_188_000, 4687)
6363
.saturating_add(T::DbWeight::get().reads(4_u64))
6464
.saturating_add(T::DbWeight::get().writes(5_u64))
6565
}
@@ -77,8 +77,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
7777
// Proof Size summary in bytes:
7878
// Measured: `979`
7979
// Estimated: `4444`
80-
// Minimum execution time: 26_179_000 picoseconds.
81-
Weight::from_parts(26_751_000, 4444)
80+
// Minimum execution time: 26_390_000 picoseconds.
81+
Weight::from_parts(26_840_000, 4444)
8282
.saturating_add(T::DbWeight::get().reads(5_u64))
8383
.saturating_add(T::DbWeight::get().writes(1_u64))
8484
}
@@ -92,8 +92,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
9292
// Proof Size summary in bytes:
9393
// Measured: `487`
9494
// Estimated: `4687`
95-
// Minimum execution time: 18_154_000 picoseconds.
96-
Weight::from_parts(18_755_000, 4687)
95+
// Minimum execution time: 18_616_000 picoseconds.
96+
Weight::from_parts(18_985_000, 4687)
9797
.saturating_add(T::DbWeight::get().reads(3_u64))
9898
.saturating_add(T::DbWeight::get().writes(1_u64))
9999
}
@@ -111,8 +111,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
111111
// Proof Size summary in bytes:
112112
// Measured: `521`
113113
// Estimated: `4687`
114-
// Minimum execution time: 24_757_000 picoseconds.
115-
Weight::from_parts(25_268_000, 4687)
114+
// Minimum execution time: 25_117_000 picoseconds.
115+
Weight::from_parts(25_498_000, 4687)
116116
.saturating_add(T::DbWeight::get().reads(3_u64))
117117
.saturating_add(T::DbWeight::get().writes(4_u64))
118118
}
@@ -136,8 +136,8 @@ impl WeightInfo for () {
136136
// Proof Size summary in bytes:
137137
// Measured: `208`
138138
// Estimated: `4687`
139-
// Minimum execution time: 19_196_000 picoseconds.
140-
Weight::from_parts(19_487_000, 4687)
139+
// Minimum execution time: 19_277_000 picoseconds.
140+
Weight::from_parts(20_188_000, 4687)
141141
.saturating_add(RocksDbWeight::get().reads(4_u64))
142142
.saturating_add(RocksDbWeight::get().writes(5_u64))
143143
}
@@ -155,8 +155,8 @@ impl WeightInfo for () {
155155
// Proof Size summary in bytes:
156156
// Measured: `979`
157157
// Estimated: `4444`
158-
// Minimum execution time: 26_179_000 picoseconds.
159-
Weight::from_parts(26_751_000, 4444)
158+
// Minimum execution time: 26_390_000 picoseconds.
159+
Weight::from_parts(26_840_000, 4444)
160160
.saturating_add(RocksDbWeight::get().reads(5_u64))
161161
.saturating_add(RocksDbWeight::get().writes(1_u64))
162162
}
@@ -170,8 +170,8 @@ impl WeightInfo for () {
170170
// Proof Size summary in bytes:
171171
// Measured: `487`
172172
// Estimated: `4687`
173-
// Minimum execution time: 18_154_000 picoseconds.
174-
Weight::from_parts(18_755_000, 4687)
173+
// Minimum execution time: 18_616_000 picoseconds.
174+
Weight::from_parts(18_985_000, 4687)
175175
.saturating_add(RocksDbWeight::get().reads(3_u64))
176176
.saturating_add(RocksDbWeight::get().writes(1_u64))
177177
}
@@ -189,8 +189,8 @@ impl WeightInfo for () {
189189
// Proof Size summary in bytes:
190190
// Measured: `521`
191191
// Estimated: `4687`
192-
// Minimum execution time: 24_757_000 picoseconds.
193-
Weight::from_parts(25_268_000, 4687)
192+
// Minimum execution time: 25_117_000 picoseconds.
193+
Weight::from_parts(25_498_000, 4687)
194194
.saturating_add(RocksDbWeight::get().reads(3_u64))
195195
.saturating_add(RocksDbWeight::get().writes(4_u64))
196196
}

substrate-node/pallets/pallet-kvstore/src/weights.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
//! Autogenerated weights for pallet_kvstore
33
//!
44
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
5-
//! DATE: 2024-06-04, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
5+
//! DATE: 2024-08-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]`
66
//! WORST CASE MAP SIZE: `1000000`
7-
//! HOSTNAME: `4b80713dc969`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
7+
//! HOSTNAME: `66e77d0da08f`, CPU: `AMD Ryzen 7 5800X 8-Core Processor`
88
//! EXECUTION: , WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024
99
1010
// Executed Command:
@@ -46,8 +46,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
4646
// Proof Size summary in bytes:
4747
// Measured: `0`
4848
// Estimated: `0`
49-
// Minimum execution time: 7_293_000 picoseconds.
50-
Weight::from_parts(7_574_000, 0)
49+
// Minimum execution time: 11_372_000 picoseconds.
50+
Weight::from_parts(12_032_000, 0)
5151
.saturating_add(T::DbWeight::get().writes(1_u64))
5252
}
5353
/// Storage: `TFKVStore::TFKVStore` (r:1 w:1)
@@ -56,8 +56,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
5656
// Proof Size summary in bytes:
5757
// Measured: `146`
5858
// Estimated: `3611`
59-
// Minimum execution time: 12_784_000 picoseconds.
60-
Weight::from_parts(13_205_000, 3611)
59+
// Minimum execution time: 12_414_000 picoseconds.
60+
Weight::from_parts(12_854_000, 3611)
6161
.saturating_add(T::DbWeight::get().reads(1_u64))
6262
.saturating_add(T::DbWeight::get().writes(1_u64))
6363
}
@@ -71,8 +71,8 @@ impl WeightInfo for () {
7171
// Proof Size summary in bytes:
7272
// Measured: `0`
7373
// Estimated: `0`
74-
// Minimum execution time: 7_293_000 picoseconds.
75-
Weight::from_parts(7_574_000, 0)
74+
// Minimum execution time: 11_372_000 picoseconds.
75+
Weight::from_parts(12_032_000, 0)
7676
.saturating_add(RocksDbWeight::get().writes(1_u64))
7777
}
7878
/// Storage: `TFKVStore::TFKVStore` (r:1 w:1)
@@ -81,8 +81,8 @@ impl WeightInfo for () {
8181
// Proof Size summary in bytes:
8282
// Measured: `146`
8383
// Estimated: `3611`
84-
// Minimum execution time: 12_784_000 picoseconds.
85-
Weight::from_parts(13_205_000, 3611)
84+
// Minimum execution time: 12_414_000 picoseconds.
85+
Weight::from_parts(12_854_000, 3611)
8686
.saturating_add(RocksDbWeight::get().reads(1_u64))
8787
.saturating_add(RocksDbWeight::get().writes(1_u64))
8888
}

substrate-node/pallets/pallet-smart-contract/src/benchmarking.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -305,11 +305,11 @@ benchmarks! {
305305

306306
let contract = SmartContractModule::<T>::contracts(contract_id).unwrap();
307307
// Get contract cost before billing to take into account nu
308-
let (cost, discount_level) = contract.calculate_contract_cost_tft(balance_init_amount, elapsed_seconds).unwrap();
308+
let (cost, discount_level) = contract.calculate_contract_cost_tft(balance_init_amount, elapsed_seconds, None).unwrap();
309309
}: _(RawOrigin::Signed(farmer), contract_id)
310310
verify {
311-
let lock = SmartContractModule::<T>::contract_number_of_cylces_billed(contract_id);
312-
assert_eq!(lock.amount_locked, cost);
311+
let contract_payment_state = SmartContractModule::<T>::contract_payment_state(contract_id).unwrap();
312+
assert_eq!(contract_payment_state.standard_reserve, cost);
313313
let contract_bill = types::ContractBill {
314314
contract_id,
315315
timestamp: SmartContractModule::<T>::get_current_timestamp_in_secs(),

0 commit comments

Comments
 (0)