-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(pallet): rework billing loop #702
Changes from 5 commits
0e3cde1
e66a40b
489529d
9b96041
a30cbc5
a0db20c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# 9. Rework smart contract billing loop | ||
|
||
Date: 2023-05-23 | ||
|
||
## Status | ||
|
||
Accepted | ||
|
||
## Context | ||
|
||
See [here](https://github.com/threefoldtech/tfchain/issues/701) for more details. | ||
|
||
## Decision | ||
|
||
(1) Each contract (node/rent/name contracts) is inserted in billing loop only once, when contract is created. | ||
There is no more exception for node contracts with no public ips and no used resources on chain. | ||
|
||
(2) When a contract is to be billed during the billing loop execution (via the offchain worker), bill it ONLY if there is effectively some IP/SU/CU/NU consumed. Technically this comes down to trigger `bill_contract()` extrinsic only when there is some used ip / resources or consumption reports linked to this contract. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -534,8 +534,10 @@ pub mod pallet { | |
let _account_id = ensure_signed(origin)?; | ||
|
||
// Clean up contract from billing loop if it doesn't exist anymore | ||
// This is the only place it should be done to guaranty we | ||
// remove contract id from billing loop at right index | ||
if Contracts::<T>::get(contract_id).is_none() { | ||
log::debug!("cleaning up deleted contract from storage"); | ||
log::debug!("cleaning up deleted contract from billing loop"); | ||
Self::remove_contract_from_billing_loop(index, contract_id)?; | ||
return Ok(().into()); | ||
} | ||
|
@@ -660,8 +662,8 @@ pub mod pallet { | |
// Let offchain worker check if there are contracts on the map at current index | ||
let current_index = Self::get_current_billing_loop_index(); | ||
|
||
let contracts = ContractsToBillAt::<T>::get(current_index); | ||
if contracts.is_empty() { | ||
let contract_ids = ContractsToBillAt::<T>::get(current_index); | ||
if contract_ids.is_empty() { | ||
log::info!( | ||
"No contracts to bill at block {:?}, index: {:?}", | ||
block_number, | ||
|
@@ -672,11 +674,28 @@ pub mod pallet { | |
|
||
log::info!( | ||
"{:?} contracts to bill at block {:?}", | ||
contracts, | ||
contract_ids, | ||
block_number | ||
); | ||
|
||
for contract_id in contracts { | ||
for contract_id in contract_ids { | ||
if let Some(c) = Contracts::<T>::get(contract_id) { | ||
if let types::ContractData::NodeContract(node_contract) = c.contract_type { | ||
let bill_ip = node_contract.public_ips > 0; | ||
let bill_cu_su = NodeContractResources::<T>::contains_key(contract_id) | ||
&& !NodeContractResources::<T>::get(contract_id).used.is_empty(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is something that is bad practice and should be avoided, it also results in the same. To me, this looks like 2 storage reads while it can be only 1 storage read. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly, in our case it results the same There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are we broken if we change the default value for resources? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if for some reason we set this for example pub fn default() -> Resources {
Resources {
hru: 1,
sru: 1,
cru: 1,
mru: 1,
}
}
pub fn empty() -> Resources {
Resources {
hru: 0,
sru: 0,
cru: 0,
mru: 0,
}
}
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. well, if the default is any other than 0 values, the user has to pay anyway |
||
let bill_nu = | ||
ContractBillingInformationByID::<T>::contains_key(contract_id) | ||
&& ContractBillingInformationByID::<T>::get(contract_id) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same idea here |
||
.amount_unbilled | ||
> 0; | ||
|
||
// Don't bill if no IP/CU/SU/NU to bill | ||
if !bill_ip && !bill_cu_su && !bill_nu { | ||
continue; | ||
} | ||
} | ||
} | ||
let _res = Self::bill_contract_using_signed_transaction(contract_id); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. move this one line up? If there is no contract (deleted somehow but still in billing loop) we don't want to push an extrinsic? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, lets check #703 (comment) for this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
cannot move this line up because when contract is deleted its id in billing loop is cleaned up at next cycle by pushing the extrinsic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes I just noticed that as well this morning, but it's very weird and hidden behaviour. It's not clear that it's subject to the offchain worker to be cleaned up or not. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now let's keep this behaviour but let's plan a future improvement where we redesign the connection between a contract and it's billing status / cycle |
||
} | ||
} | ||
|
@@ -864,22 +883,16 @@ impl<T: Config> Pallet<T> { | |
let mut id = ContractID::<T>::get(); | ||
id = id + 1; | ||
|
||
if let types::ContractData::NodeContract(ref mut nc) = contract_type { | ||
Self::_reserve_ip(id, nc)?; | ||
}; | ||
|
||
Self::validate_solution_provider(solution_provider_id)?; | ||
|
||
// Start billing frequency loop | ||
// Will always be block now + frequency | ||
match contract_type { | ||
types::ContractData::NodeContract(ref mut node_contract) => { | ||
Self::_reserve_ip(id, node_contract)?; | ||
|
||
// Insert created node contract in billing loop now only | ||
// if there is at least one public ip attached to node | ||
if node_contract.public_ips > 0 { | ||
Self::insert_contract_in_billing_loop(id); | ||
} | ||
} | ||
_ => Self::insert_contract_in_billing_loop(id), | ||
}; | ||
// Contract is inserted in billing loop ONLY once at contract creation | ||
Self::insert_contract_in_billing_loop(id); | ||
|
||
let contract = types::Contract { | ||
version: CONTRACT_VERSION, | ||
|
@@ -1013,13 +1026,6 @@ impl<T: Config> Pallet<T> { | |
// Do insert | ||
NodeContractResources::<T>::insert(contract.contract_id, &contract_resource); | ||
|
||
// Start billing frequency loop | ||
// Insert node contract in billing loop only if there | ||
// are non empty resources pushed for the contract | ||
if !contract_resource.used.is_empty() { | ||
Self::insert_contract_in_billing_loop(contract.contract_id); | ||
} | ||
|
||
// deposit event | ||
Self::deposit_event(Event::UpdatedUsedResources(contract_resource)); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
guarantee*