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

add eth_simulateV1 #484

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
31fcbe5
eth_multicallv1
KillariDev Nov 4, 2023
8b020ce
fix some issues with the spec
KillariDev Nov 20, 2023
aaab3f8
Merge remote-tracking branch 'upstream/main' into eth_multicallv1
KillariDev Nov 20, 2023
8ab6539
fix refs, fix styling
KillariDev Nov 20, 2023
61fb231
Update docs/multicall-notes.md
KillariDev Nov 27, 2023
45313dd
Update docs/multicall-notes.md
KillariDev Nov 27, 2023
d9411e0
feedback according to comments, and a 30mb limit
KillariDev Nov 27, 2023
06d27bd
Merge branch 'eth_multicallv1' of https://github.com/DarkFlorist/exec…
KillariDev Nov 27, 2023
b4ce098
use 0xee.. address instead of 0x0 address for eth transfer logs
KillariDev Dec 12, 2023
0043e12
add withdrawals to input and output
KillariDev Dec 12, 2023
3095c08
add support for EIP-4844
KillariDev Dec 19, 2023
c7ec336
rename eth_multicallV1 to eth_simulateV1
KillariDev Jan 11, 2024
c20052c
update according to meeting (nonce calculation, blobBaseFee, gasLimit…
KillariDev Jan 21, 2024
7aee5b3
add note that when targeting non-precompiles with MovePrecompileToAdd…
KillariDev Feb 1, 2024
8996847
rename focument to be eth simulate related because of the name change
KillariDev Feb 26, 2024
e76cb57
add transaction hash and index to the logs
KillariDev Feb 26, 2024
dde3124
Update docs/ethsimulatev1-notes.md
KillariDev Mar 5, 2024
db721c4
fix blobgasPrices
KillariDev Mar 7, 2024
aef37eb
use blobBaseFee for now
KillariDev Mar 8, 2024
85ad1a4
remove blobBaseFee, not part of header
KillariDev Mar 20, 2024
21c6dfa
blobBaseFee to uint64 and validator_index -> `validatorIndex`
KillariDev Mar 20, 2024
e27cf9a
Merge remote-tracking branch 'upstream/main' into eth_multicallv1
KillariDev Mar 20, 2024
b597a7f
Merge remote-tracking branch 'upstream/main' into eth_multicallv1
KillariDev Apr 24, 2024
1ccfd37
remove phantom blocks :(
KillariDev Apr 24, 2024
f297b4f
rename multicall stuff to ethsimulate stuff. Add `returnFullTransacti…
KillariDev May 29, 2024
e3332ea
use `allOf`
KillariDev May 29, 2024
e913466
when validation mode is false, baseFeePerGas and blobBaseFee are set…
KillariDev Jun 4, 2024
8141886
multicall -> eth_simulate, add chain id, add default rsv values, add…
KillariDev Jun 12, 2024
2ee9fc2
add rationale for pre-computed calls
s1na Sep 17, 2024
42d1249
update with flexibility
s1na Sep 17, 2024
55f657c
Merge pull request #3 from s1na/eth_multicallv1
KillariDev Sep 18, 2024
0256a4b
added removed field to logs which should have been there
KillariDev Sep 25, 2024
de3e3f5
make "removed" an optional field
KillariDev Oct 7, 2024
d7b4616
relax time requirement so that time can stay still
KillariDev Oct 7, 2024
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
226 changes: 226 additions & 0 deletions docs/multicall-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Multicall
This document contains some extra information that couldn't be fit to the specification document directly.

## Default block values
Unlike `eth_call`, `eth_multicallV1`'s calls are conducted inside blocks. We don't require user to define all the fields of the blocks so here are the defaults that are assumed for blocks parameters:

| parameter name | default value |
-----------------|-----------------------
| prevRandao | `0x0000000000000000000000000000000000000000000000000000000000000000` |
| feeRecipient | `0x0000000000000000000000000000000000000000` |
| mixHash | `0x0000000000000000000000000000000000000000000000000000000000000000` |
| nonce | `0x0` |
| extraData | `0x0000000000000000000000000000000000000000000000000000000000000000` |
| difficulty | The same as the base block defined as the second parameter in the call |
| gasLimit | The same as the base block defined as the second parameter in the call |
| hash | Calculated normally, except for phantom blocks, see below Phantom block section |
| parentHash | Previous blocks hash (the real hash, or phantom blocks hash) |
| timestamp | The timestamp of previous block + 1 |
| baseFeePerGas | Calculated on what it should be according to ethereum's spec. Note: baseFeePerGas is not adjusted in the phantom blocks. |
| sha3Uncles | Empty trie root |
| withdrawals | Empty array |
| uncles | Empty array |
| number | Previous block number + 1 |
| logsBloom | Calculated normally. ETH logs are not part of the calculation |
| receiptsRoot | Calculated normally |
| transactionsRoot | Calculated normally |
| size | Calculated normally |
| withdrawalsRoot | Calculated normally |
| gasUsed | Calculated normally |
| stateRoot | Calculated normally |

An interesting note here is that we decide timestamp as `previous block timestamp + 1`, while `previous block timestamp + 12` could also be an assumed default. The reasoning to use `+1` is that it's the minimum amount we have to increase the timestamp to keep them valid. While `+12` is what Mainnet uses, there are other chains that use some other values, and we didn't want to complicate the specification to consider all networks.

### Phantom blocks
The multicall allows you to define on what block number your calls or transactions are being executed on. E.g, consider following call:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_multicallV1",
"params": [
{
"blockStateCalls": [
{
"blockOverrides": {
"number": "0x64"
},
},
{
"blockOverrides": {
"number": "0xc8"
},
}
]
},
"0xa"
]
}
```

Here we want our calls to be executed in blocks 100 (`0x64`) and in 200 (`0xc8`). The block numbers can be anything as long as they are increasing and higher than the block we are building from 10 (`0xa`). Now we end up in a situation where there exists block ranges 11-99 and 101-199 that are not defined anywhere. These blocks are called "phantom blocks". What happens if you try to request block hash of any of such blocks in the EVM? How can we calculate the block hash of future blocks when we don't know the block hash of the previous block?

Our solution to this problem is to define block hash of a phantom block to be:

```
keccak(rlp([hash_of_previous_non_phantom_block, phantom_block_number]))
```

So for example in our example, you could get block hash of block 142 as follows:
```
keccak(rlp([hash of block 12, 142]))
```

The phantom blocks other properties are set to their default properties as defined by the multicall specification. We came to this definition by wanting phantom block hashes to be unique if things prior to the phantom block changes, so if tooling is storing block hashes somewhere, they should remain unique if things change in the simulation.

One other approach to this problem would be to really calculate the real block hashes for all the phantom blocks, but this would make generating blocks far in future really expensive, as to generate 100 phantom blocks, you would need to calculate 100 block hashes that all depend on each other. And in most cases, no one really cares about these blocks.

Base fee per gas is not adjusted in the phantom blocks, their base fee remains constant.

## Default values for transactions
As multicall is an extension to `eth_call` we want to enable the nice user experience that the user does not need to provide all required values for a transaction. We are assuming following defaults if the variable is not provided by the user:
| parameter name | description |
-----------------|-----------------------
| type | `0x2` |
| nonce | Defaults to correct nonce |
| to | `null` |
| from | `0x0000000000000000000000000000000000000000` |
| gas limit | Remaining gas in the current block. This is calculated dynamically one by one for each transaction that is being processed. |
| value | `0x0` |
| input | no data |
| gasPrice | `0x0` |
| maxPriorityFeePerGas | `0x0` |
| maxFeePerGas | `0x0` |
Copy link
Member

Choose a reason for hiding this comment

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

These default values will crash, since baseFeePerGas is always 7 or higher. These default values will be rejected by blockchain spec, since tx is not willing to pay base fee. Note that the default block baseFeePerGas is calculated according to spec, so this will be 7 or higher always.

Copy link
Member

Choose a reason for hiding this comment

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

Suggestion: change maxFeePerGas and gasPrice (in case of tx type 0/1) to baseFeePerGas of the block and keep maxPriorityFeePerGas 0.

Copy link
Author

Choose a reason for hiding this comment

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

These default values will crash, since baseFeePerGas is always 7 or higher. These default values will be rejected by blockchain spec, since tx is not willing to pay base fee. Note that the default block baseFeePerGas is calculated according to spec, so this will be 7 or higher always.

yeah, this will crash on validation mode. On non-validation mode, we will allow you to make transactions that have zero basefee (similar to eth_call)

| accessList | empty array |

## Overriding default values
The default values of blocks and transactions can be overriden. For Transactions we allow overriding of variables `type`, `nonce`, `to`, `from`, `gas limit`, `value`, `input`, `gasPrice`, `maxPriorityFeePerGas`, `maxFeePerGas`, `accessList`, and for blocks we allow modifications of `number`, `time`, `gasLimit`, `feeRecipient`, `prevRandao` and `baseFeePerGas`:
Copy link
Member

Choose a reason for hiding this comment

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

Slight nit: for completeness, withdrawals should also be able to be overridden (to simulate withdrawals).

```json
"blockOverrides": {
"number": "0x14",
"time": "0xc8",
"gasLimit": "0x2e631",
"feeRecipient": "0xc100000000000000000000000000000000000000",
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000001234",
"baseFeePerGas": "0x14"
},
```
All the other fields are computed automatically (eg, `stateRoot` and `gasUsed`) or kept as their default values (eg. `uncles` or `withdrawals`). When overriding `number` and `time` variables for blocks, we automatically check that the block numbers and time fields are strictly increasing (we don't allow decreasing, or duplicated block numbers or times). If the block number is increased more than `1` compared to the previous block, phantom blocks are created to fill the gaps.

An interesting note here is that an user can specify block numbers and times of some blocks, but not for others. When block numbers of times are left unspecified, the default values will be used. After the blocks have been constructed, and default values are calculated, the blocks are checked that their block numbers and times are still valid.

## ETH transfer logs
When `traceTransfers` setting is enabled on `eth_multicallV1` The multical will return logs for ethereum transfers along with the normal logs sent by contracts. The ETH transfers are identical to ERC20 transfers, except the "sending contract" is address `0x0`.
Copy link

@mds1 mds1 Dec 2, 2023

Choose a reason for hiding this comment

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

I'd suggest using 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE as the sending contract, since that's become a de facto approach for representing ETH as a token address within contracts.

Here's a simple sourcegraph search showing usage: https://sourcegraph.com/search?q=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&groupBy=repo

Copy link
Contributor

Choose a reason for hiding this comment

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

I suspect if you were to do a similar search of 0x000... you would get even more results. "A lot of people use X" is quite different from "Most people use X".

If we ignore any past precedence, I feel like 0x000... makes it a tad more explicit that this is special, and not some precompile or something. For parameters, 0xEee... makes a bit of sense because 0 is the default value for a lot of stuff (e.g., null, missing, etc.) and thus often passed on accident, so using nonzero helps protect against a class of bugs. In the case of event sourcing, 0x000... isn't provided by user/developer so they cannot get it wrong.

That being said, I don't have a strong argument against 0xEeee..., only that I think 0x000... is marginally better and I suspect more widely used.

Copy link

Choose a reason for hiding this comment

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

I agree a search for the zero address would yield more results, but only because the zero address is used for a much wider range of cases, whereas 0xEee... seems to only be used to represent ETH. This is anecdotal and it's hard to know concretely, but in my experience 0xEee... is more common for representing ETH than the zero address.

Personally, if I didn't know about the special ETH logs, seeing 0xEee... would make the log's meaning more intuitive and apparent than seeing 0x000....

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree a search for the zero address would yield more results, but only because the zero address is used for a much wider range of cases

Sorry, I meant if you could somehow limit the search of 0x0000... to only places where it was being used as a representation of ETH I suspect it would be more/bigger. That being said, perhaps the searchability of 0xEeee... is a significant selling point.

Personally, if I didn't know about the special ETH logs, seeing 0xEee... would make the log's meaning more intuitive and apparent than seeing 0x000....

For what it is worth, this feature is disabled by default and you need to set a flag to turn it on. I'm not sure that is a meaningful argument though.

Copy link

@mmsaki mmsaki Dec 25, 2023

Choose a reason for hiding this comment

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

I agree a search for the zero address would yield more results, but only because the zero address is used for a much wider range of cases, whereas 0xEee... seems to only be used to represent ETH. This is anecdotal and it's hard to know concretely, but in my experience 0xEee... is more common for representing ETH than the zero address.

I agree with @mds1 that 0xEee.. seems more appropriate for Eth transfer logs

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe the spec has since been updated to use 0xEee.....

Choose a reason for hiding this comment

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

The notes still mentions 0x0 here: https://github.com/ethereum/execution-apis/pull/484/files#diff-1e4727bbfb75c5cc85054abbf972a1cd472e9e512a9d8bbc8152210a0062182bR116

Aside: It still feels "unclean" to me using a specific non-reserved address, but in researching this I was surprised to learn that EIP-1352 was never officially adopted.

Copy link

@sambacha sambacha Apr 2, 2024

Choose a reason for hiding this comment

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

0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE is not correct if you're using EIP-1191, the correct chainId encoded address would be 0xeeeEEEeEEeeeEeEeEEEeEeeeeEEEEeEEeEeeeeeE

having it as 0x00... would be agnostic to chainId encoding.

Edit: this is a purely aesthetic choice, though I would find it helpful

Copy link
Contributor

Choose a reason for hiding this comment

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

I and others have argued against EIP-1191 in the discussion for it: ethereum/EIPs#1121

My position is still largely the same. It is backward incompatible with EIP-55 checksumming, so I generally don't recommend apps checksum to 1191 until long after almost all tooling supports reading 1191 checksums.


For example, here's a query that will simply send ether from one address to another (with a state override that gives us the ETH initially):
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_multicallV1",
"params": [
{
"blockStateCalls": [
{
"stateOverrides": {
"0xc000000000000000000000000000000000000000": {
"balance": "0x7d0"
}
},
"calls": [
{
"from": "0xc000000000000000000000000000000000000000",
"to": "0xc100000000000000000000000000000000000000",
"value": "0x3e8"
}
]
}
],
"traceTransfers": true
},
"latest"
]
}
```

The output of this query is:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": [
{
"number": "0x4",
"hash": "0x859c932c5cf0dabf8d12eb2518e063966ac1a25e2fc49f1f02574a37f358d0b5",
"timestamp": "0x1f",
"gasLimit": "0x4c4b40",
"gasUsed": "0x5208",
"feeRecipient": "0x0000000000000000000000000000000000000000",
"baseFeePerGas": "0x2310a91d",
"prevRandao": "0x0000000000000000000000000000000000000000000000000000000000000000",
"calls": [
{
"returnData": "0x",
"logs": [
{
"address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000c000000000000000000000000000000000000000",
"0x000000000000000000000000c100000000000000000000000000000000000000"
],
"data": "0x00000000000000000000000000000000000000000000000000000000000003e8",
"blockNumber": "0x4",
"transactionHash": "0xa4d41019e71335f8567e17746b708ddda8b975a9a61f909bd3df55f4866cc913",
"transactionIndex": "0x0",
"blockHash": "0x859c932c5cf0dabf8d12eb2518e063966ac1a25e2fc49f1f02574a37f358d0b5",
"logIndex": "0x0",
"removed": false
}
],
"gasUsed": "0x5208",
"status": "0x1"
}
]
}
]
}
```

Here the interesting part is:
```json
"address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
"topics": [
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000c000000000000000000000000000000000000000",
"0x000000000000000000000000c100000000000000000000000000000000000000"
],
"data": "0x00000000000000000000000000000000000000000000000000000000000003e8",
```
In the observed event, the sender address is denoted as the `0xee...` address. The first topic (`0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef`) aligns with the event signature `Transfer(address,address,uint256)`, while the second topic (`0x000000000000000000000000c000000000000000000000000000000000000000`) corresponds to the sending address, and the third topic (`0x000000000000000000000000c100000000000000000000000000000000000000`) represents the receiving address. The quantity of ETH transacted is stored in the data field.

The ETH logs will contain following types of ETH transfers:
- Transfering ETH from EOA
- Transfering ETH via contract
- Selfdestructing contract sending ETH

But not following ones:
- Gas fees
- Multicalls eth balance override

ETH logs are not part of the calculation for logs bloom filter. Also, similar to normal logs, if the transaction sends ETH but the execution reverts, no log gets issued.

## Validation
The multicall has a feature to enable or disable validation with setting `Validation`, by default, the validation is off, and the multicall mimics `eth_call` with reduced number of checks. Validation enabled mode is intended to give as close as possible simulation of real EVM block creation, except there's no checks for transaction signatures and we also allow one to send a direct transaction from a contract.
Copy link
Member

Choose a reason for hiding this comment

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

I am not entirely sure what this would remove/add. If I turn this on, in what situation would this be useful, and what would change if I would have kept it off? 🤔

Copy link
Member

Choose a reason for hiding this comment

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

For instance, if validation is set to off, are tx gas limits not checked? (So I can include a tx with gas limit higher than the block gas limit?).

Copy link
Member

@jochem-brouwer jochem-brouwer Nov 27, 2023

Choose a reason for hiding this comment

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

Never mind, it is in execute.yaml

    validation:
      title: Validation
      description: |-
        When true, the multicall does all validations that a normal EVM would do, except contract sender and signature checks. When false, multicall behaves like eth_call.

Copy link
Author

Choose a reason for hiding this comment

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

This is bit ill defined (as discussed in todays call). Because Nethermind and Geth behave bit differently with eth_call and we have been trying to figure out the differences so we could document them. The intention that is that when validation is enabled, the clients would behave exactly the same, while not fixing the issue that with non-validation, the validation rules might differ a bit.

Copy link
Member

Choose a reason for hiding this comment

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

Then I think this spec should explicitly state what should be not be validated if the validation is off. For instance, currently it only seems that you can override the sender (so you can create unsigned txs). But, it also seems that it is possible to set base fee to zero (so do not validate this as well). It should be clearly defined what should be validated and what not. (And this is non-trivial since there is a huge ruleset of what should be validated in transactions/blocks)

Copy link
Contributor

Choose a reason for hiding this comment

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

For validation mode, I'm fine with base fee not being allowed to go below 7.

Copy link
Author

Choose a reason for hiding this comment

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

I don't think it makes sense that much to validate base fee in validation mode if the user specifically sets the base fee to something. Otherwise we could remove all overrides for validation mode, as none of them can happen on mainnet.

Copy link
Contributor

@s1na s1na Nov 30, 2023

Choose a reason for hiding this comment

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

possibly also create multiple validation modes / flags if necessary?

Please no! In fact I want to argue that we should remove validation flag and default to relax-mode. My argument is that this is easy to add in later since we have designed the API param to be an object. It will be a backwards-compatible change. I'd personally like to hear from some people to say they need this first.

That said, we should still define exactly what is not validated in relax-mode. So I think that convo is important to have. And I will share here exactly what geth is validating:

For block validation, only these fields are validated:

  • Only block number and timestamp are validated to be incremental.

As for tx validation, these are NOT validated:

  • Signature is not validated
  • Nonce too high, too low, out of bounds are not validated
  • Sender is EoA is not validated
  • If gas price fields are 0, then sender doesn't need to have enough balance for gas (only value)
  • If blobGasFeeCap is 0, then blob gas requirement is skipped

Copy link
Contributor

@s1na s1na Nov 30, 2023

Choose a reason for hiding this comment

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

I'd like to add that these relaxations are all cause for a transaction not to go through. I.e. if I have a call that works in the relax-mode, sign it and submit it to a node, it will not be gossiped to the network at all.

I'd argue if there is any danger to a user it's from all of the flexibility we're providing through state and precompile overrides. E.g. they assume in the simulation to be in a certain condition by using overrides, and submit the transaction when that is not met. That tx will be mined and will revert (most probably).

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd personally like to hear from some people to say they need this first.

Our team would like it because it allows us to verify that the transaction is properly constructed and will execute against mainnet without error. In some cases we ship a set of transactions as a "bundle" to searchers, and they need to all execute properly in sequence (including things like balance checks), and in other cases we ship a single transaction to a signer and we want a high degree of confidence that it will execute on-chain just like it did during simulation.


## Failures
It is possible that user defines a transaction that cannot be included in the Ethereum block as it breaks the rules of EVM. For example, if transactions nonce is too high or low, baseFeePerGas is too low etc. In these situations the execution of multicall ends and an error is returned.

## Version number
The method name for multicall `eth_multicallV1` the intention is that after release of multicall, if new features are wanted the `eth_multicallV1` is kept as it is, and instead `eth_multicallV2` is published with the new wanted features.

## Clients can set their own limits
Clients may introduce their own limits to prevent DOS attacks using the method. We have thought of three such standard limits
- How many blocks can be defined in `BlockStateCalls`. The suggested default for this is 256 blocks
- A global gas limit (similar to the same limit for `eth_call`). The multicall cannot exceed the global gas limit over its lifespan
- The clients can set their own limit on how big the input JSON payload can be. A suggested default for this is 30mb
16 changes: 16 additions & 0 deletions src/eth/execute.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,19 @@
gasUsed:
title: Gas used
$ref: '#/components/schemas/uint'
- name: eth_multicallV1
summary: Executes a sequence of message calls building on each other's state without creating transactions on the block chain, optionally overriding block and state data
params:
- name: Payload
required: true
schema:
$ref: '#/components/schemas/MultiCallPayload'
- name: Block tag
required: false
description: "default: 'latest'"
schema:
$ref: '#/components/schemas/BlockNumberOrTagOrHash'
result:
name: Result of calls
schema:
$ref: '#/components/schemas/MultiCallResult'
Loading