Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/great-clouds-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@eth-optimism/l2geth": patch
---

Return bytes from both ExecutionManager.run and ExecutionManager.simulateMessage and be sure to properly ABI decode the return values and the nested (bool, returndata)
5 changes: 5 additions & 0 deletions .changeset/weak-suits-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@eth-optimism/contracts": patch
---

Update ABI of simulateMessage to match run
110 changes: 84 additions & 26 deletions l2geth/core/vm/evm.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
package vm

import (
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"math/big"
"strings"
"sync/atomic"
"time"

"github.com/ethereum/go-ethereum/accounts/abi"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
Expand All @@ -32,10 +34,66 @@ import (
"github.com/ethereum/go-ethereum/rollup/dump"
)

// codec is a decoder for the return values of the execution manager. It decodes
// (bool, bytes) from the bytes that are returned from both
// `ExecutionManager.run()` and `ExecutionManager.simulateMessage()`
var codec abi.ABI

// innerData represents the results returned from the ExecutionManager
// that are wrapped in `bytes`
Copy link
Collaborator

Choose a reason for hiding this comment

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

Would note somehow here that this is a convention followed by our contracts, and may not necessarily be the case for arbitrary enqueues.

On the plus side, this return type will let us finally crack #497 by adding the same return type to the OVM_L2CrossDomainMessenger. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should actually talk more about this one -- I don't think it should be necessary for us to return abi.encode(true, bytes("")) in OVM_L2CrossDomainMessenger. If we add that requirement then we're gonna get a lot of L2 contracts that weirdly return the same thing (just to make the receipts work).

Copy link
Contributor

Choose a reason for hiding this comment

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

@tynes this is what I wanted to talk to you about earlier

Copy link
Contributor

Choose a reason for hiding this comment

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

A bit confused, does this change imply that consuming contracts would need to follow a specific return value convention? My understanding was that this change only concerns the executionManager's returned values.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There needs to be a strictly defined interface between the system contracts and the userland contracts for consistency purposes. Before this PR, there was not a consistent ABI - two examples are ExecutionManger.run(...)(bytes) and ExecutionManager.simulateMessage(...)(bool,bytes). The run() bytes had a nested ABI encoded (bool,bytes).

We want to index information about the userland contracts and not the system contracts so that when users query for receipt or transaction they get the from to be the ESCDA Contract Account. We enforce that a specific ABI encoding is returned from the userland contracts and passed through the system contracts such that geth can receive the data here and parse it.

type innerData struct {
Success bool `abi:"_success"`
ReturnData []byte `abi:"_returndata"`
}

// runReturnData represents the actual return data of the ExecutionManager.
// It wraps (bool, bytes) in an ABI encoded bytes
type runReturnData struct {
ReturnData []byte `abi:"_returndata"`
}

// Will be removed when we update EM to return data in `run`.
var deadPrefix, fortyTwoPrefix, zeroPrefix []byte

func init() {
const abidata = `
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 should be defined as an interface in the contracts repo and I should be able to import its ABI

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 think that should be possible if we can add it to the spec

[
{
"type": "function",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Rename this to something canonical

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@gakonst I'd like to define a common interface that defines the interaction between system contracts and userland contracts. This ABI would need to decode 2 things. One method decodes bytes and another method decodes bool,bytes.

Methods that need to use the interface:

  • ExecutionManager.run
  • ExecutionManager.simulateMessage
  • OVM_L2CrossDomainMessenger.relayMessage

If this interface is followed, then geth will index the transactions based on the userland contracts instead of the system contracts

"name": "call",
"constant": true,
"inputs": [],
"outputs": [
{
"name": "_success",
"type": "bool"
},
{
"name": "_returndata",
"type": "bytes"
}
]
},
{
"type": "function",
"name": "blob",
"constant": true,
"inputs": [],
"outputs": [
{
"name": "_returndata",
"type": "bytes"
}
]
}
]
`

var err error
codec, err = abi.JSON(strings.NewReader(abidata))
if err != nil {
panic(fmt.Errorf("unable to create abi decoder: %v", err))
}
deadPrefix = hexutil.MustDecode("0xdeaddeaddeaddeaddeaddeaddeaddeaddead")
zeroPrefix = hexutil.MustDecode("0x000000000000000000000000000000000000")
fortyTwoPrefix = hexutil.MustDecode("0x420000000000000000000000000000000000")
Expand Down Expand Up @@ -325,39 +383,39 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas

if UsingOVM {
// OVM_ENABLED

if evm.depth == 0 {
// We're back at the root-level message call, so we'll need to modify the return data
// sent to us by the OVM_ExecutionManager to instead be the intended return data.

if len(ret) >= 96 {
// We expect that EOA contracts return at least 96 bytes of data, where the first
// 32 bytes are the boolean success value and the next 64 bytes are unnecessary
// ABI encoding data. The actual return data starts at the 96th byte and can be
// empty.
success := ret[:32]
ret = ret[96:]

if !bytes.Equal(success, AbiBytesTrue) && !bytes.Equal(success, AbiBytesFalse) {
// If the first 32 bytes not either are the ABI encoding of "true" or "false",
// then the user hasn't correctly ABI encoded the result. We return the null
// hex string as a default here (an annoying default that would convince most
// people to just use the standard form).
ret = common.FromHex("0x")
} else if bytes.Equal(success, AbiBytesFalse) {
// If the first 32 bytes are the ABI encoding of "false", then we need to add an
// artificial error that represents the revert.
// Attempt to decode the returndata as as ExecutionManager.run when
// it is not an `eth_call` and as ExecutionManager.simulateMessage
// when it is an `eth_call`. If the data is not decodable as ABI
// encoded bytes, then return nothing. If the data is able to be
// decoded as bytes, then attempt to decode as (bool, bytes)
isDecodable := true
returnData := runReturnData{}
if err := codec.Unpack(&returnData, "blob", ret); err != nil {
isDecodable = false
}

switch isDecodable {
case true:
inner := innerData{}
// If this fails to decode, the nil values will be set in
// `inner`, meaning that it will be interpreted as reverted
// execution with empty returndata
_ = codec.Unpack(&inner, "call", returnData.ReturnData)
Copy link
Contributor

Choose a reason for hiding this comment

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

Currently we decode under the assumption that userland contracts will return (bool, bytes). I'd like to also support (bool) returndata. One way to cheat this is by appending the zero hash to the returndata if you want a hack that will work consistently.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fineeee no hacks. Better just add a second decoding step.

if !inner.Success {
err = errExecutionReverted
}
} else {
// User hasn't conformed the standard format, just return "null" for the success
// (with no return data) to convince them to use the standard.
ret = common.FromHex("0x")
ret = inner.ReturnData
case false:
ret = []byte{}
}
}

if evm.Context.EthCallSender == nil {
log.Debug("Reached the end of an OVM execution", "ID", evm.Id, "Return Data", hexutil.Encode(ret), "Error", err)
}
if evm.Context.EthCallSender == nil {
log.Debug("Reached the end of an OVM execution", "ID", evm.Id, "Return Data", hexutil.Encode(ret), "Error", err)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1882,7 +1882,6 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
)
external
returns (
bool,
bytes memory
)
{
Expand All @@ -1899,18 +1898,19 @@ contract OVM_ExecutionManager is iOVM_ExecutionManager, Lib_AddressResolver {
if (isCreate) {
(address created, bytes memory revertData) = ovmCREATE(_transaction.data);
if (created == address(0)) {
return (false, revertData);
return abi.encode(false, revertData);
} else {
// The eth_call RPC endpoint for to = undefined will return the deployed bytecode
// in the success case, differing from standard create messages.
return (true, Lib_EthUtils.getCode(created));
return abi.encode(true, Lib_EthUtils.getCode(created));
}
} else {
return ovmCALL(
(bool success, bytes memory returndata) = ovmCALL(
_transaction.gasLimit,
_transaction.entrypoint,
_transaction.data
);
return abi.encode(success, returndata);
}
}
}