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

EVM: writeContract fixes, never return contract code as RETURNDATA #901

Merged
merged 1 commit into from
Dec 12, 2021

Conversation

jlokier
Copy link
Contributor

@jlokier jlokier commented Dec 7, 2021

This fixes #867 "EIP-170 related consensus error at Goerli block 5080941", and equivalent on other networks.

This combines a change on the EVM-caller side with an EVM-side change from @jangko 6548ff9 "fixes CREATE/CREATE2's returndata bug", making the caller EVM ignore any data except from REVERT.

Either change works by itself. The reason for both is to ensure we definitely comply with ambiguous EVMC expectations from either side of that boundary, and it makes the internal API clearer.

As well as fixing a specific consensus issue, there are some other EVM logic changes too: Refactored writeContract, how RETURNDATA is handled inside the EVM, and changed behaviour with quirks before EIP-2 (Homestead).

The fix allows sync to pass block 5080941 on Goerli, and probably equivalent on other networks. Here's a trace at batch 5080897..5081088:

TRC 2021-10-01 21:18:12.883+01:00 Persisting blocks                  file=persist_blocks.nim:43 fromBlock=5080897 toBlock=5081088
...
DBG 2021-10-01 21:18:13.270+01:00 Contract code size exceeds EIP170  topics="vm computation" file=computation.nim:236 limit=24577 actual=31411
DBG 2021-10-01 21:18:13.271+01:00 gasUsed neq cumulativeGasUsed      file=process_block.nim:68 block=5080941/0A3537BC5BDFC637349E1C77D9648F2F65E2BF973ABF7956618F854B769DF626 gasUsed=3129669 cumulativeGasUsed=3132615
TRC 2021-10-01 21:18:13.271+01:00 peer disconnected                  file=blockchain_sync.nim:407 peer=<IP:PORT>

Although it says "Contract code size" and "gasUsed", this bug is more general than either contract size or gas. It's due to incorrect behaviour of EVM instructions RETURNDATA and RETURNDATASIZE.

Sometimes when writeContract decides to reject writing the contract for any of several reasons (for example just insufficient gas), the unwritten contract code was being used as the "return data", and given to the caller. If the caller used RETURNDATA or RETURNDATASIZE ops, those incorrectly reported the contract code that didn't get written.

EIP-211 (https://eips.ethereum.org/EIPS/eip-211) describes RETURNDATA:

"CREATE and CREATE2 are considered to return the empty buffer in the
success case and the failure data in the failure case".

The language is ambiguous. In fact "failure case" means when the contract uses REVERT to finish. It doesn't mean other failures like out of gas, EIP-170 limit, EIP-3541, etc.

To be thorough, and to ensure we always do the right thing with real EVMC when that's finalised, this patch fixes the RETURNDATA issue in two places, either of which make Goerli block 5080941 pass.

writeContract has been refactored to be caller, and so has where it's called. It sets an error in the usual way if contract writing is rejected -- that's anticipating EVMC, where we'll use different error codes later.

Overall four behaviour changes:

  1. On the callee side, it doesn't set c.outputData except for REVERT.

  2. On the caller side, it doesn't read child.outputData except for REVERT.

  3. There was a bug in processing before Homestead fork (EIP-2). We did not match the spec or other implementations; now we do. When there's insufficient gas, before Homestead it's treated as success but with an empty contract.

    https://github.com/ethereum/pyethereum/blob/d117c8f3fd93359fc641fd850fa799436f7c43b5/ethereum/processblock.py#L304
    https://github.com/ethereum/go-ethereum/blob/401354976bb4/core/vm/instructions.go#L586

  4. The Byzantium check has been removed, as it's unnecessary.

Copy link
Member

@zah zah left a comment

Choose a reason for hiding this comment

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

How come this is not covered by any test?

Is it possible to add it to the test suite by capturing it a premix test?

proc writeContract*(c: Computation) =
template withExtra(tracer: untyped, args: varargs[untyped]) =
tracer args, newContract=($c.msg.contractAddress),
blockNumber=c.vmState.blockNumber, blockHash=($c.vmState.blockHash)
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you are trying to re-invent the logScope feature of Chronicles here:

https://github.com/status-im/nim-chronicles#logscope

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How come this is not covered by any test?

Is it possible to add it to the test suite by capturing it a premix test?

I guess I'd need a Goerli archive node to capture in premix? On top, I don't know how to make a premix test, but that's a good idea. Probably several tests covering different issues here.

There is a lot to learn (and a I think lot of time) just to make one of those, though - and passing consensus on Goerli is an effective test for the practical issue. I think we have to choose where to spend this time.

That said, @jangko offered to make such a test in this case (a while ago) - and that would be better as he's familiar with it, and perhaps has the archive node set up. It would be even better if they were submitted to fixtures/Hive though - all these kinds of tests should go there really, as they apply to all clients.

I've generally found our premix-generated tests rather opaque: They must be passed, but don't explain what they are testing and are hard to change if you didn't write them.

This fixes #867 "EIP-170 related consensus error at Goerli block 5080941", and
equivalent on other networks.

This combines a change on the EVM-caller side with an EVM-side change from
@jangko 6548ff9 "fixes CREATE/CREATE2's `returndata` bug", making the caller
EVM ignore any data except from `REVERT`.

Either change works by itself.  The reason for both is to ensure we definitely
comply with ambiguous EVMC expectations from either side of that boundary, and
it makes the internal API clearer.

As well as fixing a specific consensus issue, there are some other EVM logic
changes too: Refactored `writeContract`, how `RETURNDATA` is handled inside the
EVM, and changed behaviour with quirks before EIP-2 (Homestead).

The fix allows sync to pass block 5080941 on Goerli, and probably equivalent on
other networks.  Here's a trace at batch 5080897..5081088:

```
TRC 2021-10-01 21:18:12.883+01:00 Persisting blocks                  file=persist_blocks.nim:43 fromBlock=5080897 toBlock=5081088
...
DBG 2021-10-01 21:18:13.270+01:00 Contract code size exceeds EIP170  topics="vm computation" file=computation.nim:236 limit=24577 actual=31411
DBG 2021-10-01 21:18:13.271+01:00 gasUsed neq cumulativeGasUsed      file=process_block.nim:68 block=5080941/0A3537BC5BDFC637349E1C77D9648F2F65E2BF973ABF7956618F854B769DF626 gasUsed=3129669 cumulativeGasUsed=3132615
TRC 2021-10-01 21:18:13.271+01:00 peer disconnected                  file=blockchain_sync.nim:407 peer=<IP:PORT>
```

Although it says "Contract code size" and "gasUsed", this bug is more general
than either contract size or gas.  It's due to incorrect behaviour of EVM
instructions `RETURNDATA` and `RETURNDATASIZE`.

Sometimes when `writeContract` decides to reject writing the contract for any
of several reasons (for example just insufficient gas), the unwritten contract
code was being used as the "return data", and given to the caller.  If the
caller used `RETURNDATA` or `RETURNDATASIZE` ops, those incorrectly reported
the contract code that didn't get written.

EIP-211 (https://eips.ethereum.org/EIPS/eip-211) describes `RETURNDATA`:
> "`CREATE` and `CREATE2` are considered to return the empty buffer in the
> success case and the failure data in the failure case".

The language is ambiguous.  In fact "failure case" means when the contract uses
`REVERT` to finish.  It doesn't mean other failures like out of gas, EIP-170
limit, EIP-3541, etc.

To be thorough, and to ensure we always do the right thing with real EVMC when
that's finalised, this patch fixes the `RETURNDATA` issue in two places, either
of which make Goerli block 5080941 pass.

`writeContract` has been refactored to be caller, and so has where it's called.
It sets an error in the usual way if contract writing is rejected -- that's
anticipating EVMC, where we'll use different error codes later.

Overall four behaviour changes:

1. On the callee side, it doesn't set `c.outputData` except for `REVERT`.
2. On the caller side, it doesn't read `child.outputData` except for `REVERT`.
3. There was a bug in processing before Homestead fork (EIP-2).  We did not
   match the spec or other implementations; now we do.  When there's
   insufficient gas, before Homestead it's treated as success but with an empty
   contract.

   https://github.com/ethereum/pyethereum/blob/d117c8f3fd93359fc641fd850fa799436f7c43b5/ethereum/processblock.py#L304
   https://github.com/ethereum/go-ethereum/blob/401354976bb4/core/vm/instructions.go#L586

4. The Byzantium check has been removed, as it's unnecessary.

Signed-off-by: Jamie Lokier <[email protected]>
@jangko jangko changed the base branch from master to jl/evmc-dynamic-loader December 12, 2021 09:35
Base automatically changed from jl/evmc-dynamic-loader to master December 12, 2021 15:43
@jangko jangko merged commit 4b89ca3 into master Dec 12, 2021
@jangko jangko deleted the jl/evm-writedata-fix branch December 12, 2021 15:43
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.

EIP-170 related consensus error at Goerli block 5080941
3 participants