Skip to content

Commit 2757408

Browse files
feat(tests): EIP-6110 DepositEvent layout+topic tests (#1371)
* new(tests): Add EIP-6110 Sepolia Variant Contract test * fix(tests): Log code * fix(tests): Log code * fix: ABI encoding optional * fix: parametrize for ABI (No-ABI test disabled) * fix(docs): add docs to helper * fix(tests): lint modified_contract (?) * feat(tests): add DEPOSIT_EVENT_SIGNATURE_HASH EIP-6110 * tmp: add sepolia deposit log data * change test and helper methods * convert back to little-endian encoding * feat(helpers): add helper method to construct modified deposit logs * feat(tests): use new helper in modified contract tests * update test modified contract * fix test filler to produce the correct requests hash * feat(tests): add EIP6110 invalid event layout tests * feat(tests): add exception to transaction * cleanup(tests): use DEFAULTs where appropriate * feat(tests): add invalid log length test * chore(tests) lint * Update tests/prague/eip6110_deposits/helpers.py Co-authored-by: Mario Vega <[email protected]> * Update tests/prague/eip6110_deposits/helpers.py Co-authored-by: Mario Vega <[email protected]> * Update tests/prague/eip6110_deposits/test_modified_contract.py Co-authored-by: Mario Vega <[email protected]> * Update tests/prague/eip6110_deposits/test_modified_contract.py Co-authored-by: Mario Vega <[email protected]> * Update tests/prague/eip6110_deposits/helpers.py Co-authored-by: Mario Vega <[email protected]> * Update tests/prague/eip6110_deposits/helpers.py Co-authored-by: Mario Vega <[email protected]> * chore(tests): lint * chore(tests): make typecheck say "congratulations" * chore(tests): add pytestmark * chore(tests): remove TODO * Update tests/prague/eip6110_deposits/test_modified_contract.py --------- Co-authored-by: Mario Vega <[email protected]>
1 parent 297322f commit 2757408

File tree

4 files changed

+330
-1
lines changed

4 files changed

+330
-1
lines changed

src/ethereum_test_exceptions/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,11 @@ class TransactionException(ExceptionBase):
385385
"""
386386
Transaction type 4 included before activation fork.
387387
"""
388+
INVALID_DEPOSIT_EVENT_LAYOUT = auto()
389+
"""
390+
Transaction emits a `DepositEvent` in the deposit contract (EIP-6110), but the layout
391+
of the event does not match the required layout.
392+
"""
388393

389394

390395
@unique

tests/prague/eip6110_deposits/helpers.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,62 @@ def sha256(*args: bytes) -> bytes:
1717
return sha256_hashlib(b"".join(args)).digest()
1818

1919

20+
def create_deposit_log_bytes(
21+
pubkey_size: int = 48,
22+
pubkey_data: bytes = b"",
23+
pubkey_offset: int = 160,
24+
withdrawal_credentials_size: int = 32,
25+
withdrawal_credentials_data: bytes = b"",
26+
withdrawal_credentials_offset: int = 256,
27+
amount_size: int = 8,
28+
amount_data: bytes = b"",
29+
amount_offset: int = 320,
30+
signature_size: int = 96,
31+
signature_data: bytes = b"",
32+
signature_offset: int = 384,
33+
index_size: int = 8,
34+
index_data: bytes = b"",
35+
index_offset: int = 512,
36+
) -> bytes:
37+
"""Create the deposit log bytes."""
38+
result = bytearray(576)
39+
offset = 0
40+
41+
def write_uint256(value: int):
42+
nonlocal offset
43+
result[offset : offset + 32] = value.to_bytes(32, byteorder="big")
44+
offset += 32
45+
46+
def write_bytes(data: bytes, size: int):
47+
nonlocal offset
48+
padded = data.ljust(size, b"\x00")
49+
result[offset : offset + size] = padded
50+
offset += size
51+
52+
write_uint256(pubkey_offset)
53+
write_uint256(withdrawal_credentials_offset)
54+
write_uint256(amount_offset)
55+
write_uint256(signature_offset)
56+
write_uint256(index_offset)
57+
58+
write_uint256(pubkey_size)
59+
write_bytes(pubkey_data, 64)
60+
61+
write_uint256(withdrawal_credentials_size)
62+
write_bytes(withdrawal_credentials_data, 32)
63+
64+
write_uint256(amount_size)
65+
write_bytes(amount_data, 32)
66+
67+
write_uint256(signature_size)
68+
write_bytes(signature_data, 96)
69+
70+
write_uint256(index_size)
71+
write_bytes(index_data, 32)
72+
73+
return bytes(result)
74+
75+
2076
class DepositRequest(DepositRequestBase):
2177
"""Deposit request descriptor."""
2278

@@ -94,6 +150,45 @@ def calldata(self) -> bytes:
94150
+ self.signature
95151
)
96152

153+
def log(self, *, include_abi_encoding: bool = True) -> bytes:
154+
"""
155+
Return the log data for the deposit event.
156+
157+
event DepositEvent(
158+
bytes pubkey,
159+
bytes withdrawal_credentials,
160+
bytes amount,
161+
bytes signature,
162+
bytes index
163+
);
164+
"""
165+
data = bytearray(576)
166+
if include_abi_encoding:
167+
# Insert ABI encoding
168+
data[30:32] = b"\x00\xa0" # Offset: pubkey (160)
169+
data[62:64] = b"\x01\x00" # Offset: withdrawal_credentials (256)
170+
data[94:96] = b"\x01\x40" # Offset: amount (320)
171+
data[126:128] = b"\x01\x80" # Offset: signature (384)
172+
data[158:160] = b"\x02\x00" # Offset: index (512)
173+
data[190:192] = b"\x00\x30" # Size: pubkey (48)
174+
data[286:288] = b"\x00\x20" # Size: withdrawal_credentials (32)
175+
data[350:352] = b"\x00\x08" # Size: amount (8)
176+
data[414:416] = b"\x00\x60" # Size: signature (96)
177+
data[542:544] = b"\x00\x08" # Size: index (8)
178+
offset = 192
179+
data[offset : offset + len(self.pubkey)] = self.pubkey # [192:240]
180+
offset += 48 + len(self.pubkey)
181+
data[offset : offset + len(self.withdrawal_credentials)] = (
182+
self.withdrawal_credentials
183+
) # [288:320]
184+
offset += 32 + len(self.withdrawal_credentials)
185+
data[offset : offset + 8] = (self.amount).to_bytes(8, byteorder="little") # [352:360]
186+
offset += 56 + 8
187+
data[offset : offset + len(self.signature)] = self.signature # [416:512]
188+
offset += 32 + len(self.signature)
189+
data[offset : offset + 8] = (self.index).to_bytes(8, byteorder="little") # [544:552]
190+
return bytes(data)
191+
97192
def with_source_address(self, source_address: Address) -> "DepositRequest":
98193
"""Return a copy."""
99194
return self.copy()

tests/prague/eip6110_deposits/spec.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,7 @@ class Spec:
2121
https://eips.ethereum.org/EIPS/eip-6110.
2222
"""
2323

24-
DEPOSIT_CONTRACT_ADDRESS = 0x00000000219AB540356CBB839CBE05303D7705FA
24+
DEPOSIT_CONTRACT_ADDRESS = 0x00000000219AB540356CBB839CBE05303D7705FA # Mainnet
25+
DEPOSIT_EVENT_SIGNATURE_HASH = (
26+
0x649BBC62D0E31342AFEA4E5CD82D4049E7E1EE912FC0889AA790803BE39038C5
27+
)
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
"""Test variants of the deposit contract which adheres the log-style as described in EIP-6110."""
2+
3+
import pytest
4+
5+
from ethereum_test_exceptions.exceptions import TransactionException
6+
from ethereum_test_tools import (
7+
Account,
8+
Alloc,
9+
Block,
10+
BlockchainTestFiller,
11+
Header,
12+
Requests,
13+
Transaction,
14+
)
15+
from ethereum_test_tools import Macros as Om
16+
from ethereum_test_tools import Opcodes as Op
17+
18+
from .helpers import DepositRequest, create_deposit_log_bytes
19+
from .spec import Spec, ref_spec_6110
20+
21+
pytestmark = [
22+
pytest.mark.valid_from("Prague"),
23+
pytest.mark.execute(pytest.mark.skip(reason="modifies pre-alloc")),
24+
]
25+
26+
REFERENCE_SPEC_GIT_PATH = ref_spec_6110.git_path
27+
REFERENCE_SPEC_VERSION = ref_spec_6110.version
28+
29+
EVENT_ARGUMENTS_NAMES = ["pubkey", "withdrawal_credentials", "amount", "signature", "index"]
30+
EVENT_ARGUMENTS_LAYOUT_TYPE = ["size", "offset"]
31+
EVENT_ARGUMENTS = [
32+
f"{name}_{layout}" for name in EVENT_ARGUMENTS_NAMES for layout in EVENT_ARGUMENTS_LAYOUT_TYPE
33+
]
34+
EVENT_ARGUMENT_VALUES = ["zero", "max_uint256"]
35+
36+
37+
DEFAULT_DEPOSIT_REQUEST = DepositRequest(
38+
pubkey=0x01,
39+
withdrawal_credentials=0x02,
40+
amount=120_000_000_000_000_000,
41+
signature=0x03,
42+
index=0x0,
43+
)
44+
DEFAULT_DEPOSIT_REQUEST_LOG_DATA_DICT = {
45+
"pubkey_data": bytes(DEFAULT_DEPOSIT_REQUEST.pubkey),
46+
"withdrawal_credentials_data": bytes(DEFAULT_DEPOSIT_REQUEST.withdrawal_credentials),
47+
# Note: after converting to bytes, it is converted to little-endian by `[::-1]`
48+
# (This happens on-chain also, but this is done by the solidity contract)
49+
"amount_data": bytes.fromhex("0" + DEFAULT_DEPOSIT_REQUEST.amount.hex()[2:])[::-1],
50+
"signature_data": bytes(DEFAULT_DEPOSIT_REQUEST.signature),
51+
"index_data": bytes(DEFAULT_DEPOSIT_REQUEST.index),
52+
}
53+
DEFAULT_REQUEST_LOG = create_deposit_log_bytes(**DEFAULT_DEPOSIT_REQUEST_LOG_DATA_DICT) # type: ignore
54+
55+
56+
@pytest.mark.parametrize(
57+
"include_deposit_event",
58+
[
59+
pytest.param(True),
60+
pytest.param(False),
61+
],
62+
)
63+
def test_extra_logs(
64+
blockchain_test: BlockchainTestFiller,
65+
pre: Alloc,
66+
include_deposit_event: bool,
67+
):
68+
"""Test deposit contract emitting more log event types than the ones in mainnet."""
69+
# Supplant mainnet contract with a variant that emits a `Transfer`` log
70+
# If `include_deposit_event` is `True``, it will also emit a `DepositEvent` log`
71+
72+
# ERC20 token transfer log (Sepolia)
73+
# https://sepolia.etherscan.io/tx/0x2d71f3085a796a0539c9cc28acd9073a67cf862260a41475f000dd101279f94f
74+
# JSON RPC:
75+
# curl https://sepolia.infura.io/v3/APIKEY \
76+
# -X POST \
77+
# -H "Content-Type: application/json" \
78+
# -d '{"jsonrpc": "2.0", "method": "eth_getLogs",
79+
# "params": [{"address": "0x7f02C3E3c98b133055B8B348B2Ac625669Ed295D",
80+
# "blockHash": "0x8062a17fa791f5dbd59ea68891422e3299ca4e80885a89acf3fc706c8bceef53"}],
81+
# "id": 1}'
82+
83+
# {"jsonrpc":"2.0","id":1,"result":
84+
# [{"removed":false,"logIndex":"0x80","transactionIndex":"0x56",
85+
# "transactionHash":"0x2d71f3085a796a0539c9cc28acd9073a67cf862260a41475f000dd101279f94f",
86+
# "blockHash":"0x8062a17fa791f5dbd59ea68891422e3299ca4e80885a89acf3fc706c8bceef53",
87+
# "blockNumber":"0x794fb5",
88+
# "address":"0x7f02c3e3c98b133055b8b348b2ac625669ed295d",
89+
# "data":"0x0000000000000000000000000000000000000000000000000000000000000001",
90+
# "topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
91+
# "0x0000000000000000000000006885e36bfcb68cb383dfe90023a462c03bcb2ae5",
92+
# "0x00000000000000000000000080b5dc88c98e528bf9cb4b7f0f076ac41da24651"]
93+
94+
bytecode = Op.LOG3(
95+
# ERC-20 token transfer log
96+
# ERC-20 token transfers are LOG3, since the topic, the sender, and receiver
97+
# are all topics (the sender and receiver are `indexed` in the solidity event)
98+
0,
99+
32,
100+
0xDDF252AD1BE2C89B69C2B068FC378DAA952BA7F163C4A11628F55A4DF523B3EF,
101+
0x000000000000000000000000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA,
102+
0x000000000000000000000000BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB,
103+
)
104+
105+
requests = Requests()
106+
107+
if include_deposit_event:
108+
bytecode += Om.MSTORE(DEFAULT_REQUEST_LOG) + Op.LOG1(
109+
0,
110+
len(DEFAULT_REQUEST_LOG),
111+
Spec.DEPOSIT_EVENT_SIGNATURE_HASH,
112+
)
113+
requests = Requests(DEFAULT_DEPOSIT_REQUEST)
114+
bytecode += Op.STOP
115+
116+
pre[Spec.DEPOSIT_CONTRACT_ADDRESS] = Account(
117+
code=bytecode,
118+
nonce=1,
119+
balance=0,
120+
)
121+
sender = pre.fund_eoa()
122+
123+
tx = Transaction(
124+
to=Spec.DEPOSIT_CONTRACT_ADDRESS,
125+
sender=sender,
126+
gas_limit=100_000,
127+
)
128+
129+
blockchain_test(
130+
pre=pre,
131+
blocks=[
132+
Block(
133+
txs=[tx],
134+
header_verify=Header(
135+
requests_hash=requests,
136+
),
137+
),
138+
],
139+
post={},
140+
)
141+
142+
143+
@pytest.mark.parametrize(
144+
"log_argument,value",
145+
[(log_argument, value) for log_argument in EVENT_ARGUMENTS for value in EVENT_ARGUMENT_VALUES],
146+
)
147+
@pytest.mark.exception_test
148+
def test_invalid_layout(
149+
blockchain_test: BlockchainTestFiller, pre: Alloc, log_argument: str, value: str
150+
):
151+
"""Test deposit contract emitting logs with invalid layouts (sizes/offsets)."""
152+
log_params = {**DEFAULT_DEPOSIT_REQUEST_LOG_DATA_DICT}
153+
log_params[log_argument] = 0 if value == "zero" else 2**256 - 1 # type: ignore
154+
155+
deposit_request_log = create_deposit_log_bytes(**log_params) # type: ignore
156+
157+
bytecode = Om.MSTORE(deposit_request_log) + Op.LOG1(
158+
0,
159+
len(deposit_request_log),
160+
Spec.DEPOSIT_EVENT_SIGNATURE_HASH,
161+
)
162+
bytecode += Op.STOP
163+
164+
pre[Spec.DEPOSIT_CONTRACT_ADDRESS] = Account(
165+
code=bytecode,
166+
nonce=1,
167+
balance=0,
168+
)
169+
sender = pre.fund_eoa()
170+
171+
tx = Transaction(
172+
to=Spec.DEPOSIT_CONTRACT_ADDRESS,
173+
sender=sender,
174+
gas_limit=100_000,
175+
error=TransactionException.INVALID_DEPOSIT_EVENT_LAYOUT,
176+
)
177+
178+
blockchain_test(
179+
pre=pre,
180+
blocks=[
181+
Block(txs=[tx], exception=TransactionException.INVALID_DEPOSIT_EVENT_LAYOUT),
182+
],
183+
post={},
184+
)
185+
186+
187+
@pytest.mark.parametrize(
188+
"slice_bytes",
189+
[
190+
pytest.param(True),
191+
pytest.param(False),
192+
],
193+
)
194+
@pytest.mark.exception_test
195+
def test_invalid_log_length(blockchain_test: BlockchainTestFiller, pre: Alloc, slice_bytes: bool):
196+
"""Test deposit contract emitting logs with invalid log length (one byte more or less)."""
197+
changed_log = DEFAULT_REQUEST_LOG[:-1] if slice_bytes else DEFAULT_REQUEST_LOG + b"\x00"
198+
199+
bytecode = Om.MSTORE(changed_log) + Op.LOG1(
200+
0,
201+
len(changed_log),
202+
Spec.DEPOSIT_EVENT_SIGNATURE_HASH,
203+
)
204+
bytecode += Op.STOP
205+
206+
pre[Spec.DEPOSIT_CONTRACT_ADDRESS] = Account(
207+
code=bytecode,
208+
nonce=1,
209+
balance=0,
210+
)
211+
sender = pre.fund_eoa()
212+
213+
tx = Transaction(
214+
to=Spec.DEPOSIT_CONTRACT_ADDRESS,
215+
sender=sender,
216+
gas_limit=100_000,
217+
error=TransactionException.INVALID_DEPOSIT_EVENT_LAYOUT,
218+
)
219+
220+
blockchain_test(
221+
pre=pre,
222+
blocks=[
223+
Block(txs=[tx], exception=TransactionException.INVALID_DEPOSIT_EVENT_LAYOUT),
224+
],
225+
post={},
226+
)

0 commit comments

Comments
 (0)