Skip to content

Commit 03bdea1

Browse files
committed
docs: revamp the state test tutorial with a non-yul example
1 parent 4270c14 commit 03bdea1

File tree

1 file changed

+80
-88
lines changed

1 file changed

+80
-88
lines changed
Lines changed: 80 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,170 +1,153 @@
11
# State Transition Tests
22

3-
This tutorial teaches you to create a state transition execution specification test. These tests verify that a starting pre-state will reach a specified post-state after executing a single transaction.
3+
This tutorial teaches you to create a state transition execution specification test using the Python Opcodes minilang for writing EVM bytecode. These tests verify that a starting pre-state will reach a specified post-state after executing a single transaction. In this example, we'll create a simple contract using bytecode and then interact with it through a transaction to verify the expected state changes.
4+
5+
For an overview of different test types available, see [Types of Tests](../../writing_tests/types_of_tests.md).
46

57
## Pre-requisites
68

7-
Before proceeding with this tutorial, it is assumed that you have prior knowledge and experience with the following:
9+
This tutorial will require some prior knowledge and experience with the following:
10+
11+
- Repository set-up, see [installation](../../getting_started/installation.md).
12+
- Ability to run `fill`, see [Getting Started: Filling Tests](../../filling_tests/getting_started.md).
13+
- Basic familiarity with [Python](https://docs.python.org/3/tutorial/).
14+
15+
## Building a State Test
16+
17+
The most effective method of learning how to write tests is to study a straightforward example. In this tutorial we will build a simple state test that deploys a contract with bytecode and verifies its execution.
818

9-
- Repository set-up, see [installation](../../getting_started/installation.md).and run an execution specification test as outlined in the .
10-
- Able to run `fill`, see [Getting Started: Filling Tests](../../filling_tests/getting_started.md).
11-
- Understand how to read a [static state transition test](https://ethereum-tests.readthedocs.io/en/latest/state-transition-tutorial.html#the-source-code).
12-
- Know the basics of the [EVM](https://www.evm.codes/).
13-
- Familiarity with [Python](https://docs.python.org/3/tutorial/).
19+
### Complete Test Example
1420

15-
## Example Test
21+
We'll examine a simple test that uses the Python Opcodes minilang to write EVM bytecode. This example is based on the CHAINID opcode test from `tests/istanbul/eip1344_chainid/test_chainid.py`.
22+
23+
Let's examine each section.
24+
25+
```python
26+
"""State test tutorial demonstrating contract deployment and interaction."""
27+
```
1628

17-
The most effective method of learning how to write tests is to study a straightforward example. In this tutorial we will go over a simple state test that adds two numbers and stores them in the storage.
29+
In Python, multi-line strings are denoted using `"""`. As a convention, a file's purpose is often described in the opening string of the file.
1830

1931
```python
20-
from ethereum_test_forks import Fork, Frontier, Homestead
21-
from ethereum_test_tools import (
22-
Account,
23-
Alloc,
24-
Environment,
25-
StateTestFiller,
26-
Transaction,
27-
)
32+
import pytest
33+
34+
from ethereum_test_tools import Account, Alloc, Environment, StateTestFiller, Transaction
35+
from ethereum_test_tools.vm.opcode import Opcodes as Op
2836
```
2937

30-
In this snippet the required constants, types and helper functions are imported from `ethereum_test_tools` and `ethereum_test_forks`. We will go over these as we come across them.
38+
In this snippet the required constants, types and helper functions are imported from `ethereum_test_tools`. The `Opcodes` class (aliased as `Op`) provides the Python minilang for writing EVM bytecode. We will go over these as we come across them.
3139

3240
```python
33-
@pytest.mark.valid_from("Homestead")
41+
@pytest.mark.valid_from("Istanbul")
3442
```
3543

3644
In Python this kind of definition is called a [*decorator*](https://docs.python.org/3/search.html?q=decorator).
3745
It modifies the action of the function after it.
38-
In this case, the decorator is a custom [pytest fixture](https://docs.pytest.org/en/latest/explanation/fixtures.html) defined by the execution-specs-test framework that specifies that the test is valid for the [Homestead fork](https://ethereum.org/en/history/#homestead) and all forks after it. The framework will then fill this test case for all forks in the fork range specified by the command-line arguments.
46+
In this case, the decorator is a custom [pytest mark](https://docs.pytest.org/en/latest/how-to/mark.html) defined by the execution-specs-test framework that specifies that the test is valid for the [Istanbul fork](https://ethereum.org/en/history/#istanbul) and all forks after it. The framework will then fill this test case for all forks in the fork range specified by the command-line arguments.
47+
48+
For more information about test markers and fork validity, see [Test Markers](../../writing_tests/test_markers.md).
3949

4050
!!! info "Filling the test"
4151
To fill this test for all the specified forks, we can specify pytest's `-k` flag that [filters test cases by keyword expression](https://docs.pytest.org/en/latest/how-to/usage.html#specifying-tests-selecting-tests):
4252

4353
```console
44-
fill -k test_example
54+
fill -k test_state_test_example
4555
```
4656

4757
and to fill it for a specific fork range, we can provide the `--from` and `--until` command-line arguments:
4858

4959
```console
50-
fill -k test_example --from London --until Paris
60+
fill -k test_state_test_example --from London --until Paris
5161
```
5262

5363
```python
54-
def test_example(state_test: StateTestFiller, pre: Alloc, fork: Fork):
55-
"""
56-
Test that adds two numbers and stores the result in storage at key 0.
57-
"""
64+
def test_state_test_example(state_test: StateTestFiller, pre: Alloc):
65+
"""Test state transition using Opcodes minilang bytecode."""
5866
```
5967

6068
This is the format of a [Python function](https://docs.python.org/3/tutorial/controlflow.html#defining-functions).
6169
It starts with `def <function name>(<parameters>):`, and then has indented code for the function.
6270
The function definition ends when there is a line that is no longer indented. As with files, by convention functions start with a string that explains what the function does.
6371

64-
!!! note "The `state_test` function argument"
65-
This test defines a state test and, as such, *must* include the `state_test` in its function arguments. This is a callable object (actually a wrapper class to the `StateTest`); we will see how it is called later.
72+
The function parameters (`state_test` and `pre`) are [pytest fixtures](https://docs.pytest.org/en/latest/explanation/fixtures.html) provided by the execution-spec-tests framework. Pytest fixtures are a powerful dependency injection mechanism that automatically provide objects to your test functions.
6673

67-
!!! note "The `pre` function argument"
68-
For all types of tests, it is highly encouraged that we define the `pre` allocation as a function argument, which will be populated with the pre-state requirements during the execution of the test function (see below).
74+
**The `state_test` fixture** is a callable that you *must* include in *state test* function arguments. When called at the end of your test function with the environment, pre-state, transaction, and expected post-state, it generates the actual test fixtures. This callable is a wrapper around the `StateTest` class.
75+
76+
**The `pre` fixture** provides an `Alloc` object that manages the pre-state allocation for your test. It offers methods like `fund_eoa()` and `deploy_contract()` that automatically generate unique addresses and add accounts to the blockchain state that will exist before your transaction executes. The fixture handles address generation and ensures no conflicts occur.
6977

7078
```python
71-
env = Environment()
79+
env = Environment(number=1)
7280
```
7381

74-
This line specifies that `env` is an [`Environment`](https://github.com/ethereum/execution-spec-tests/blob/8b4504aaf6ae0b69c3e847a6c051e64fcefa4db0/src/ethereum_test_tools/common/types.py#L711) object, and that we just use the default parameters.
75-
If necessary we can modify the environment to have different block gas limits, block numbers, etc.
76-
In most tests the defaults are good enough.
77-
78-
For more information, [see the static test documentation](../../running_tests/test_formats/state_test.md).
82+
This line specifies that `env` is an [`Environment`](https://github.com/ethereum/execution-spec-tests/blob/8b4504aaf6ae0b69c3e847a6c051e64fcefa4db0/src/ethereum_test_tools/common/types.py#L711) object. In this example, we only override the block `number` to 1, leaving all other values at their defaults. It's recommended to use default values whenever possible and only specify custom values when required for your specific test scenario.
7983

80-
### Pre State
84+
#### Pre State
8185

82-
For every test we need to define the pre-state requirements, so we are certain of what is on the "blockchain" before the transaction is executed.
83-
It can be used as a [dictionary](https://docs.python.org/3/tutorial/datastructures.html#dictionaries), which is the Python term for an associative array, but the appropriate way to populate it is by using the methods `fund_eoa`, `deploy_contract` or `fund_address` from the `Alloc` object.
86+
For every test we need to define the pre-state requirements, so we are certain of what is on the "blockchain" before the transaction is executed. The `pre` fixture provides an `Alloc` object with methods to create accounts that are automatically added to the pre-state.
8487

8588
In this example we are using the `deploy_contract` method to deploy a contract to some address available in the pre-state.
8689

8790
```python
8891
contract_address = pre.deploy_contract(
89-
code = Op.PUSH1(0)
90-
+ Op.PUSH1(2)
91-
+ Op.PUSH1(1)
92-
+ Op.ADD
93-
+ Op.SSTORE
94-
+ Op.STOP
95-
,
96-
balance=0x0BA1A9CE0BA1A9CE,
92+
code=Op.PUSH1(0x03) + Op.PUSH1(0x00) + Op.SSTORE + Op.STOP
9793
)
9894
```
9995

100-
Specifically we deploy a contract with EVM bytecode that adds two numbers and stores the result in storage.
96+
Specifically we deploy a contract written with Opcodes minilang code that stores the value `0x03` at storage slot `0x00`. The code consists of:
10197

102-
```python
103-
balance=0x0BA1A9CE0BA1A9CE,
104-
```
98+
- `PUSH1(0x03)`: Push the value 3 onto the stack.
99+
- `PUSH1(0x00)`: Push the storage key 0 onto the stack.
100+
- `SSTORE`: Store the value at the specified key.
101+
- `STOP`: End execution.
105102

106-
This field is the balance: the amount of Wei that the account has. It usually doesn't matter what its value is in the case of state test contracts.
103+
As the return value of the `deploy_contract` method, we get the address where the contract was deployed. This address is stored in the `contract_address` variable, which will later be used as the target of our transaction.
107104

108-
```python
109-
contract_address = pre.deploy_contract(
110-
```
105+
You can also specify additional parameters for the contract if needed:
111106

112-
As return value of the `deploy_contract` method we get the address where the contract was deployed and put it in the `contract_address` variable, which will later be used in the transaction.
107+
- `balance` parameter to set the contract's initial balance (though often not necessary for state test contracts)
108+
- `storage` parameter to set initial storage values (though in this example we don't need initial storage since our contract will set it through the `SSTORE` opcode)
113109

114-
```python
115-
storage={
116-
0x00: 0x03,
117-
},
118-
```
110+
You can combine opcodes using the `+` operator to create more complex bytecode sequences.
119111

120-
We could also specify a starting storage for the contract, which is done by adding a `storage` parameter to the `deploy_contract` method.
112+
Generally for execution spec tests the `SSTORE` instruction acts as a high-level assertion method to check pre to post-state changes. The test filler achieves this by verifying that the correct value is held within post-state storage, hence we can validate that the bytecode has run successfully.
121113

122-
Generally for execution spec tests the `sstore` instruction acts as a high-level assertion method to check pre to post-state changes. The test filler achieves this by verifying that the correct value is held within post-state storage, hence we can validate that the code has run successfully.
114+
Next, we need to create an account that will send the transaction to our contract:
123115

124116
```python
125-
sender = pre.fund_eoa(amount=0x0BA1A9CE0BA1A9CE)
117+
sender = pre.fund_eoa()
126118
```
127119

128-
In this line we specify that we require a single externally owned account (EOA) with a balance of `0x0BA1A9CE0BA1A9CE` Wei.
120+
This line creates a single externally owned account (EOA) with a default balance. You can specify a custom amount with `amount=0x0BA1A9CE0BA1A9CE` if needed.
129121

130122
The returned object, which includes a private key, an address, and a nonce, is stored in the `sender` variable and will later be used as the sender of the transaction.
131123

132124
#### Transactions
133125

134126
```python
135127
tx = Transaction(
136-
ty=0x0,
137-
chain_id=0x01,
128+
ty=0x2,
138129
sender=sender,
139130
to=contract_address,
140-
gas_limit=500000,
141-
gas_price=10,
142-
protected=False if fork in [Frontier, Homestead] else True,
131+
gas_limit=100_000,
143132
)
144133
```
145134

146-
With the pre-state built, we can add a description for the [`Transaction`](https://github.com/ethereum/execution-spec-tests/blob/8b4504aaf6ae0b69c3e847a6c051e64fcefa4db0/src/ethereum_test_tools/common/types.py#L887).
147-
148-
```python
149-
sender=sender,
150-
```
151-
152-
We use the sender variable from the pre-state to specify the sender of the transaction, which already has the necessary information to sign the transaction, and also contains the correct `nonce` for the transaction.
135+
With the pre-state built, we can now create the transaction that will call our contract. Let's examine the key components of this [`Transaction`](../../../src/ethereum_test_tools/common/types.py):
153136

154-
The `nonce` is a protection mechanism to prevent replay attacks, and the current rules of Ethereum require that the nonce of a transaction is equal to the number of transactions sent from the sender's address, starting from zero. This means that the first transaction sent from an address must have a nonce of zero, the second transaction must have a nonce of one, and so on.
137+
- **`sender=sender`**: We use the EOA we created earlier, which already has the necessary information to sign the transaction and contains the correct `nonce`. The `nonce` is a protection mechanism to prevent replay attacks - it must equal the number of transactions sent from the sender's address, starting from zero. The framework automatically manages nonce incrementing for us.
155138

156-
The `nonce` field of the `sender` variable is automatically incremented for us by the `Transaction` object when the transaction is signed, so if we were to create another transaction with the same sender, the nonce would be incremented by one yet another time.
139+
- **`to=contract_address`**: This specifies the address of the contract we want to call, which is the contract we deployed earlier.
157140

158-
```python
159-
to=contract_address,
160-
```
141+
- **`gas_limit=100_000`**: This sets a high enough gas limit to ensure our simple contract execution doesn't run out of gas.
161142

162-
The `to` field specifies the address of the contract we want to call and, in this case, it is the address of the contract we deployed earlier.
143+
- **`ty=0x2`**: This specifies the transaction type (EIP-1559).
163144

164-
For more information, [see the static test documentation](../../running_tests/test_formats/state_test.md)
145+
For more information, [see the static test documentation](../../running_tests/test_formats/state_test.md).
165146

166147
#### Post State
167148

149+
Now we need to define what we expect the blockchain state to look like after our transaction executes:
150+
168151
```python
169152
post = {
170153
contract_address: Account(
@@ -177,16 +160,25 @@ For more information, [see the static test documentation](../../running_tests/te
177160

178161
This is the post-state which is equivalent to [`expect`](https://ethereum-tests.readthedocs.io/en/latest/test_filler/state_filler.html#expect) in static tests, but without the indexes. It is similar to the pre-state, except that we do not need to specify everything, only those accounts and fields we wish to test.
179162

180-
In this case, we look at the storage of the contract we called and add to it what we expect to see. In this example storage cell `0x00` should be `0x03` as in the pre-state we essentially stored the result of the instruction `add(1, 2)`.
163+
In this case, we look at the storage of the contract we called and add to it what we expect to see. In this example storage cell `0x00` should be `0x03` as we stored this value using the `SSTORE` opcode in our contract bytecode.
181164

182-
#### State Test
165+
#### Running the State Test
166+
167+
Finally, we execute the test by calling the state test wrapper with all our defined components:
183168

184169
```python
185170
state_test(env=env, pre=pre, post=post, tx=tx)
186171
```
187172

188-
This line calls the wrapper to the `StateTest` object that provides all the objects required (for example, the fork parameter) in order to fill the test, generate the test fixtures and write them to file (by default, `./fixtures/<blockchain,state>_tests/example/test_example.json`).
173+
This line calls the wrapper to the `StateTest` object that provides all the objects required in order to fill the test, generate the test fixtures and write them to file (by default, `./fixtures/<blockchain,state>_tests/example/state_test_example/test_state_test_example.json`).
174+
175+
Note that even though we defined a `StateTest`, the `fill` command will also generate other derivative test fixtures: `BlockchainTest`, `BlockchainTestEngine`, and `BlockchainTestEngineX`. For more information about test types and when to use each, see [Test Types: Prefer StateTest for Single Transactions](../../writing_tests/types_of_tests/#prefer-state_test-for-single-transactions).
189176

190177
## Conclusion
191178

192-
At this point you should be able to state transition tests within a single block.
179+
At this point you should be able to write state transition tests within a single block.
180+
181+
## Next Steps
182+
183+
- Learn about [Adding a New Test](../../writing_tests/adding_a_new_test.md) to understand test organization and structure.
184+
- Explore [Fork Methods](../../writing_tests/fork_methods.md) for writing tests that adapt to different Ethereum forks.

0 commit comments

Comments
 (0)