Skip to content
This repository was archived by the owner on Nov 6, 2020. It is now read-only.

Set the block gas limit to the value returned by a contract call #10928

Merged
merged 2 commits into from
Jan 13, 2020

Conversation

vkomenda
Copy link
Contributor

@parity-cla-bot
Copy link

It looks like @vkomenda signed our Contributor License Agreement. 👍

Many thanks,

Parity Technologies CLA Bot

@jam10o-new jam10o-new added A0-pleasereview 🤓 Pull request needs code review. M2-config 📂 Chain specifications and node configurations. M4-core ⛓ Core client code / Rust. labels Jul 30, 2019
@jam10o-new jam10o-new added this to the 2.7 milestone Jul 30, 2019
@vkomenda vkomenda force-pushed the poanetwork-block-gas-limit branch 2 times, most recently from 4c2477a to 17eb64d Compare August 27, 2019 12:14
@vkomenda
Copy link
Contributor Author

I've rebased the PR. Please review :)

@dvdplm
Copy link
Collaborator

dvdplm commented Aug 28, 2019

@vkomenda sorry about the churn on master recently. The test failure seem to be a simple import fix (Header from common-types).

Copy link
Collaborator

@dvdplm dvdplm left a 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
Copy link
Collaborator

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?

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.)

.map_err(|_| CallError::StatePruned.to_string())?;

let from = Address::default();
let transaction = transaction::Transaction {
Copy link
Collaborator

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;
Copy link
Collaborator

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)?).

Copy link
Contributor Author

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.

Copy link
Collaborator

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in poanetwork#189.

@@ -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> {
Copy link
Collaborator

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

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)?

Copy link
Contributor

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 in call_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.

Copy link
Contributor

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) {
Copy link
Collaborator

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() {
Copy link
Collaborator

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?

@vkomenda
Copy link
Contributor Author

@vkomenda sorry about the churn on master recently. The test failure seem to be a simple import fix (Header from common-types).

@dvdplm, which module has the error? I couldn't find it with cargo test.

@vkomenda vkomenda force-pushed the poanetwork-block-gas-limit branch from 17eb64d to e5d88fd Compare August 28, 2019 17:22
@dvdplm
Copy link
Collaborator

dvdplm commented Aug 28, 2019

which module has the error? I couldn't find it with cargo test.

oh I bet you don't see the CI output right? It's here:

error[E0412]: cannot find type `Header` in this scope
   --> ethcore/private-tx/src/key_server_keys.rs:164:44
    |
164 |         fn call_contract_before(&self, _header: &Header, _address: Address, _data: Bytes) -> Result<Bytes, String> {
    |                                                  ^^^^^^ not found in this scope
help: possible candidates are found in other modules, you can import them into scope
    |
140 |     use ethjson::blockchain::Header;
    |
140 |     use ethjson::blockchain::header::Header;
    |
140 |     use types::encoded::Header;
    |
140 |     use types::header::Header;
    |

error: aborting due to previous error

For more information about this error, try `rustc --explain E0412`.
error: Could not compile `ethcore-private-tx`.

FYI you might want to run cargo test --all --release in the repo root to make sure you run all tests. :)

@afck
Copy link
Contributor

afck commented Aug 30, 2019

The test was failing because it expected verify_block_basic to check the gas limit, which we moved to verify_block_family now. (And because of a missing header = good.clone();.)
Is it okay for verify_block_basic to not check the gas limit anymore?

Otherwise we'd have to add something like fn use_gas_limit_contract(&self, block: &BlockNumber) -> bool to Engine that doesn't call the contract and only looks into the configuration to find out whether it would use the contract at that block number, and check the gas limit in verify_header_params (used in verify_block_basic) only if use_gas_limit_contract returns false.

@varasev: The meaning of blockGasLimit has changed now. It is supposed to return the next block's gas limit. Is that practical in posdao-contracts?

@varasev
Copy link
Contributor

varasev commented Aug 30, 2019

The meaning of blockGasLimit has changed now. It is supposed to return the next block's gas limit. Is that practical in posdao-contracts?

Ok

@varasev
Copy link
Contributor

varasev commented Aug 30, 2019

The meaning of blockGasLimit has changed now. It is supposed to return the next block's gas limit. Is that practical in posdao-contracts?

What about block #1? As far as I understand, the engine will call blockGasLimit to get the gas limit for the next block. It means that when it is called on the block #1, the blockGasLimit will return a limit for block #2. What limit will be for block #1 then?

@afck
Copy link
Contributor

afck commented Aug 30, 2019

It will actually be called "after the genesis block", I think. So it will be called at the point where the block.number is still 0 and the EVM state is the state between blocks 0 and 1.

@@ -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>> {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@@ -68,6 +68,11 @@ impl TransactionFilter {
)
}

/// Get a reference to the contract address.
pub fn contract_address(&self) -> &Address {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same here

Copy link
Contributor Author

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.

Copy link
Contributor

@VladLupashevskyi VladLupashevskyi left a 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.

@vkomenda
Copy link
Contributor Author

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 Engine::populate_from_parent. Do you think the miner can mine transactions in a block before its header has been constructed?

@VladLupashevskyi
Copy link
Contributor

@vkomenda I just have noticed that gas_range_target is not updated with the value from contract and it's used in block opening here:

https://github.com/paritytech/parity-ethereum/blob/bceb1d569147cf113520b904bf84255d100b4d1d/ethcore/src/client/client.rs#L2332

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 OpenBlock , however I have checked and tx is included without any issues.

@afck afck force-pushed the poanetwork-block-gas-limit branch from e8d282c to 1f96ce0 Compare September 26, 2019 10:24
Copy link
Contributor

@afck afck left a 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() })));
}
}
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@afck
Copy link
Contributor

afck commented Sep 27, 2019

I can't reproduce that test failure. For me, cargo test --release --all works.
(Rebasing fixed it.)
(Rebasing again broke it again.)

@afck afck force-pushed the poanetwork-block-gas-limit branch from 1f96ce0 to ea63954 Compare September 30, 2019 08:05
@afck afck force-pushed the poanetwork-block-gas-limit branch 2 times, most recently from ea9f4b6 to 6644b56 Compare October 8, 2019 14:03
@afck afck force-pushed the poanetwork-block-gas-limit branch from 6644b56 to 52c53c7 Compare October 15, 2019 12:03
@afck
Copy link
Contributor

afck commented Oct 15, 2019

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.

Copy link
Collaborator

@dvdplm dvdplm left a 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> {
Copy link
Collaborator

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?

Copy link
Contributor

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),
}

Copy link
Collaborator

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?

Copy link
Contributor

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 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))

Copy link
Collaborator

@debris debris left a 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());
}
}
Copy link
Collaborator

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

Copy link
Collaborator

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

Copy link
Contributor

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.

Copy link
Collaborator

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

Copy link
Contributor

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.

Copy link
Contributor

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?

Copy link
Collaborator

@dvdplm dvdplm left a 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.

@varasev
Copy link
Contributor

varasev commented Oct 28, 2019

Hi guys, please consider @afck's last answers. Are there still some things we need to improve in this PR?

@afck afck force-pushed the poanetwork-block-gas-limit branch from 77bc863 to ab9e912 Compare October 29, 2019 10:00
@afck
Copy link
Contributor

afck commented Oct 29, 2019

Another test failure that I can't reproduce locally.

@ordian
Copy link
Member

ordian commented Jan 10, 2020

It seems that the code is reviewed well. Please resolve the merge conflicts, so that we can merge the PR :)

afck added 2 commits January 10, 2020 20:52
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.
@afck afck force-pushed the poanetwork-block-gas-limit branch from ab9e912 to 4d52c90 Compare January 11, 2020 11:07
@afck
Copy link
Contributor

afck commented Jan 11, 2020

Thanks! I rebased it.

@ordian ordian merged commit 73354d8 into openethereum:master Jan 13, 2020
@ordian ordian added A8-looksgood 🦄 Pull request is reviewed well. and removed A0-pleasereview 🤓 Pull request needs code review. labels Jan 13, 2020
@afck afck deleted the poanetwork-block-gas-limit branch January 13, 2020 10:32
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A8-looksgood 🦄 Pull request is reviewed well. M2-config 📂 Chain specifications and node configurations. M4-core ⛓ Core client code / Rust.
Projects
None yet
Development

Successfully merging this pull request may close these issues.