Set the block gas limit to the value returned by a contract call#10928
Conversation
|
It looks like @vkomenda signed our Contributor License Agreement. 👍 Many thanks, Parity Technologies CLA Bot |
4c2477a to
17eb64d
Compare
|
I've rebased the PR. Please review :) |
|
@vkomenda sorry about the churn on master recently. The test failure seem to be a simple import fix ( |
dvdplm
left a comment
There was a problem hiding this comment.
My main concern here regards the changes to the Engine trait. I'm reluctant to add more code to it than we absolutely need. It seems like it's needed only in verification? Is it possible to coalesce the gas limit logic into a single fn gas_limits() -> (u64, u64) and have the default impl return (max, min) and Aura do the custom logic?
| /// Like `call`, but with various defaults. Designed to be used for calling contracts. | ||
| fn call_contract(&self, id: BlockId, address: Address, data: Bytes) -> Result<Bytes, String>; | ||
|
|
||
| /// Makes a constant call to a contract, at the beginning of the block corresponding to the given header, i.e. with |
There was a problem hiding this comment.
Maybe dumb q: what is "constant" about the call? Isn't it called for every block of the override is in effect?
There was a problem hiding this comment.
It's "constant" in the sense that it doesn't create a transaction and modify the blockchain's actual state. Not sure what the right word for this is.
There was a problem hiding this comment.
Maybe "stateless" or "immutable"? But technically the contract ABI can allow a contract call that modifies the state. I think assuming that the call modifies the state would be more complete.
There was a problem hiding this comment.
Not sure what the best wording is. In this blog post it's called "local" and "read-only call", as opposed to "verified" and "potentially state-changing transaction".
(To clarify: This can be used with calls that would modify the state. It's just that it throws away the modified state instead of creating a transaction which, when mined, would actually change the blockchain's EVM state.)
| .map_err(|_| CallError::StatePruned.to_string())?; | ||
|
|
||
| let from = Address::default(); | ||
| let transaction = transaction::Transaction { |
There was a problem hiding this comment.
I think this merits a comment: looks like it's a "fake" tx used to satisfy the type signature of do_virtual_call? Maybe it can be extracted to its own method to make the intent clear?
| ))); | ||
| } | ||
| } else { | ||
| let min_gas_limit = engine.params().min_gas_limit; |
There was a problem hiding this comment.
Seems like there should be an engine.min_gas_limit() method (or perhaps engine.gas_limits() -> (u64, u64)?).
There was a problem hiding this comment.
I'll add a method although it's redundant because the field min_gas_limit is public.
There was a problem hiding this comment.
min_gas_limit being public is… not that great! There's probably some historical reason for that.
The point I was trying to make was this: I'm a bit hesitant about this PR because it adds code that is useful only to Aura. Ideally we should architect things such that engine-specific behaviour stays in that engine's code. One idea I had was to have a single gas_limits() method on Engine which would let the Aura implementation to run the logic it needs without "polluting" other engines.
There was a problem hiding this comment.
This can be done, I think.
@varasev, is it OK to move blockGasLimitContract from the general engine parameter set to the AuRa parameter set? I would also recommend renaming it as blockGasLimitContracts since we are changing it to support only a map.
There was a problem hiding this comment.
Yes, it's OK to move, but don't rename, please, because the map still assumes that there is only one contract at a time.
If you move this parameter, please also move it in our https://github.com/poanetwork/parity-ethereum/tree/aura-pos branch.
There was a problem hiding this comment.
@varasev OK, I'll copy that in aura-pos. Are you sure about the name? Wouldn't plural look better alongside blockRewardContractTransitions?
There was a problem hiding this comment.
Then that should be named blockGasLimitContractTransitions
| .map(|executed| executed.output) | ||
| } | ||
|
|
||
| fn call_contract_before(&self, header: &Header, address: Address, data: Bytes) -> Result<Bytes, String> { |
There was a problem hiding this comment.
Why is that even needed? Why can't use use call_contract and pass BlockId with parent_hash?
There was a problem hiding this comment.
We can definitely use call later on in this function. I'm not sure about replacing call_contract_before with call_contract because state is defined differently in those two functions.
There was a problem hiding this comment.
It looks like the use of mut state in call_contract_before is incorrect because it is then dropped. So whatever changes call makes to it are forgotten.
@afck, what are your thoughts on using call_contract instead of call_contract_before (which should fix the use of mutable state)?
There was a problem hiding this comment.
You're probably right: We should just pass in the block number into block_gas_limit as a parameter.
Originally this was motivated by a smart contract (written before this Rust code) that had a limitBlockGas getter that returned whether the gas limit should be reduced for the current block. So we needed a ("not exactly sane") way to call this with a state that didn't exist yet: where the block number is the number of the block that we're currently creating. The new blockGasLimit implementation has a similar problem: It calls a few methods that use the current block number.
@varasev: Would it be feasible to just pass the block number into blockGasLimit as an argument instead?
It looks like the use of
mut stateincall_contract_beforeis incorrect because it is then dropped.
Client::call_contract drops the state as well: It ends up via call in do_virtual_call, where it is used to create an Executive instance and a diff. It's not persisted, however, and that's intended: These methods are meant to run calls locally and return the call's output. They don't create transactions and thus can't modify the actual EVM state.
There was a problem hiding this comment.
Would it be feasible to just pass the block number into
blockGasLimitas an argument instead?
No, the blockGasLimit calls other getters of other contracts to determine the gas limit for the current block. The returned values of those getters depend on the current state of the current block.
| if let Some(limit) = engine.maximum_gas_limit() { | ||
| if header.gas_limit() > &limit { | ||
| return Err(From::from(BlockError::InvalidGasLimit(OutOfBounds { min: None, max: Some(limit), found: *header.gas_limit() }))); | ||
| if let Some(gas_limit) = engine.gas_limit_override(header) { |
There was a problem hiding this comment.
I'm pretty sure it won't really work correctly. What guarantees do we have that parent_hash is already imported? State-touching checks are only allowed further down in verification afair.
| let max_gas = parent_gas_limit + parent_gas_limit / gas_limit_divisor; | ||
| if header.gas_limit() <= &min_gas || header.gas_limit() >= &max_gas { | ||
| return Err(From::from(BlockError::InvalidGasLimit(OutOfBounds { min: Some(min_gas), max: Some(max_gas), found: *header.gas_limit() }))); | ||
| if engine.gas_limit_override(header).is_none() { |
There was a problem hiding this comment.
That's pretty wasteful to call the contract twice during verification (one for verify_header and second time here), why not move the other gas_limit verification in here?
17eb64d to
e5d88fd
Compare
oh I bet you don't see the CI output right? It's here: FYI you might want to run |
443ffb9 to
c03c4ba
Compare
|
The test was failing because it expected Otherwise we'd have to add something like @varasev: The meaning of |
Ok |
What about block |
|
It will actually be called "after the genesis block", I think. So it will be called at the point where the |
| } | ||
|
|
||
| /// Get a reference to the transaction filter, if present. | ||
| pub fn tx_filter(&self) -> Option<&Arc<TransactionFilter>> { |
There was a problem hiding this comment.
Why do we need to have it here, since it's not used anywhere, or have I missed smth?
There was a problem hiding this comment.
Sorry, this is perhaps an unwanted effect of a rebase.
| } | ||
|
|
||
| /// Get a reference to the contract address. | ||
| pub fn contract_address(&self) -> &Address { |
There was a problem hiding this comment.
Agreed. This method can be removed as well as Machine::tx_filter.
VladLupashevskyi
left a comment
There was a problem hiding this comment.
Don't we need to let the miner know about gas limit transitions? It seems to me that following the logic of having contract for gas limit configurations should mean that the miner would have to change the limit on the fly, otherwise you cannot be sure that transactions will get mined after such a transition until the validator node configuration is updated.
We set the gas limit once for every block at the time of header construction in |
|
@vkomenda I just have noticed that Which would lead to issues that miner wouldn't allow tx to be included to the block if its gas cost is more then it's set for |
e8d282c to
1f96ce0
Compare
afck
left a comment
There was a problem hiding this comment.
I removed some unused imports, and the unused functions as discussed, rebased on master and squashed the commits (because a lot of the changes in them have been reverted now).
I just have noticed that gas_range_target is not updated
I'm not sure whether it should be: To my mind, the periods where the contract returns Some(value) should be considered exceptions where we suspend the normal gas limit policy and use value instead. After that, maybe it would be best to return to the previous behavior, with the previous target range?
| if header.gas_limit() > &limit { | ||
| return Err(From::from(BlockError::InvalidGasLimit(OutOfBounds { min: None, max: Some(limit), found: *header.gas_limit() }))); | ||
| } | ||
| } |
There was a problem hiding this comment.
Would it be better to keep these checks, whenever the gas limit contract returns None?
There was a problem hiding this comment.
They've moved to line 331 below.
|
|
1f96ce0 to
ea63954
Compare
ea9f4b6 to
6644b56
Compare
6644b56 to
52c53c7
Compare
|
I rebased again, due to merge conflicts. Please let me know if there are any further changes you would like me to make to the PR. |
dvdplm
left a comment
There was a problem hiding this comment.
lgtm (but do consider my comment about adding fn gas_limits().
|
|
||
| /// Overrides the block gas limit. Whenever this returns `Some` for a header, the next block's gas limit must be | ||
| /// exactly that value. | ||
| fn gas_limit_override(&self, _header: &Header) -> Option<U256> { |
There was a problem hiding this comment.
So I think I made this suggestion before but perhaps it got lost.
In order to keep the Engine trait from growing ever fatter, how about we have a fn gas_limits(&self) -> Option<(U256, U256)> that return the min/max limit. The default impl could return (self.params().min_gas_limit, self.params().max_gas_limit) and then Aura can provide its own impl with the limit override logic tucked away nicely. This would require refactoring other code around the project but lets us remove maximum_gas_limit(&self) and saves us from adding two new methods.
Not sure if you tried this and it doesn't work or if you feel it's a bad idea?
There was a problem hiding this comment.
Sorry, I missed that!
I'm not sure about this idea: When the engine changes the gas limit, we don't want to do these checks:
At the moment, it seems to be impossible to produce a valid block if the interval based on the gas limit divisor doesn't overlap with the interval based on the engine's min and max gas limits.
Maybe engines that use a gas limit contract should just use a gas limit divisor of 1, but that would still prevent them from more than doubling the gas limit from one block to the next.
But we could make gas_limits return a bool as well, that tells us whether it's an override. Or it could return an enum?
enum GasLimits {
MinMax(U256, U256),
Override(U256),
}There was a problem hiding this comment.
Hmm, so why do you need to know that there was an override? Why isn't it enough to know what the bounds must be at that block? In this PR there are 3 error cases: gas too low, too high, overridden but mismatching – why is it important to know that the error came from an override?
There was a problem hiding this comment.
There seem to be two different intervals for the block gas limit:
- the engine's min/max and
- the range calculated using the gas limit divisor, which allows us to change the limit only by a certain percentage compared to the previous block.
The second limitation can conflict with the first, if the engine makes sudden changes to the gas limit. So I think it should be disabled if there is an override.
I'm not really sure at all what's the best way to approach this. To give you some context, the gist is:
In POSDAO, there will be rather expensive implicit calls to the governance contracts once per week, to distribute rewards and select new validators. While these are ongoing, we want to limit the gas available for transactions, so that the total CPU usage per block remains roughly constant. (For details see: poanetwork#119 (comment))
debris
left a comment
There was a problem hiding this comment.
I reviewed the code carefully. It look good, just a few minor grumbles
| OutOfBounds { min: None, max: Some(limit), found: *header.gas_limit() } | ||
| ).into()); | ||
| } | ||
| } |
There was a problem hiding this comment.
this function is called verify_parent, but this logic has nothing to do with the parent header
There was a problem hiding this comment.
what if you move this verification to engine's fn verify_block_basic? If you do that you will not need to add additional methods to the Engine trait
There was a problem hiding this comment.
Done.
I also moved the checks back to verify_header_params that were originally there.
Since I'm now calling gas_limit_override three times instead of once, I added a cache to memoize the results, to avoid executing the same contract call multiple times.
There was a problem hiding this comment.
Thanks for changes, but that's not exactly what I wanted 😅
I believe that files:
ethcore/verification/src/verification.rsethcore/engine/src/engine.rs
can and should not be modified by this pr.
All the changes can be encapsulated in ethcore/engines/authority-round/src/lib.rs file
There was a problem hiding this comment.
How could this be achieved without modifying ethcore/verification/src/verification.rs? After all, it's an exception to block verification to allow for gas limits set by the engine.
There was a problem hiding this comment.
What we could do is move all the gas limit related verification code from verification.rs to the engines, and make the current behavior the Engine trait's default implementation. Do you think that would be better?
|
Hi guys, please consider @afck's last answers. Are there still some things we need to improve in this PR? |
77bc863 to
ab9e912
Compare
|
Another test failure that I can't reproduce locally. |
|
It seems that the code is reviewed well. Please resolve the merge conflicts, so that we can merge the PR :) |
Lower gas limit if TxPermission.limitBlockGas. Call blockGasLimit before every block. Make the block gas limit contract a separate config option. Add `info` level logging of block gas limit switching block-gas-limit subcrate and responses to review comments simplified call_contract_before moved block_gas_limit_contract_transitions to AuRa params removed call_contract_before Update and fix test_verify_block. Remove some unused imports and functions.
ab9e912 to
4d52c90
Compare
|
Thanks! I rebased it. |
This PR allows to have programmable block gas limit.
See also:
TxPermission.limitBlockGas()returnstruepoanetwork/parity-ethereum#119