From e1d10dbc5d0e517debe5ca9006d91b666fde76b5 Mon Sep 17 00:00:00 2001 From: Dimitry Kh Date: Tue, 23 Jul 2024 12:30:10 +0200 Subject: [PATCH] build the changed files array once --- .github/workflows/coverage.yaml | 81 ++++--- .../eip1153_tstore/test_tstore_reentrancy2.py | 214 ++++++++++++++++++ 2 files changed, 253 insertions(+), 42 deletions(-) create mode 100644 tests/cancun/eip1153_tstore/test_tstore_reentrancy2.py diff --git a/.github/workflows/coverage.yaml b/.github/workflows/coverage.yaml index 0f63a825f6..a0add571a4 100644 --- a/.github/workflows/coverage.yaml +++ b/.github/workflows/coverage.yaml @@ -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 @@ -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" @@ -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 @@ -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) @@ -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" @@ -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 diff --git a/tests/cancun/eip1153_tstore/test_tstore_reentrancy2.py b/tests/cancun/eip1153_tstore/test_tstore_reentrancy2.py new file mode 100644 index 0000000000..9b4af1c3b7 --- /dev/null +++ b/tests/cancun/eip1153_tstore/test_tstore_reentrancy2.py @@ -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)