-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Set the block gas limit to the value returned by a contract call #10928
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 ( |
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.
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?
use types::ids::BlockId; | ||
|
||
/// Provides `call_contract` method | ||
pub trait CallContract { | ||
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.)
ethcore/src/client/client.rs
Outdated
.map_err(|_| CallError::StatePruned.to_string())?; | ||
|
||
let from = Address::default(); | ||
let transaction = transaction::Transaction { |
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll add a method although it's redundant because the field min_gas_limit
is public.
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then that should be named blockGasLimitContractTransitions
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.
Makes sense.
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.
Done in poanetwork#189.
ethcore/src/client/client.rs
Outdated
@@ -1511,6 +1511,49 @@ impl CallContract for Client { | |||
.map_err(|e| format!("{:?}", e)) | |||
.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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 state
incall_contract_before
is 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be feasible to just pass the block number into
blockGasLimit
as 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
ethcore/machine/src/machine.rs
Outdated
@@ -96,6 +96,11 @@ impl Machine { | |||
self.ethash_extensions.as_ref() | |||
} | |||
|
|||
/// Get a reference to the transaction filter, if present. | |||
pub fn tx_filter(&self) -> Option<&Arc<TransactionFilter>> { |
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 to have it here, since it's not used anywhere, or have I missed smth?
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.
Sorry, this is perhaps an unwanted effect of a rebase.
ethcore/machine/src/tx_filter.rs
Outdated
@@ -68,6 +68,11 @@ impl TransactionFilter { | |||
) | |||
} | |||
|
|||
/// Get a reference to the contract address. | |||
pub fn contract_address(&self) -> &Address { |
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 same here
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.
Agreed. This method can be removed as well as Machine::tx_filter
.
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.
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
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be better to keep these checks, whenever the gas limit contract returns None
?
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.
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. |
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.
lgtm (but do consider my comment about adding fn gas_limits()
.
@@ -331,6 +331,12 @@ pub trait Engine: Sync + Send { | |||
Ok(*header.author()) | |||
} | |||
|
|||
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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))
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this function is called verify_parent
, but this logic has nothing to do with the parent header
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for changes, but that's not exactly what I wanted 😅
I believe that files:
ethcore/verification/src/verification.rs
ethcore/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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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?
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.
A few questions and comments. I agree with @debris though: anything that is engine specific (and this is) and can be done in the engine, definitely should. I know this can be tricky at times.
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()
returnstrue
poanetwork/parity-ethereum#119