State Transition Tests¶
This tutorial teaches you to create a state transition execution specification test. These tests verify that a blockchain, starting from a defined pre-state, will reach a specified post-state after executing a set of specific transactions.
Pre-requisites¶
Before proceeding with this tutorial, it is assumed that you have prior knowledge and experience with the following:
- Set up and run an execution specification test as outlined in the quick start guide.
- Understand how to read a static state transition test.
- Know the basics of Yul, which is an EVM assembly language.
- Familiarity with Python.
Example Tests¶
The most effective method of learning how to write tests is to study a couple of straightforward examples. In this tutorial we will go over the Yul state test.
Yul Test¶
You can find the source code for the Yul test in tests/homestead/yul/test_yul_example.py. It is the spec test equivalent of this static test.
Lets examine each section.
"""
Test Yul Source Code Examples
"""
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.
from ethereum_test_forks import Fork
from ethereum_test_tools import (
Account,
Environment,
StateTestFiller,
TestAddress,
Transaction,
Yul,
)
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.
@pytest.mark.valid_from("Berlin")
In Python this kind of definition is called a decorator. It modifies the action of the function after it. In this case, the decorator is a custom pytest fixture defined by the execution-specs-test framework that specifies that the test is valid for the Berlin fork and all forks after it. The framework will then execute this test case for all forks in the fork range specified by the command-line arguments.
Executing the test
To execute this test for all the specified forks, we can specify pytest's -k
flag that filters test cases by keyword expression:
fill -k test_yul
and to execute it for a specific fork range, we can provide the --from
and --until
command-line arguments:
fill -k test_yul --from London --until Merge
def test_yul(state_test: StateTestFiller, fork: Fork):
"""
Test YUL compiled bytecode.
"""
This is the format of a Python function.
It starts with def <function name>(<parameters>):
, and then has indented code for the function.
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.
The state_test
function argument
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.
env = Environment()
This line specifies that env
is an Environment
object, and that we just use the default parameters.
If necessary we can modify the environment to have different block gas limits, block numbers, etc.
In most tests the defaults are good enough.
For more information, see the static test documentation.
Pre State¶
pre = {
Here we define the pre-state section, the one that tells us what is on the "blockchain" before the test. It is a dictionary, which is the Python term for an associative array.
"0x1000000000000000000000000000000000000000": Account(
The keys of the dictionary are addresses (as strings), and the values are Account
objects.
You can read more about address fields in the static test documentation.
balance=0x0BA1A9CE0BA1A9CE,
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.
code=Yul(
Here we define the Yul code for the contract. It is defined as a multi-line string and starts and ends with curly braces ({ <yul> }
).
When running the test filler fill
, the solidity compiler solc
will automatically translate the Yul to EVM opcode at runtime.
Note
Currently Yul and direct EVM opcode are supported in execution spec tests. LLL and Solidity may be supported in the future.
"""
{
function f(a, b) -> c {
c := add(a, b)
}
sstore(0, f(1, 2))
return(0, 32)
}
"""
),
),
Within this example test Yul code we have a function definition, and inside it we are using the Yul add
instruction. When compiled with solc
it translates the instruction directly to the ADD
opcode. For further Yul instructions see here. Notice that function is utilized with the Yul sstore
instruction, which stores the result of add(1, 2)
to the storage address 0x00
.
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 Yul code has run successfully.
TestAddress: Account(balance=0x0BA1A9CE0BA1A9CE),
}
TestAddress
is an address for which the test filler has the private key.
This means that the test runner can issue a transaction as that contract.
Of course, this address also needs a balance to be able to issue transactions.
Transactions¶
tx = Transaction(
ty=0x0,
chain_id=0x0,
nonce=0,
to="0x1000000000000000000000000000000000000000",
gas_limit=500000,
gas_price=10,
protected=False,
)
With the pre-state specified, we can add a description for the Transaction
.
For more information, see the static test documentation
Post State¶
post = {
"0x1000000000000000000000000000000000000000": Account(
storage={
0x00: 0x03,
},
),
}
This is the post-state which is equivalent to 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.
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 Yul instruction add(1, 2)
.
State Test¶
state_test(env=env, pre=pre, post=post, txs=[tx])
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/example/yul_example/test_yul.json
).
Conclusion¶
At this point you should be able to state transition tests within a single block.