Skip to content

Commit

Permalink
build the changed files array once
Browse files Browse the repository at this point in the history
  • Loading branch information
winsvega committed Jul 23, 2024
1 parent b4d4ab0 commit e1d10db
Show file tree
Hide file tree
Showing 2 changed files with 253 additions and 42 deletions.
81 changes: 39 additions & 42 deletions .github/workflows/coverage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,31 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3

- name: Fetch target branch
run: git fetch origin ${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}
- name: Fetch github branches and detect introduces .py files
run: |
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
# Fetch changes when PR comes from remote repo
git fetch origin +refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}
git fetch origin +refs/pull/${{ github.event.pull_request.number }}/head:refs/remotes/origin/PR-${{ github.event.pull_request.number }}
files=$(git diff --name-status origin/${{ github.base_ref }}...origin/PR-${{ github.event.pull_request.number }} -- tests/ | grep -E '^[AM]' | grep '\.py$')
else
# Fetch the base branch and the head branch
git fetch origin ${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}
git fetch origin ${{ github.head_ref }}:refs/remotes/origin/${{ github.head_ref }}
files=$(git diff --name-status origin/${{ github.base_ref }}...origin/${{ github.head_ref }} -- tests/ | grep -E '^[AM]' | grep '\.py$')
fi
# Eliminate git diff lines, select only .py paths
echo "Detected changed/new files:"
py_files=()
for file in "${files[@]}"; do
file_fixed=$(echo "$file" | cut -c 3-)
py_files+=("$file_fixed")
echo $file_fixed
done
py_files_str=$(IFS=,; echo "${py_files[*]}")
echo "NEW_TESTS=$py_files_str" >> $GITHUB_ENV
- name: Log in to Docker Hub
uses: docker/login-action@v3
Expand Down Expand Up @@ -125,38 +148,20 @@ jobs:
# This command diffs the .py scripts introduced by a PR
- name: Parse and fill introduced test sources
run: |
source $GITHUB_ENV
IFS=',' read -r -a files <<< "$NEW_TESTS"
python3 -m venv ./venv/
source ./venv/bin/activate
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
# Fetch changes when PR comes from remote repo
git fetch origin +refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}
git fetch origin +refs/pull/${{ github.event.pull_request.number }}/head:refs/remotes/origin/PR-${{ github.event.pull_request.number }}
files=$(git diff --name-status origin/${{ github.base_ref }}...origin/PR-${{ github.event.pull_request.number }} -- tests/ | grep -E '^[AM]' | grep '\.py$')
else
# Fetch the base branch and the head branch
git fetch origin ${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}
git fetch origin ${{ github.head_ref }}:refs/remotes/origin/${{ github.head_ref }}
# Perform the diff
files=$(git diff --name-status origin/${{ github.base_ref }}...origin/${{ github.head_ref }} -- tests/ | grep -E '^[AM]' | grep '\.py$')
fi
echo "Modified or new .py files in tests folder:"
echo "$files" | while read line; do
file=$(echo "$line" | cut -c 3-)
echo $file
done
# fill new tests
# using `|| true` here because if no tests found, pyspec fill returns error code
mkdir -p fixtures/state_tests
mkdir -p fixtures/eof_tests
# Use a while loop with a here-string to avoid subshell issues
while IFS= read -r line; do
file=$(echo "$line" | cut -c 3-)
while IFS= read -r file; do
echo "Fill: $file"
fill "$file" --until=Cancun --evm-bin evmone-t8n || true >> filloutput.log 2>&1
(fill "$file" --fork=CancunEIP7692 --evm-bin evmone-t8n -k eof_test || true) > >(tee -a filloutput.log filloutputEOF.log) 2>&1
done <<< "$files"
Expand All @@ -175,6 +180,10 @@ jobs:
filesState=$(find fixtures/state_tests -type f -name "*.json")
filesEOF=$(find fixtures/eof_tests -type f -name "*.json")
if [ -z "$filesState" ] && [ -z "$filesEOF" ]; then
echo "Error: No filled JSON fixtures found in fixtures."
exit 1
fi
PATCH_TEST_PATH=${{ github.workspace }}/evmtest_coverage/coverage/PATCH_TESTS
mkdir -p $PATCH_TEST_PATH
Expand All @@ -187,17 +196,9 @@ jobs:
echo "--------------------"
echo "converted-ethereum-tests.txt seem untouched, try to fill pre-patched version of .py files:"
if [ "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]; then
files=$(git diff --name-status origin/${{ github.base_ref }}...origin/PR-${{ github.event.pull_request.number }} -- tests/ | grep -E '^[AM]' | grep '\.py$')
else
files=$(git diff --name-status origin/${{ github.base_ref }}...origin/${{ github.head_ref }} -- tests/ | grep -E '^[AM]' | grep '\.py$')
fi
echo "Modified or new .py files in tests folder:"
echo "$files" | while read line; do
file=$(echo "$line" | cut -c 3-)
echo $file
done
# load introduces .py files
source $GITHUB_ENV
IFS=',' read -r -a files <<< "$NEW_TESTS"
git checkout main
PREV_COMMIT=$(git rev-parse HEAD)
Expand All @@ -212,8 +213,8 @@ jobs:
mkdir -p fixtures/state_tests
mkdir -p fixtures/eof_tests
while IFS= read -r line; do
file=$(echo "$line" | cut -c 3-)
while IFS= read -r file; do
echo "Fill: $file"
fill "$file" --until=Cancun --evm-bin evmone-t8n || true >> filloutput.log 2>&1
(fill "$file" --fork=CancunEIP7692 --evm-bin evmone-t8n -k eof_test || true) > >(tee -a filloutput.log filloutputEOF.log) 2>&1
done <<< "$files"
Expand All @@ -225,10 +226,6 @@ jobs:
filesState=$(find fixtures/state_tests -type f -name "*.json")
filesEOF=$(find fixtures/eof_tests -type f -name "*.json")
if [ -z "$filesState" ] && [ -z "$filesEOF" ]; then
echo "Error: No filled JSON fixtures found in fixtures from before the PR."
exit 1
fi
BASE_TEST_PATH=${{ github.workspace }}/evmtest_coverage/coverage/BASE_TESTS
mkdir -p $BASE_TEST_PATH
Expand Down
214 changes: 214 additions & 0 deletions tests/cancun/eip1153_tstore/test_tstore_reentrancy2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
"""
Ethereum Transient Storage EIP Tests
https://eips.ethereum.org/EIPS/eip-1153
"""

from enum import Enum

import pytest

from ethereum_test_tools import (
Account,
Address,
Alloc,
Case,
Environment,
Hash,
StateTestFiller,
Switch,
Transaction,
)
from ethereum_test_tools.vm.opcode import Bytecode
from ethereum_test_tools.vm.opcode import Macros as Om
from ethereum_test_tools.vm.opcode import Opcodes as Op

REFERENCE_SPEC_GIT_PATH = "EIPS/eip-1153.md"
REFERENCE_SPEC_VERSION = "2f8299df31bb8173618901a03a8366a3183479b0"


class CallDestType(Enum):
"""Call dest type"""

REENTRANCY = 1
EXTERNAL_CALL = 2


@pytest.mark.valid_from("Cancun")
@pytest.mark.parametrize("call_type", [Op.CALL, Op.CALLCODE, Op.DELEGATECALL, Op.STATICCALL])
@pytest.mark.parametrize("call_return", [Op.RETURN, Op.REVERT, Om.OOG])
@pytest.mark.parametrize("call_dest_type", [CallDestType.REENTRANCY, CallDestType.EXTERNAL_CALL])
def test_tstore_reentrancy2(
state_test: StateTestFiller,
pre: Alloc,
call_type: Op,
call_return: Op,
call_dest_type: CallDestType,
):
"""
Ported .json vectors:
(06_tstoreInReentrancyCallFiller.yml)
Reentrant calls access the same transient storage
(07_tloadAfterReentrancyStoreFiller.yml)
Successfully returned calls do not revert transient storage writes
(08_revertUndoesTransientStoreFiller.yml)
Revert undoes the transient storage writes from a call.
(09_revertUndoesAllFiller.yml)
Revert undoes all the transient storage writes to the same key from the failed call.
(11_tstoreDelegateCallFiller.yml)
delegatecall manipulates transient storage in the context of the current address.
(13_tloadStaticCallFiller.yml)
Transient storage cannot be manipulated in a static context, tstore reverts
(20_oogUndoesTransientStoreInCallFiller.yml)
Out of gas undoes the transient storage writes from a call.
"""
tload_value_set_before_call = 80
tload_value_set_in_call = 90

# Storage cells
slot_tload_before_call = 0
slot_tload_in_subcall_result = 1
slot_tload_after_call = 2
slot_subcall_worked = 3
slot_tload_1_after_call = 4
slot_tstore_overwrite = 5
slot_code_worked = 6

# Function names
do_tstore = 1
do_reenter = 2
call_dest_address: Bytecode | Address
call_dest_address = Op.ADDRESS()

def make_call(call_type: Op) -> Bytecode:
if call_type == Op.DELEGATECALL or call_type == Op.STATICCALL:
return call_type(Op.GAS(), call_dest_address, 0, 32, 32, 32)
else:
return call_type(Op.GAS(), call_dest_address, 0, 0, 32, 32, 32)

subcall_code = (
Op.TSTORE(0, 89)
+ Op.TSTORE(0, tload_value_set_in_call)
+ Op.TSTORE(1, 11)
+ Op.TSTORE(1, 12)
+ Op.MSTORE(0, Op.TLOAD(0))
+ call_return(0, 32)
)

address_code = pre.deploy_contract(
balance=0,
code=subcall_code,
storage={},
)
if call_dest_type == CallDestType.EXTERNAL_CALL:
call_dest_address = address_code

address_to = pre.deploy_contract(
balance=1_000_000_000_000_000_000,
code=Switch(
cases=[
Case(
condition=Op.EQ(Op.CALLDATALOAD(0), do_tstore),
action=subcall_code,
),
Case(
condition=Op.EQ(Op.CALLDATALOAD(0), do_reenter),
action=Op.TSTORE(0, tload_value_set_before_call)
+ Op.SSTORE(slot_tload_before_call, Op.TLOAD(0))
+ Op.MSTORE(0, do_tstore)
+ Op.MSTORE(32, 0xFF)
+ Op.SSTORE(slot_subcall_worked, make_call(call_type))
+ Op.SSTORE(slot_tload_in_subcall_result, Op.MLOAD(32))
+ Op.SSTORE(slot_tload_after_call, Op.TLOAD(0))
+ Op.SSTORE(slot_tload_1_after_call, Op.TLOAD(1))
+ Op.TSTORE(0, 50)
+ Op.SSTORE(slot_tstore_overwrite, Op.TLOAD(0))
+ Op.SSTORE(slot_code_worked, 1),
),
],
default_action=None,
),
storage={
slot_tload_before_call: 0xFF,
slot_tload_in_subcall_result: 0xFF,
slot_tload_after_call: 0xFF,
slot_subcall_worked: 0xFF,
slot_tload_1_after_call: 0xFF,
slot_tstore_overwrite: 0xFF,
slot_code_worked: 0xFF,
},
)

on_failing_calls = call_type == Op.STATICCALL or call_return in [Op.REVERT, Om.OOG]
on_successful_delegate_or_callcode = call_type in [
Op.DELEGATECALL,
Op.CALLCODE,
] and call_return not in [Op.REVERT, Om.OOG]

if call_dest_type == CallDestType.REENTRANCY:
post = {
address_to: Account(
storage={
slot_code_worked: 1,
slot_tload_before_call: tload_value_set_before_call,
slot_tload_in_subcall_result: (
# we fail to obtain in call result if it fails
0xFF
if call_type == Op.STATICCALL or call_return == Om.OOG
else tload_value_set_in_call
),
# reentrant tstore overrides value in upper level
slot_tload_after_call: (
tload_value_set_before_call
if on_failing_calls
else tload_value_set_in_call
),
slot_tload_1_after_call: 0 if on_failing_calls else 12,
slot_tstore_overwrite: 50,
# tstore in static call not allowed
slot_subcall_worked: 0 if on_failing_calls else 1,
}
)
}
else:
post = {
address_to: Account(
storage={
slot_code_worked: 1,
slot_tload_before_call: tload_value_set_before_call,
slot_tload_in_subcall_result: (
# we fail to obtain in call result if it fails
0xFF
if call_type == Op.STATICCALL or call_return == Om.OOG
else tload_value_set_in_call
),
# external tstore overrides value in upper level only in delegate and callcode
slot_tload_after_call: (
tload_value_set_in_call
if on_successful_delegate_or_callcode
else tload_value_set_before_call
),
slot_tload_1_after_call: 12 if on_successful_delegate_or_callcode else 0,
slot_tstore_overwrite: 50,
# tstore in static call not allowed, reentrancy means external call here
slot_subcall_worked: 0 if on_failing_calls else 1,
}
)
}

tx = Transaction(
sender=pre.fund_eoa(7_000_000_000_000_000_000),
to=address_to,
gas_price=10,
data=Hash(do_reenter),
gas_limit=5000000,
value=0,
)

state_test(env=Environment(), pre=pre, post=post, tx=tx)

0 comments on commit e1d10db

Please sign in to comment.