Skip to content

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:

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 here. 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 theADD opcode. For further Yul instructions see here. Notice that function is utilised 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.