Skip to content

Commit

Permalink
Add tests for CCIP Read offchain resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
fselmo committed May 11, 2022
1 parent c345ae4 commit 206626b
Show file tree
Hide file tree
Showing 19 changed files with 761 additions and 58 deletions.
6 changes: 3 additions & 3 deletions ens/contract_data.py

Large diffs are not rendered by default.

120 changes: 120 additions & 0 deletions tests/core/contracts/test_offchain_lookup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import pytest

from eth_abi import (
decode_abi,
)

from web3._utils.module_testing.offchain_lookup_contract import (
OFFCHAIN_LOOKUP_ABI,
OFFCHAIN_LOOKUP_BYTECODE,
OFFCHAIN_LOOKUP_BYTECODE_RUNTIME,
)
from web3._utils.module_testing.utils import (
mock_offchain_lookup_request_response,
)
from web3._utils.type_conversion_utils import (
to_hex_if_bytes,
)
from web3.exceptions import (
TooManyRequests,
)

# "test offchain lookup" as an abi-encoded string
OFFCHAIN_LOOKUP_TEST_DATA = '0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001474657374206f6666636861696e206c6f6f6b7570000000000000000000000000' # noqa: E501
# "web3py" as an abi-encoded string
WEB3PY_AS_HEXBYTES = '0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067765623370790000000000000000000000000000000000000000000000000000' # noqa: E501


@pytest.fixture
def OffchainLookup(w3):
# compiled from `web3/_utils/module_testing/contract_sources/OffchainLookup.sol`
return w3.eth.contract(
abi=OFFCHAIN_LOOKUP_ABI,
bytecode=OFFCHAIN_LOOKUP_BYTECODE,
bytecode_runtime=OFFCHAIN_LOOKUP_BYTECODE_RUNTIME,
)


@pytest.fixture
def offchain_lookup_contract(
w3, wait_for_block, OffchainLookup, wait_for_transaction, address_conversion_func,
):
wait_for_block(w3)
deploy_txn_hash = OffchainLookup.constructor().transact({'gas': 10000000})
deploy_receipt = wait_for_transaction(w3, deploy_txn_hash)
contract_address = address_conversion_func(deploy_receipt['contractAddress'])

bytecode = w3.eth.get_code(contract_address)
assert bytecode == OffchainLookup.bytecode_runtime
deployed_offchain_lookup = OffchainLookup(address=contract_address)
assert deployed_offchain_lookup.address == contract_address
return deployed_offchain_lookup


def test_offchain_lookup_functionality(
offchain_lookup_contract, monkeypatch,
):
normalized_address = to_hex_if_bytes(offchain_lookup_contract.address)
mock_offchain_lookup_request_response(
monkeypatch,
mocked_request_url=f'https://web3.py/gateway/{normalized_address}/{OFFCHAIN_LOOKUP_TEST_DATA}.json', # noqa: E501
mocked_json_data=WEB3PY_AS_HEXBYTES,
)
response = offchain_lookup_contract.caller.testOffchainLookup(OFFCHAIN_LOOKUP_TEST_DATA)
assert decode_abi(['string'], response)[0] == 'web3py'


@pytest.mark.parametrize('status_code_non_4xx_error', [100, 300, 500, 600])
def test_eth_call_offchain_lookup_tries_next_url_for_non_4xx_error_status_and_tests_POST(
offchain_lookup_contract, monkeypatch, status_code_non_4xx_error,
) -> None:
normalized_contract_address = to_hex_if_bytes(offchain_lookup_contract.address).lower()

# The next url in our test contract doesn't contain '{data}', triggering the POST request
# logic. The idea here is to return a bad status for the first url (GET) and a success
# status from the second call (POST) to test both that we move on to the next url with
# non 4xx status and that the POST logic is also working as expected.
mock_offchain_lookup_request_response(
monkeypatch,
mocked_request_url=f'https://web3.py/gateway/{normalized_contract_address}/{OFFCHAIN_LOOKUP_TEST_DATA}.json', # noqa: E501
mocked_status_code=status_code_non_4xx_error,
mocked_json_data=WEB3PY_AS_HEXBYTES,
)
mock_offchain_lookup_request_response(
monkeypatch,
http_method='POST',
mocked_request_url=f'https://web3.py/gateway/{normalized_contract_address}.json',
mocked_status_code=200,
mocked_json_data=WEB3PY_AS_HEXBYTES,
sender=normalized_contract_address,
calldata=OFFCHAIN_LOOKUP_TEST_DATA,
)
response = offchain_lookup_contract.caller.testOffchainLookup(OFFCHAIN_LOOKUP_TEST_DATA)
assert decode_abi(['string'], response)[0] == 'web3py'


@pytest.mark.parametrize('status_code_4xx_error', [400, 410, 450, 499])
def test_eth_call_offchain_lookup_calls_raise_for_status_for_4xx_status_code(
offchain_lookup_contract, monkeypatch, status_code_4xx_error,
) -> None:
normalized_contract_address = to_hex_if_bytes(offchain_lookup_contract.address).lower()
mock_offchain_lookup_request_response(
monkeypatch,
mocked_request_url=f'https://web3.py/gateway/{normalized_contract_address}/{OFFCHAIN_LOOKUP_TEST_DATA}.json', # noqa: E501
mocked_status_code=status_code_4xx_error,
mocked_json_data=WEB3PY_AS_HEXBYTES,
)
with pytest.raises(Exception, match="called raise_for_status\\(\\)"):
offchain_lookup_contract.caller.testOffchainLookup(OFFCHAIN_LOOKUP_TEST_DATA)


def test_offchain_lookup_raises_on_continuous_redirect(
offchain_lookup_contract, monkeypatch,
):
normalized_address = to_hex_if_bytes(offchain_lookup_contract.address)
mock_offchain_lookup_request_response(
monkeypatch,
mocked_request_url=f'https://web3.py/gateway/{normalized_address}/0x.json',
)
with pytest.raises(TooManyRequests, match="Too many CCIP read redirects"):
offchain_lookup_contract.caller.continuousOffchainLookup()
16 changes: 8 additions & 8 deletions tests/ens/test_offchain_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@


class MockHttpSuccessResponse:
status_code = 200

def __init__(self, request_type, *args, **_kwargs):
# validate the expected urls
if request_type == 'get':
Expand All @@ -54,25 +56,23 @@ def __init__(self, request_type, *args, **_kwargs):
assert args[1] == EXPECTED_POST_URL

@staticmethod
def raise_for_status():
pass
def raise_for_status(): pass # noqa: E704

@staticmethod
def json():
return {'data': OFFCHAIN_RESOLVER_DATA}
def json(): return {'data': OFFCHAIN_RESOLVER_DATA} # noqa: E704


class MockHttpBadFormatResponse:
status_code = 200

def __init__(self, *args):
assert args[1] == EXPECTED_GET_URL

@staticmethod
def raise_for_status():
pass
def raise_for_status(): pass # noqa: E704

@staticmethod
def json():
return {'not_data': OFFCHAIN_RESOLVER_DATA}
def json(): return {'not_data': OFFCHAIN_RESOLVER_DATA} # noqa: E704


def test_offchain_resolution_with_get_request(ens, monkeypatch):
Expand Down
12 changes: 12 additions & 0 deletions tests/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
MATH_ABI,
MATH_BYTECODE,
)
from web3._utils.module_testing.offchain_lookup_contract import (
OFFCHAIN_LOOKUP_ABI,
OFFCHAIN_LOOKUP_BYTECODE,
)
from web3._utils.module_testing.revert_contract import (
_REVERT_CONTRACT_ABI,
REVERT_CONTRACT_BYTECODE,
Expand Down Expand Up @@ -37,6 +41,14 @@ def revert_contract_factory(w3):
return contract_factory


@pytest.fixture(scope="module")
def offchain_lookup_contract_factory(w3):
contract_factory = w3.eth.contract(
abi=OFFCHAIN_LOOKUP_ABI, bytecode=OFFCHAIN_LOOKUP_BYTECODE
)
return contract_factory


@pytest.fixture(scope="module")
def event_loop(request):
loop = asyncio.get_event_loop_policy().new_event_loop()
Expand Down
29 changes: 26 additions & 3 deletions tests/integration/generate_fixtures/go_ethereum.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import pprint
import shutil
import socket
import subprocess
import sys
import time
Expand All @@ -21,9 +22,6 @@
)

import common
from tests.utils import (
get_open_port,
)
from web3 import Web3
from web3._utils.module_testing.emitter_contract import (
CONTRACT_EMITTER_ABI,
Expand All @@ -34,12 +32,24 @@
MATH_ABI,
MATH_BYTECODE,
)
from web3._utils.module_testing.offchain_lookup_contract import (
OFFCHAIN_LOOKUP_ABI,
OFFCHAIN_LOOKUP_BYTECODE,
)
from web3._utils.module_testing.revert_contract import (
_REVERT_CONTRACT_ABI,
REVERT_CONTRACT_BYTECODE,
)


def get_open_port():
sock = socket.socket()
sock.bind(('127.0.0.1', 0))
port = sock.getsockname()[1]
sock.close()
return str(port)


@contextlib.contextmanager
def graceful_kill_on_exit(proc):
try:
Expand Down Expand Up @@ -258,6 +268,18 @@ def setup_chain_state(w3):
block_hash_revert_no_msg = w3.eth.get_block(txn_receipt_revert_with_no_msg['blockHash'])
print('BLOCK_HASH_REVERT_NO_MSG:', block_hash_revert_no_msg['hash'])

#
# Offchain Lookup Contract
#
offchain_lookup_factory = w3.eth.contract(
abi=OFFCHAIN_LOOKUP_ABI,
bytecode=OFFCHAIN_LOOKUP_BYTECODE,
)
offchain_lookup_deploy_receipt = common.deploy_contract(
w3, 'offchain_lookup', offchain_lookup_factory
)
assert is_dict(offchain_lookup_deploy_receipt)

#
# Empty Block
#
Expand Down Expand Up @@ -290,6 +312,7 @@ def setup_chain_state(w3):
'math_address': math_deploy_receipt['contractAddress'],
'emitter_deploy_txn_hash': emitter_deploy_receipt['transactionHash'],
'emitter_address': emitter_deploy_receipt['contractAddress'],
'offchain_lookup_address': offchain_lookup_deploy_receipt['contractAddress'],
'txn_hash_with_log': txn_hash_with_log,
'block_hash_with_log': block_with_log['hash'],
'empty_block_hash': empty_block['hash'],
Expand Down
Binary file modified tests/integration/geth-1.10.17-fixture.zip
Binary file not shown.
5 changes: 5 additions & 0 deletions tests/integration/go_ethereum/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,8 @@ def block_hash_revert_with_msg(geth_fixture_data):
@pytest.fixture(scope="module")
def revert_contract(revert_contract_factory, geth_fixture_data):
return revert_contract_factory(address=geth_fixture_data['revert_address'])


@pytest.fixture(scope="module")
def offchain_lookup_contract(offchain_lookup_contract_factory, geth_fixture_data):
return offchain_lookup_contract_factory(address=geth_fixture_data['offchain_lookup_address'])
15 changes: 15 additions & 0 deletions tests/integration/test_ethereum_tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ def revert_contract(w3, revert_contract_factory, revert_contract_deploy_txn_hash
return revert_contract_factory(contract_address)


#
# Offchain Lookup Contract Setup
#
@pytest.fixture(scope="module")
def offchain_lookup_contract(w3, offchain_lookup_contract_factory):
deploy_txn_hash = offchain_lookup_contract_factory.constructor().transact(
{'from': w3.eth.coinbase}
)
deploy_receipt = w3.eth.wait_for_transaction_receipt(deploy_txn_hash)
assert is_dict(deploy_receipt)
contract_address = deploy_receipt['contractAddress']
assert is_checksum_address(contract_address)
return offchain_lookup_contract_factory(contract_address)


UNLOCKABLE_PRIVATE_KEY = '0x392f63a79b1ff8774845f3fa69de4a13800a59e7083f5187f1558f0797ad0f01'


Expand Down
File renamed without changes.
54 changes: 54 additions & 0 deletions web3/_utils/contract_sources/OffchainLookup.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// This contract is meant to test CCIP Read / Offchain Lookup functionality as part of EIP-3668. Multiple functions
// may be added here for testing and the contract can be recompiled for `test_offchain_lookup.py` and other tests.

pragma solidity ^0.8.13;

contract OffchainLookupTests {
string[] urls = ["https://web3.py/gateway/{sender}/{data}.json", "https://web3.py/gateway/{sender}.json"];

error OffchainLookup(address sender, string[] urls, bytes callData, bytes4 callbackFunction, bytes extraData);

// This function is meant to test the offchain lookup functionality specified in EIP-3668.
function testOffchainLookup(bytes calldata specifiedDataFromTest) external returns(bytes memory) {
// assert that the test specifies "offchain lookup test" to start the test
string memory dataFromTestAsString = abi.decode(specifiedDataFromTest, (string));
require(keccak256(abi.encodePacked(dataFromTestAsString)) == keccak256("test offchain lookup"), "test data validation failed.");

revert OffchainLookup(
address(this),
urls,
specifiedDataFromTest,
this.testOffchainLookupWithProof.selector,
specifiedDataFromTest
);
}

function testOffchainLookupWithProof(bytes calldata result, bytes calldata extraData) external returns(bytes memory) {
// assert the result came from the mocked response from our tests... must mock in tests with the appropriate
// value and validate the assertion here.
string memory resultAsString = abi.decode(result, (string));
require(keccak256(abi.encodePacked(resultAsString)) == keccak256("web3py"), "http request result validation failed.");

// assert this `extraData` value is the same test-provided value from the original revert at `testOffchainLookup()`
string memory extraDataAsString = abi.decode(extraData, (string));
require(keccak256(abi.encodePacked(extraDataAsString)) == keccak256("test offchain lookup"), "extraData validation failed.");

return result;
}

// This function is meant to test that continuous OffchainLookup reverts raise an exception after too many
// redirects. This example technically breaks the flow described in EIP-3668 but this is solely meant to trigger
// continuous OffchainLookup reverts and test that we catch this sort of activity and stop it. Currently this limit
// is set to 4 redirects.
function continuousOffchainLookup() external returns(bytes memory) {
bytes memory _callData;

revert OffchainLookup(
address(this),
urls,
_callData,
this.continuousOffchainLookup.selector,
_callData
);
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// This contract is a much simpler version of the contract set up on mainnet for 'offchainexample.eth', deployed at
// the address 0xC1735677a60884ABbCF72295E88d47764BeDa282. We get rid of the 'expires' time constraint on the
// SignatureVerifier.verify() check so that we can use an actual 'data' payload that comes back from resolving via
// the OffchainResolver for 'offchainexample.eth' on mainnet. A bit hacky but it works for the purpose of testing :)
/**
* This contract is a much simpler version of the contract set up on mainnet for 'offchainexample.eth', deployed at
* the address 0xC1735677a60884ABbCF72295E88d47764BeDa282. We get rid of the 'expires' time constraint on the
* SignatureVerifier.verify() check so that we can use an actual 'data' payload that comes back from resolving via
* the OffchainResolver for 'offchainexample.eth' on mainnet. A bit hacky but it works for the purpose of testing :)
*/

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Strings.sol)
Expand Down
File renamed without changes.
2 changes: 2 additions & 0 deletions web3/_utils/module_testing/emitter_contract_old.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
# compiled with `0.8.11`.
# See: https://github.com/ethereum/web3.py/issues/2301

# contract source at web3/_utils/contract_sources/Emitter_old.sol

CONTRACT_EMITTER_CODE_OLD = (
"608060405234801561001057600080fd5b50610aed806100206000396000f300608060405260043"
"6106100ae5763ffffffff7c01000000000000000000000000000000000000000000000000000000"
Expand Down
Loading

0 comments on commit 206626b

Please sign in to comment.