Skip to content
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

core/vm: set basefee to 0 internally on eth_call #28470

Merged
merged 8 commits into from
Nov 8, 2023

Conversation

karalabe
Copy link
Member

@karalabe karalabe commented Nov 6, 2023

After shipping 1559, we've added support for running eth_call (and it's derivatives) with a NoBaseFee vm.config flag. This is needed because people usually don't set gas prices on eth calls, so all the usual checks around maxfee > basefee, etc cannot pass. https://github.com/ethereum/go-ethereum/blob/master/core/state_transition.go#L293

One place we didn't handle is the basefee opcode when running the EVM. Currently that returns a non-zero number, even if NoBaseFee was requested. This poses an interesting issue:

tx.gasprice - block.basefee;

This above Solidity code reverts if ran with a gasprice of 0. That cannot happen for transaction execution, so the live network isn't impacted. And accessing the basefee in an eth_call seems pointless so people don't really hit it.

However, running a gas estimation on the above code will always fail if the user does not provide a gas price to the estimator. Whilst it really makes sense to provide a gas price (since tx execution might depend on it), if we allow 0 priced calls/estimations, then we must set the basefee to 0 too. Otherwise we create an impossible scenario (basefee > total fee), and all bets are off when the execution starts out with junk state.

FWIW, it would also make sense to consider if we want to disallow gas estimation with 0 price, since it feels wrong. My gut feeling is yes, but nobody knows what we're breaking (especially since ethclient itself fogot to forward the gas fields until this week).

@karalabe karalabe added this to the 1.13.5 milestone Nov 6, 2023
core/vm/eips.go Outdated
Comment on lines 218 to 219
if interpreter.evm.Config.NoBaseFee {
scope.Stack.push(new(uint256.Int))
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of checking for NoBaseFee in the production opcode, can't we just set the interpreter.evm.Context.BaseFee to a new(uint256.Int) in the NoBaseFee-situation?

Copy link
Member Author

Choose a reason for hiding this comment

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

I did consider that too. The only catch is that some code paths feed us the context directly, and we're not copying currently, so setting the basefee would modify the passed in context. We'd need to add some copying to avoid that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Like this

diff --git a/core/vm/eips.go b/core/vm/eips.go
index 44d4ba9cff..35f0a3f7c2 100644
--- a/core/vm/eips.go
+++ b/core/vm/eips.go
@@ -215,12 +215,8 @@ func opTstore(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]b
 
 // opBaseFee implements BASEFEE opcode
 func opBaseFee(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) {
-	if interpreter.evm.Config.NoBaseFee {
-		scope.Stack.push(new(uint256.Int))
-	} else {
-		baseFee, _ := uint256.FromBig(interpreter.evm.Context.BaseFee)
-		scope.Stack.push(baseFee)
-	}
+	baseFee, _ := uint256.FromBig(interpreter.evm.Context.BaseFee)
+	scope.Stack.push(baseFee)
 	return nil, nil
 }
 
diff --git a/core/vm/evm.go b/core/vm/evm.go
index 2c6cc7d484..1f84ff7660 100644
--- a/core/vm/evm.go
+++ b/core/vm/evm.go
@@ -133,6 +133,9 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig
 		chainConfig: chainConfig,
 		chainRules:  chainConfig.Rules(blockCtx.BlockNumber, blockCtx.Random != nil, blockCtx.Time),
 	}
+	if config.NoBaseFee {
+		evm.Context.BaseFee = new(big.Int)
+	}
 	evm.interpreter = NewEVMInterpreter(evm)
 	return evm
 }

Copy link
Contributor

Choose a reason for hiding this comment

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

I did consider that too.

Ah I didn't see this comment. But I don't think you are quite correct -- the blockCtx is passed by value, not ref, in the path I used.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, I think Martin's approach is better, BaseFee is an environment context.

We can also leave a TODO for BlobBaseFee, we might face the same problem for this opcode too.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll switch to Martin's approach, but we need to be aware that BaseFee form the context is used in many locations, each potentially becoming a footgun with 0.

Copy link
Member Author

Choose a reason for hiding this comment

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

@rjl493456442 Wrt the blob fee, I think we can piggie-back on the same flag. BaseFee, BlobBaseFee, the concept is the same and setting it to zero should also be the same.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ugh, no, Martin's fix isn't good (neither is mine).

Currently the logic is that we only disable base fee checks if 1) NoBaseFee is set and 2) gas prices are 0. Both our suggested fixes unilaterally nuke it out always, which is bad.

Martin's proposal cannot work because you don't know during construction time if tx will be run with or without gas price. My solution could work if I pulled the gas price up too, but it's a bit ugh. Maybe a cleaner solution is to set/rest the field before actually executing the bytecode for each tx individually. .Will look into that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe Martin's does work. The EVM is reconstructed for each tx.

@MariusVanDerWijden
Copy link
Member

MariusVanDerWijden commented Nov 6, 2023

I thought the whole idea is that basefee is never nil.
In core/state_transition we say

// This will panic if baseFee is nil, but basefee presence is verified
// as part of header validation.

In my opinion the issue is not with the opcode then but with the way we construct the evm.Context without a valid basefee

edit: Ahh nvm, now I get it, thanks Martin

@holiman
Copy link
Contributor

holiman commented Nov 6, 2023

I thought the whole idea is that basefee is never nil.

Yeah the problem isn't that it's nil, it's non-nil (and quite large). So the problem is that basefee is larger than gasprice

Comment on lines 329 to 330
// Skip the checks if gas fields are zero and blobBaseFee was explicitly disabled (eth_call)
if !st.evm.Config.NoBaseFee && msg.BlobGasFeeCap.BitLen() > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

The way this became changed now, the check is skipped if msg.BlobGasFeeCap == 0. And msg comes from the transaction we're about to execute. In other words, we omit a check depending on either

  1. If a debug-setting is enabled, OR
  2. A certain field in a transaction is zero.

It's not obvious to me why it is safe to do number 2)

Copy link
Member Author

Choose a reason for hiding this comment

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

Shit, good catch. Should have been an ||

@@ -327,7 +327,7 @@ func (st *StateTransition) preCheck() error {
if st.evm.ChainConfig().IsCancun(st.evm.Context.BlockNumber, st.evm.Context.Time) {
if st.blobGasUsed() > 0 {
// Skip the checks if gas fields are zero and blobBaseFee was explicitly disabled (eth_call)
if !st.evm.Config.NoBaseFee && msg.BlobGasFeeCap.BitLen() > 0 {
if !st.evm.Config.NoBaseFee || msg.BlobGasFeeCap.BitLen() > 0 {
Copy link
Contributor

@holiman holiman Nov 7, 2023

Choose a reason for hiding this comment

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

I had to, again, look at it for a while before my brain parsed it out fully. Yeah it's ok now, but I'd prefer having it rewritten for clarity:

// skip checks if basefee was disabled and blobBaseFee is zero (eth_call) 
skipCheck := st.evm.Config.NoBaseFee && msg.BlobGasFeeCap.BitLen() == 0
if !skipCheck {

Copy link
Member Author

Choose a reason for hiding this comment

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

Fair enough

Copy link
Member Author

Choose a reason for hiding this comment

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

Pushed this fix and also added it to the 1559 basefee

@karalabe
Copy link
Member Author

karalabe commented Nov 7, 2023

I've removed the check for blobfee > 2^256. We don't need it because we use uint256 in blobtxs, so it cannot happen, apart from the last mile converstion to big.Int internally. We should start converting more things to uint256 as it would get rid of these cornercases.

Copy link
Contributor

@holiman holiman left a comment

Choose a reason for hiding this comment

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

LGTM

@karalabe karalabe merged commit 470dba8 into ethereum:master Nov 8, 2023
2 checks passed
devopsbo3 pushed a commit to HorizenOfficial/go-ethereum that referenced this pull request Nov 10, 2023
* core/vm: set basefee to 0 internally on eth_call

* core: nicer 0-basefee, make it work for blob fees too

* internal/ethapi: make tests a bit more complex

* core: fix blob fee checker

* core: make code a bit more readable

* core: fix some test error strings

* core/vm: Get rid of weird comment

* core: dict wrong typo
devopsbo3 added a commit to HorizenOfficial/go-ethereum that referenced this pull request Nov 10, 2023
devopsbo3 added a commit to HorizenOfficial/go-ethereum that referenced this pull request Nov 10, 2023
@hkalodner
Copy link
Contributor

I wanted to note that this change broke gas estimation in our code when called without a gas price set for the following

 uint256 submissionFee = calculateRetryableSubmissionFee(data.length, block.basefee);

    function calculateRetryableSubmissionFee(uint256 dataLength, uint256 baseFee)
        public
        view
        override(AbsInbox, IInboxBase)
        returns (uint256)
    {
        // Use current block basefee if baseFee parameter is 0
        return (1400 + 6 * dataLength) * (baseFee == 0 ? block.basefee : baseFee);
    }

When the transaction is estimated, block.basefee now returns 0 which differs significantly from on-chain execution leading to incorrect estimation. We have some workarounds so this isn't a blocker for us, but it seems to generally make estimation execution differ more from on-chain execution than the previous behavior.

I'm wondering whether or not you considered resolving the tx.gasprice - block.basefee issue by setting tx.gasprice = block.basefee = current base fee instead of setting both equal to 0. That would solve the same issue without setting the base fee to 0, though could definitely be missing other factors.

As I mentioned we have a workaround, but this lead to some breakage so I thought I'd see if there was interest in alternative options.

@s1na
Copy link
Contributor

s1na commented Dec 8, 2023

by setting tx.gasprice = block.basefee = current base fee instead of setting both equal to 0

This will lead to bad UI. As accounts would need funds for fee so state overrides always have to be used.

However IF that's ok for you you can already set the gasPrice field of the transaction to the current block's base fee or higher and it should work.

A crucial note here is: block base fee is set to 0 only if tx.gasPrice is also 0.

@karalabe
Copy link
Member Author

karalabe commented Dec 8, 2023

@hkalodner As @s1na also mentioned, you should really be doing gas pricing first and then estimation. Estimation needs good prices anyway and if the prices are non zero, then estimate will also use the real basefee.

@karalabe
Copy link
Member Author

karalabe commented Dec 8, 2023

E.g. in our abigen generated code we do tx.gasprice = eth_gasPrice; and then do eth_estimateGas.

Dergarcon pushed a commit to specialmechanisms/mev-geth-0x2mev that referenced this pull request Jan 31, 2024
* core/vm: set basefee to 0 internally on eth_call

* core: nicer 0-basefee, make it work for blob fees too

* internal/ethapi: make tests a bit more complex

* core: fix blob fee checker

* core: make code a bit more readable

* core: fix some test error strings

* core/vm: Get rid of weird comment

* core: dict wrong typo
colinlyguo pushed a commit to scroll-tech/go-ethereum that referenced this pull request Oct 31, 2024
* core/vm: set basefee to 0 internally on eth_call

* core: nicer 0-basefee, make it work for blob fees too

* internal/ethapi: make tests a bit more complex

* core: fix blob fee checker

* core: make code a bit more readable

* core: fix some test error strings

* core/vm: Get rid of weird comment

* core: dict wrong typo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants