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
[storage_gas] implement customizable storage gas config #3810
Conversation
aptos-move/framework/aptos-framework/sources/state_storage.move
Outdated
Show resolved
Hide resolved
ef26764
to
a5eaa0a
Compare
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.
Cool stuff.
However, this is a breaking change.
We can't just delete GasParameter
but we can stop using it.
We'll also need to figure out how to upgrade long lived testnet.
a5eaa0a
to
b63c8f1
Compare
b63c8f1
to
fef2ad1
Compare
fef2ad1
to
ca4a16a
Compare
aptos-move/framework/aptos-framework/sources/configs/gas_schedule.move
Outdated
Show resolved
Hide resolved
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.
This is not a compatible change. I have not seen a clean indicating on slack that we are ok with breaking changes now.
Frankly, I also think this is a lot of new code in the last minute. There are little tests with this code, or do I miss them?
/// epoch for gas calculation of the entire next epoch. -- The data is one | ||
/// epoch older than ideal, but the Vm doesn't need to worry about reloading | ||
/// gas parameters after the first txn of an epoch. | ||
struct GasParameter has key, store { |
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.
You can't remove this without reset of the chain.
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.
That's something we'll discuss with @davidiw ... Yeah, I guess I have to keep and put it to the end of the file for now tho.
ca4a16a
to
36120c6
Compare
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.
Overall LGTM! Thanks for the hard work!
}; | ||
}; | ||
// Check the corner case that current_usage_bps drops before the first point. | ||
let (left, right) = if (current_usage_bps < vector::borrow(points, i).x) { |
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.
This could only happen when i == 0
, right?
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.
yes. it is a corner case.
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.
Then I wonder if it's clearer to replace the i
s with 0
s?
while (i <= len) { | ||
let cur = if (i == 0) { &Point {x: 0, y: 0} } else { vector::borrow(points, i - 1) }; | ||
let next = if (i == len) { &Point {x: BASIS_POINT_DENOMINATION, y: BASIS_POINT_DENOMINATION} } else { vector::borrow(points, i) }; | ||
assert!(cur.x < next.x && cur.y <= next.y, error::invalid_argument(EINVALID_MONOTONICALLY_NON_DECREASING_CURVE)); | ||
i = i + 1; | ||
} |
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.
Since you already checked the points at the two ends, you can just loop from i = 0 to (len - 2) inclusive and get rid of the special checks.
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.
I checked the points at the ends. but if doesn't tells me points[len - 1].x < end_point.x and points[len - 1].y <= end_point.y. So tho I have len points, actually we have len + 2 in total and have to compare len + 1 pairs that's why we loop i in [0, len], which has len + 1 iterations.
Point {x: 2000, y: 300}, | ||
Point {x: 3000, y: 600}, | ||
Point {x: 4000, y: 1000}, | ||
Point {x: 5000, y: 1500}, |
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.
@msmouse when we discussed previously, you said something like "we want the the cost to be 1.15x of the base cost at 50% utilization". However here it's base + 0.15x the max price hike. Just to double check: which one is correct?
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.
0.15x the max price hike
^ is what I meant, sorry
aptos-move/framework/aptos-framework/sources/configs/gas_schedule.move
Outdated
Show resolved
Hide resolved
14690e6
to
82e3a44
Compare
new_point(9500, 6372), | ||
new_point(9900, 9138), |
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.
beautiful
@@ -44,4 +46,11 @@ module aptos_framework::gas_schedule { | |||
// Need to trigger reconfiguration so validator nodes can sync on the updated gas schedule. | |||
reconfiguration::reconfigure(); | |||
} | |||
|
|||
public fun set_storage_gas_config(aptos_framework: &signer, config: StorageGasConfig) { |
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.
Why do we need this extra function? Can't we just call storage_gas::set_config directly?
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.
we can. But I think @vgao1996 insists we control gas-related params from a single module.
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.
Can't we just call storage_gas::set_config directly?
I'm totally fine with calling storage_gas::set_config
directly.
@vgao1996 insists we control gas-related params from a single module.
Well I did prefer us having a single entity to manage all gas-related stuff, but my definition for this single entity is pretty broad -- the combined group of "gas_schedule" & "storage" gas is conceptually still a single entity to me ;)
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.
With that being said I remember there were some technical issues preventing us from having everything in the same module in the first place, that is, modules cannot have cyclic dependencies and I have a feeling that the same issue also applies here.
Correct me if I'm wrong:
When we update the gas schedule, we want to call reconfigure()
, so the module this entry point is in needs to depend on reconfiguration
. However, storage_gas
has an on_reconfig
event so the reconfiguration module needs to depend on it. Therefore we cannot really call reconfigure()
in storage_gas
, but have to do it in a different module...
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.
@vgao1996 is correct about the reconfig. Yes, we want reconfig
after set_storage_gas_config but we cannot do it inside storage_gas. we can only set_config
inside.
*borrow_global_mut<StorageGasConfig>(@aptos_framework) = config; | ||
} | ||
|
||
public(friend) fun initialize(aptos_framework: &signer) { |
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.
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.
uhhhh, we definitely need a way to initialize the module. But what do you mean by republish?
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.
alright. It seems I have to rename it as "init_module"
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.
What you should be able to do is add an init_module
which calls this function here. This should cover the case when the module is loaded the first time during upgrade, whereas this function covers genesis.
82e3a44
to
c5e483f
Compare
c5e483f
to
dca00b5
Compare
} | ||
|
||
public(friend) fun on_new_block(epoch: u64) acquires StateStorageUsage { | ||
assert!( |
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.
Why add these checks now? If this fails, it crashes the entire network
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.
if the check doesn't fail, the next line will also fail, right?
usage: Usage, | ||
} | ||
|
||
public(friend) fun on_reconfig() { |
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.
You can delete friend-only functions now I think. cc @wrwg
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.
let me try.
} | ||
|
||
// ======================== deprecated ============================ | ||
friend aptos_framework::reconfiguration; |
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.
Can we just remove this then?
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.
it breaks the module_upgrade_test
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.
Weird cc @wrwg. I'm not aware of friend statements being checked in compatibility validation
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.
The friend_a_private feature is merged, but the feature isn't on by default. And it can't be turned on before rolled out, strictly spoken (because of replay).
For now any changes to friend function or module declarations will cause the upgrade to fail. But we can clean this once we upgraded testnet. So keep everything for now, remove later.
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.
I think it changes visibility. It took me a while to figure it out.
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.
The friend M
decls need to be a superset of the old module. With the new feature this check is not longer happening, but you can't use that yet.
friend aptos_framework::genesis; | ||
friend aptos_framework::reconfiguration; | ||
|
||
const ESTORAGE_GAS_CONFIG: u64 = 0; |
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.
These are not visible to end users, but can we still add comments here explaining what these error codes are for?
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.
to me it is quite self-explanatory. But I can add some.
*borrow_global_mut<StorageGasConfig>(@aptos_framework) = config; | ||
} | ||
|
||
fun init_module(aptos_framework: &signer) { |
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.
Did you verify that this works in the context of a package upgrade?
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.
Is there an easy way to do that?
is that something fn init_module()
tries to do?
calculate_gas(config.target_usage, usage, &config.write_curve) | ||
} | ||
|
||
public(friend) fun on_reconfig() acquires StorageGas, StorageGasConfig { |
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.
Can this fail? If this fails, it crashes the network
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.
all the code triggered by reconfig will crash the network if fails. I think we avoid all the possible overflow issues. But if you can anything risky, I am glad to discuss.
Forge is running suite
|
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.
fun calculate_gas(max_usage: u64, current_usage: u64, curve: &GasCurve): u64 { | ||
let capped_current_usage = if (current_usage > max_usage) max_usage else current_usage; | ||
let points = &curve.points; | ||
let num_points = vector::length(points); | ||
let current_usage_bps = capped_current_usage * BASIS_POINT_DENOMINATION / max_usage; | ||
|
||
// Check the corner case that current_usage_bps drops before the first point. | ||
let (left, right) = if (num_points == 0) { | ||
(&Point {x: 0, y: 0}, &Point {x: BASIS_POINT_DENOMINATION, y: BASIS_POINT_DENOMINATION}) | ||
} else { | ||
let (i, j) = (0, num_points - 1); | ||
while (i < j) { | ||
let mid = j - (j - i) / 2; | ||
if (current_usage_bps < vector::borrow(points, mid).x) { | ||
j = mid - 1; | ||
} else { | ||
i = mid; | ||
}; | ||
}; | ||
if (current_usage_bps < vector::borrow(points, 0).x) { | ||
(&Point {x: 0, y: 0}, vector::borrow(points, 0)) | ||
} else { | ||
// Check the corner case that current_usage_bps drops after the last point. | ||
( | ||
vector::borrow(points, i), | ||
if (i == num_points - 1) { | ||
&Point { x: BASIS_POINT_DENOMINATION, y: BASIS_POINT_DENOMINATION } | ||
} else { | ||
vector::borrow(points, i + 1) | ||
} | ||
) | ||
} | ||
}; | ||
|
||
curve.min_gas + (curve.max_gas - curve.min_gas) * (left.y + (current_usage_bps - left.x) * (right.y - left.y) / (right.x - left.x)) / BASIS_POINT_DENOMINATION | ||
} |
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.
@davidiw @vgao1996 Can our SMART smart contract engineers help review the code to reassure us that it will not abort in any cases?
- line 209 would not overflow because, line 186 checks
max_usage * BASIS_POINT_DENOMINATION <= u64::MAX
- Please review the binary search logic is valid.
- Please check 239 will not overflow or underflow( such as current_usage_bps - left.x).
Some background:
in validate_curve()
, we check 0 <= point.x, point.y <= BASIS_POINT_DENOMINATION, x is strictly monotonically increasing while y is monotonically non-decreasing;
max_gas * BASIS_POINT_DENOMINATION <= u64::MAX
.
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.
Yeah when I read this part of the code previously I also checked that the code would not overflow/underflow
let item_curve = base_8192_exponential_curve(10, 10000); | ||
let byte_curve = base_8192_exponential_curve(1, 1000); |
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.
@lightmark This is wrong.. the base 8192 curve is for max / min = 100x, the base 1M curve is for 1000x..
But none the less, we need to update these together with @vgao1996 's initial gas parameters. @vgao1996 if I'm reading your pending diff correctly, we should set min_price
for read to be 1000, which is the value of load_data.per_byte in the current schedule, right? And according to the discussion yesterday, the max will be 100x that, if we are going with the base 8192 curve.
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.
Yes
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.
We should set both in the same governance proposal.
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.
And / or we should send another PR to make init_module
set things to the "correct" numbers already?
Description
We implement a customizable curve for storage-based gas base unit and map x <-[0, 10000] to y<-[0, 10000].
storage gas = min_gas + (max_gas - min_gas) * (current_usage / target_utilization)
The curve is a vector of point(x, y), both are basis points.
Test Plan
ut
This change is