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

feat(fw): calculate genesis state root without calling t8n #450

Merged
merged 4 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Test fixtures for use by clients are available for each release on the [Github r
- ✨ Improve handling of the argument passed to `solc --evm-version` when compiling Yul code ([#418](https://github.com/ethereum/execution-spec-tests/pull/418)).
- 🐞 Fix `fill -m yul_test` which failed to filter tests that are (dynamically) marked as a yul test ([#418](https://github.com/ethereum/execution-spec-tests/pull/418)).
- 🔀 Helper methods `to_address`, `to_hash` and `to_hash_bytes` have been deprecated in favor of `Address` and `Hash`, which are automatically detected as opcode parameters and pushed to the stack in the resulting bytecode ([#422](https://github.com/ethereum/execution-spec-tests/pull/422)).
- 🔀 Locally calculate state root for the genesis blocks in the blockchain tests instead of calling t8n ([#450](https://github.com/ethereum/execution-spec-tests/pull/450)).

### 🔧 EVM Tools

Expand Down
102 changes: 41 additions & 61 deletions docs/getting_started/debugging_t8n_tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,64 +14,45 @@ In particular, a script `t8n.sh` is generated for each call to the `t8n` command
For example, running:

```console
fill tests/berlin/eip2930_access_list/ --fork Berlin \
fill tests/berlin/eip2930_access_list/ --fork Berlin -m blockchain_test \
--evm-dump-dir=/tmp/evm-dump
```

will produce the directory structure:

```text
📂 /tmp/evm-dump
└── 📂 blockchain_tests
└── 📂 berlin__eip2930_access_list__test_acl__test_access_list
└── 📂 fork_Berlin
├── 📂 0
│   ├── 📄 args.py
│   ├── 📂 input
│   │   ├── 📄 alloc.json
│   │   ├── 📄 env.json
│   │   └── 📄 txs.json
│   ├── 📂 output
│   │   ├── 📄 alloc.json
│   │   ├── 📄 result.json
│   │   └── 📄 txs.rlp
│   ├── 📄 returncode.txt
│   ├── 📄 stderr.txt
│   ├── 📄 stdin.txt
│   ├── 📄 stdout.txt
│   └── 📄 t8n.sh
└── 📂 1
├── 📄 args.py
├── 📂 input
│   ├── 📄 alloc.json
│   ├── 📄 env.json
│   └── 📄 txs.json
├── 📂 output
│   ├── 📄 alloc.json
│   ├── 📄 result.json
│   └── 📄 txs.rlp
├── 📄 returncode.txt
├── 📄 stderr.txt
├── 📄 stdin.txt
├── 📄 stdout.txt
└── 📄 t8n.sh
└── 📂 berlin__eip2930_access_list__test_acl__test_access_list
└── 📂 fork_Berlin_blockchain_test
└── 📂 0
   ├── 📄 args.py
   ├── 📂 input
   │   ├── 📄 alloc.json
   │   ├── 📄 env.json
   │   └── 📄 txs.json
   ├── 📂 output
   │   ├── 📄 alloc.json
   │   ├── 📄 result.json
   │   └── 📄 txs.rlp
   ├── 📄 returncode.txt
   ├── 📄 stderr.txt
   ├── 📄 stdin.txt
   ├── 📄 stdout.txt
   └── 📄 t8n.sh
```

where the directories `0` and `1` correspond to the different calls made to the `t8n` tool executed during the test:
where the directory `0` is the starting index of the different calls made to the `t8n` tool executed during the test, and since the test only contains one block, there is only one directory present.

- `0` corresponds to the call used to calculate the state root of the test's initial alloc (which is why it has an empty transaction list).
- `1` corresponds to the call used to execute the first transaction or block from the test.

Note, there may be more directories present `2`, `3`, `4`,... if the test executes more transactions/blocks.
Note, there may be more directories present `1`, `2`, `3`,... if the test executes more blocks.

Each directory contains files containing information corresponding to the call, for example, the `args.py` file contains the arguments passed to the `t8n` command and the `output/alloc.json` file contains the output of the `t8n` command's `--output-alloc` flag.

### The `t8n.sh` Script

The `t8n.sh` script written to the debug directory can be used to reproduce a specific call made to the `t8n` command during the test session. For example, if a Besu `t8n-server` has been started on port `3001`, the request made by the test for first transaction can be reproduced as:
The `t8n.sh` script written to the debug directory can be used to reproduce a specific call made to the `t8n` command during the test session. For example, if a Besu `t8n-server` has been started on port `3001`, the request made by the test for first block can be reproduced as:

```console
/tmp/besu/test_access_list_fork_Berlin/1/t8n.sh 3001
/tmp/besu/test_access_list_fork_Berlin/0/t8n.sh 3001
```

which writes the response the from the `t8n-server` to the console output:
Expand Down Expand Up @@ -110,7 +91,7 @@ The `--verify-fixtures` flag can be used to run go-ethereum's `evm blocktest` co
For example, running:

```console
fill tests/berlin/eip2930_access_list/ --fork Berlin \
fill tests/berlin/eip2930_access_list/ --fork Berlin -m blockchain_test \
--evm-dump-dir==/tmp/evm-dump \
--evm-bin=../evmone/build/bin/evmone-t8n \
--verify-fixtures-bin=../go-ethereum/build/bin/evm \
Expand All @@ -121,25 +102,24 @@ will additionally run the `evm blocktest` command on every JSON fixture file and

```text
📂 /tmp/evm-dump
└── 📂 blockchain_tests
└── 📂 berlin__eip2930_access_list__test_acl__test_access_list
├── 📄 fixtures.json
├── 📂 fork_Berlin
│   ├── 📂 0
│   │   ├── 📄 args.py
│   │   ├── 📂 input
│   │   │   ├── 📄 alloc.json
│   │   │   ├── 📄 env.json
│   │   │   └── 📄 txs.json
│   │   ├── 📂 output
│   │   │   ├── 📄 alloc.json
│ ... ... ...
├── 📄 verify_fixtures_args.py
├── 📄 verify_fixtures_returncode.txt
├── 📄 verify_fixtures.sh
├── 📄 verify_fixtures_stderr.txt
└── 📄 verify_fixtures_stdout.txt
└── 📂 berlin__eip2930_access_list__test_acl__test_access_list
├── 📄 fixtures.json
├── 📂 fork_Berlin_blockchain_test
│   ├── 📂 0
│   │   ├── 📄 args.py
│   │   ├── 📂 input
│   │   │   ├── 📄 alloc.json
│   │   │   ├── 📄 env.json
│   │   │   └── 📄 txs.json
│   │   ├── 📂 output
│   │   │   ├── 📄 alloc.json
│ ... ... ...
├── 📄 verify_fixtures_args.py
├── 📄 verify_fixtures_returncode.txt
├── 📄 verify_fixtures.sh
├── 📄 verify_fixtures_stderr.txt
└── 📄 verify_fixtures_stdout.txt
```

where the `verify_fixtures.sh` script can be used to reproduce the `evm blocktest` command.
Expand Down
133 changes: 76 additions & 57 deletions src/ethereum_test_tools/common/types.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Useful types for generating Ethereum tests.
"""

from copy import copy, deepcopy
from dataclasses import dataclass, fields
from itertools import count
Expand All @@ -20,8 +21,10 @@

from coincurve.keys import PrivateKey, PublicKey
from ethereum import rlp as eth_rlp
from ethereum.base_types import Uint
from ethereum.base_types import U256, Uint
from ethereum.crypto.hash import keccak256
from ethereum.frontier.fork_types import Account as FrontierAccount
from ethereum.frontier.state import State, set_account, set_storage, state_root
from trie import HexaryTrie

from ethereum_test_forks import Fork
Expand Down Expand Up @@ -64,13 +67,11 @@ def __repr__(self) -> str:
MIN_STORAGE_KEY_VALUE = -(2**255)


class Storage(SupportsJSON):
class Storage(SupportsJSON, dict):
"""
Definition of a storage in pre or post state of a test
"""

data: Dict[int, int]

current_slot: Iterator[int]

StorageDictType: ClassVar[TypeAlias] = Dict[
Expand Down Expand Up @@ -220,49 +221,43 @@ def key_value_to_string(value: int) -> str:
hex_str = "0" + hex_str
return "0x" + hex_str

def __init__(self, input: StorageDictType | "Storage" = {}, start_slot: int = 0):
def __init__(self, input: StorageDictType | "Storage" = {}, *, start_slot: int = 0):
"""
Initializes the storage using a given mapping which can have
keys and values either as string or int.
Strings must be valid decimal or hexadecimal (starting with 0x)
numbers.
"""
self.data = {}
for key in input:
value = Storage.parse_key_value(input[key])
key = Storage.parse_key_value(key)
self.data[key] = value
super().__init__(
(Storage.parse_key_value(k), Storage.parse_key_value(v)) for k, v in input.items()
)
self.current_slot = count(start_slot)

def __len__(self) -> int:
"""Returns number of elements in the storage"""
return len(self.data)

def __iter__(self) -> Iterator[int]:
"""Returns iterator of the storage"""
return iter(self.data)

def __contains__(self, key: str | int | bytes) -> bool:
def __contains__(self, key: object) -> bool:
"""Checks for an item in the storage"""
key = Storage.parse_key_value(key)
return key in self.data
assert (
isinstance(key, str)
or isinstance(key, int)
or isinstance(key, bytes)
or isinstance(key, SupportsBytes)
)
return super().__contains__(Storage.parse_key_value(key))

def __getitem__(self, key: str | int | bytes) -> int:
def __getitem__(self, key: str | int | bytes | SupportsBytes) -> int:
"""Returns an item from the storage"""
key = Storage.parse_key_value(key)
if key not in self.data:
raise KeyError()
return self.data[key]
return super().__getitem__(Storage.parse_key_value(key))

def __setitem__(self, key: str | int | bytes, value: str | int | bytes): # noqa: SC200
def __setitem__(
self, key: str | int | bytes | SupportsBytes, value: str | int | bytes | SupportsBytes
): # noqa: SC200
"""Sets an item in the storage"""
self.data[Storage.parse_key_value(key)] = Storage.parse_key_value(value)
super().__setitem__(Storage.parse_key_value(key), Storage.parse_key_value(value))

def __delitem__(self, key: str | int | bytes):
def __delitem__(self, key: str | int | bytes | SupportsBytes):
"""Deletes an item from the storage"""
del self.data[Storage.parse_key_value(key)]
super().__delitem__(Storage.parse_key_value(key))

def store_next(self, value: str | int | bytes) -> int:
def store_next(self, value: str | int | bytes | SupportsBytes) -> int:
"""
Stores a value in the storage and returns the key where the value is stored.

Expand All @@ -278,9 +273,9 @@ def __json__(self, encoder: JSONEncoder) -> Mapping[str, str]:
hex string formatting.
"""
res: Dict[str, str] = {}
for key in self.data:
for key, value in self.items():
key_repr = Storage.key_value_to_string(key)
val_repr = Storage.key_value_to_string(self.data[key])
val_repr = Storage.key_value_to_string(value)
if key_repr in res and val_repr != res[key_repr]:
raise Storage.AmbiguousKeyValue(
key_1=key_repr, val_1=res[key_repr], key_2=key, val_2=val_repr
Expand All @@ -295,10 +290,10 @@ def contains(self, other: "Storage") -> bool:
Used for comparison with test expected post state and alloc returned
by the transition tool.
"""
for key in other.data:
if key not in self.data:
for key in other:
if key not in self:
return False
if self.data[key] != other.data[key]:
if self[key] != other[key]:
return False
return True

Expand All @@ -310,39 +305,35 @@ def must_contain(self, address: Address, other: "Storage"):
by the transition tool.
Raises detailed exception when a difference is found.
"""
for key in other.data:
if key not in self.data:
for key in other:
if key not in self:
# storage[key]==0 is equal to missing storage
if other[key] != 0:
raise Storage.MissingKey(key=key)
elif self.data[key] != other.data[key]:
elif self[key] != other[key]:
raise Storage.KeyValueMismatch(
address=address, key=key, want=self.data[key], got=other.data[key]
address=address, key=key, want=self[key], got=other[key]
)

def must_be_equal(self, address: Address, other: "Storage"):
"""
Succeeds only if "self" is equal to "other" storage.
"""
# Test keys contained in both storage objects
for key in self.data.keys() & other.data.keys():
if self.data[key] != other.data[key]:
for key in self.keys() & other.keys():
if self[key] != other[key]:
raise Storage.KeyValueMismatch(
address=address, key=key, want=self.data[key], got=other.data[key]
address=address, key=key, want=self[key], got=other[key]
)

# Test keys contained in either one of the storage objects
for key in self.data.keys() ^ other.data.keys():
if key in self.data:
if self.data[key] != 0:
raise Storage.KeyValueMismatch(
address=address, key=key, want=self.data[key], got=0
)
for key in self.keys() ^ other.keys():
if key in self:
if self[key] != 0:
raise Storage.KeyValueMismatch(address=address, key=key, want=self[key], got=0)

elif other.data[key] != 0:
raise Storage.KeyValueMismatch(
address=address, key=key, want=0, got=other.data[key]
)
elif other[key] != 0:
raise Storage.KeyValueMismatch(address=address, key=key, want=0, got=other[key])


@dataclass(kw_only=True)
Expand Down Expand Up @@ -583,10 +574,11 @@ class Alloc(dict, Mapping[Address, Account], SupportsJSON):
"""

def __init__(self, d: Mapping[FixedSizeBytesConvertible, Account | Dict] = {}):
for address, account in d.items():
address = Address(address)
assert address not in self, f"Duplicate address in alloc: {address}"
self[address] = Account.from_dict(account)
super().__init__(
(Address(address), Account.from_dict(account)) for address, account in d.items()
)
if len(self) != len(d):
raise Exception("Duplicate addresses in alloc")

@classmethod
def merge(cls, alloc_1: "Alloc", alloc_2: "Alloc") -> "Alloc":
Expand Down Expand Up @@ -619,6 +611,33 @@ def __json__(self, encoder: JSONEncoder) -> Mapping[str, Any]:
{Address(address): Account.from_dict(account) for address, account in self.items()}
)

def state_root(self) -> bytes:
"""
Returns the state root of the allocation.
"""
state = State()
for address, account in self.items():
set_account(
state=state,
address=address,
account=FrontierAccount(
nonce=Uint(Number(account.nonce)) if account.nonce is not None else Uint(0),
balance=(
U256(Number(account.balance)) if account.balance is not None else U256(0)
),
code=Bytes(account.code) if account.code is not None else b"",
),
)
if account.storage is not None:
for key, value in account.storage.items():
set_storage(
state=state,
address=address,
key=Hash(key),
value=U256(Number(value)),
)
return state_root(state)


def alloc_to_accounts(got_alloc: Dict[str, Any]) -> Mapping[str, Account]:
"""
Expand Down
Loading
Loading