Skip to content

Commit 9d65c48

Browse files
committed
feat: start to implement blob sender plugin
1 parent 211f052 commit 9d65c48

File tree

8 files changed

+443
-3
lines changed

8 files changed

+443
-3
lines changed

packages/testing/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ dependencies = [
5252
"ckzg>=2.1.3,<3",
5353
"tenacity>=9.0.0,<10",
5454
"Jinja2>=3,<4",
55+
"eth-account>=0.13.7",
56+
"eth-utils>=5.3.1",
57+
"eth-keys>=0.7.0",
5558
]
5659

5760
[project.urls]

packages/testing/src/execution_testing/cli/pytest_commands/execute.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,18 @@ def command(pytest_args: List[str], **_kwargs: Any) -> None:
9595
Path("cli/pytest_commands/plugins/execute/execute_recover.py")
9696
],
9797
)
98+
99+
blob_sender = _create_execute_subcommand(
100+
"blob-sender",
101+
"pytest-execute-blob-sender.ini",
102+
"Send blobs to a remote RPC endpoint.",
103+
required_args=[
104+
"--rpc-endpoint=http://localhost:8545",
105+
"--rpc-seed-key=1",
106+
"--chain-id=1",
107+
"--fork=Cancun",
108+
],
109+
command_logic_test_paths=[
110+
Path("cli/pytest_commands/plugins/execute/blob_sender/blob_sender.py")
111+
],
112+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Execute module to send blobs."""
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""Blob sender plugin."""
2+
3+
import pytest
4+
from eth_account import Account
5+
from eth_keys import keys
6+
from eth_utils import to_checksum_address
7+
8+
from execution_testing.base_types.base_types import Address, Bytes
9+
from execution_testing.forks import ForkSetAdapter
10+
from execution_testing.logging import (
11+
get_logger,
12+
)
13+
from execution_testing.rpc import EthRPC
14+
from execution_testing.test_types.blob_types import Blob
15+
16+
logger = get_logger(__name__)
17+
18+
19+
def pytest_addoption(parser: pytest.Parser) -> None:
20+
"""Add command-line options to pytest."""
21+
blob_sender_group = parser.getgroup(
22+
"blob_sender", "Arguments defining blob_sender behavior"
23+
)
24+
blob_sender_group.addoption(
25+
"--blob-seed",
26+
action="store",
27+
dest="blob_seed",
28+
required=False,
29+
type=int,
30+
default=1,
31+
help=(
32+
"Blob data is dynamically derived from this seed.\nNote: "
33+
"This is the starting seed. If you send more than one blob, each "
34+
"additional blob will have its seed increased by 1.\nMax value: 6"
35+
),
36+
)
37+
blob_sender_group.addoption(
38+
"--blob-amount",
39+
action="store",
40+
dest="blob_amount",
41+
required=False,
42+
type=int,
43+
default=1,
44+
help=("Amount of blobs to generate and send"),
45+
)
46+
47+
48+
# --------------------------- Helper functions --------------------------------
49+
50+
51+
def hex_to_bytes(s: str) -> bytes:
52+
"""Takes hex string and returns it as bytes."""
53+
s = s[2:] if s[:2].lower() == "0x" else s
54+
if len(s) % 2:
55+
s = "0" + s
56+
return bytes.fromhex(s)
57+
58+
59+
def privkey_hex_to_addr(*, privkey_hex: str) -> str:
60+
"""Takes private key hex string and returns derived checksum address."""
61+
# convert hex to bytes
62+
privkey_bytes = hex_to_bytes(privkey_hex)
63+
64+
# derive pubkey
65+
pk = keys.PrivateKey(privkey_bytes)
66+
67+
# derive address
68+
addr = pk.public_key.to_checksum_address()
69+
return addr
70+
71+
72+
def gwei_float_to_wei_int(gwei: float) -> int:
73+
"""Convert gwei float to wei int."""
74+
return int(gwei * (10**9))
75+
76+
77+
# -----------------------------------------------------------------------------
78+
79+
80+
@pytest.hookimpl(tryfirst=True)
81+
def pytest_configure(config: pytest.Config) -> None:
82+
"""
83+
Set the provided command-line arguments.
84+
"""
85+
# skip validation if we're just showing help
86+
if config.option.help:
87+
return
88+
89+
blob_seed = config.getoption("blob_seed")
90+
91+
blob_amount = config.getoption("blob_amount")
92+
assert blob_amount <= 6, (
93+
"you may only send up to 6 blobs per tx, but you tried to "
94+
f"send {blob_amount} blobs in one tx!"
95+
)
96+
97+
fork_str = config.getoption("single_fork")
98+
chain_id = config.getoption("chain_id")
99+
rpc_endpoint = config.getoption("rpc_endpoint")
100+
101+
sender_privkey_hex = config.getoption("rpc_seed_key")
102+
sender_address = privkey_hex_to_addr(privkey_hex=sender_privkey_hex)
103+
104+
if not fork_str:
105+
pytest.exit(
106+
"ERROR: --fork is required for blob-sender command.\n"
107+
"Example Usage: uv run execute blob-sender -v -s --fork=Osaka --rpc-seed-key=0000000000000000000000000000000000000000000000000000000000000001 --rpc-endpoint=http://example.org --chain-id=11155111 --eest-log-level=INFO --blob-seed=5 --blob-amount=3", # noqa: E501
108+
returncode=pytest.ExitCode.USAGE_ERROR,
109+
)
110+
111+
# Convert fork string to Fork instance
112+
fork_set = ForkSetAdapter.validate_python(fork_str)
113+
fork = next(iter(fork_set))
114+
115+
# get sender nonce on target network
116+
eth_rpc = EthRPC(rpc_endpoint)
117+
nonce = eth_rpc.get_transaction_count(Address(sender_address))
118+
119+
logger.info(
120+
"\nBlob Sender Plugin Configuration:"
121+
f"\n\tFork: {fork}"
122+
f"\n\tAmount of blobs to send: {blob_amount}"
123+
f"\n\tStarting seed for blob generation: {blob_seed}"
124+
f"\n\tSender Address: {sender_address}"
125+
f"\n\tSender Nonce: {nonce}"
126+
f"\n\tChain ID: {chain_id}"
127+
)
128+
129+
versioned_hashes: list[Bytes] = []
130+
blob_list: list[Bytes] = []
131+
for current_seed in range(blob_seed, blob_seed + blob_amount):
132+
print(f"Generating blob with seed {current_seed} for fork {fork}..")
133+
b = Blob.from_fork(fork, seed=current_seed)
134+
# blobs.append(b)
135+
print("Successfully generated blob file:", b.name)
136+
137+
# extract relevant info from blob
138+
data_hex = b.data.hex()
139+
if data_hex.startswith("0x"):
140+
data_hex = data_hex[2:]
141+
142+
assert len(data_hex) == 131072 * 2, (
143+
f"Data should be 128KB, got {len(data_hex)} bytes"
144+
)
145+
146+
blob_bytes = Bytes(data_hex)
147+
blob_list.append(blob_bytes)
148+
149+
versioned_hashes.append(Bytes(b.versioned_hash))
150+
151+
# define type 3 tx
152+
max_priority_fee_per_gas = 2 # gwei
153+
max_fee_per_gas = 3.7 # gwei
154+
max_fee_per_blob_gas = 6 # gwei
155+
156+
tx_dict = {
157+
"type": 3, # EIP-4844 blob transaction
158+
"chainId": chain_id,
159+
"nonce": nonce,
160+
"from": to_checksum_address(sender_address),
161+
"to": to_checksum_address(
162+
sender_address
163+
), # just send it to yourself, on mainnet some L2's put "FF000....00<decimal-chainid>" as address # noqa: E501
164+
"value": 0,
165+
"gas": 21_000,
166+
"maxPriorityFeePerGas": gwei_float_to_wei_int(
167+
max_priority_fee_per_gas
168+
),
169+
"maxFeePerGas": gwei_float_to_wei_int(max_fee_per_gas),
170+
"maxFeePerBlobGas": gwei_float_to_wei_int(max_fee_per_blob_gas),
171+
"data": "0x",
172+
"accessList": [],
173+
"blobVersionedHashes": versioned_hashes,
174+
}
175+
176+
signed_tx_obj = Account.sign_transaction(
177+
tx_dict, sender_privkey_hex, blobs=blob_list
178+
)
179+
# signed_raw_tx_hex = "0x" + signed_tx_obj.raw_transaction.hex()
180+
# logger.info(
181+
# "done. you can send this now via "
182+
# f"eth_sendRawTransaction: {signed_raw_tx_hex}"
183+
# )
184+
185+
# send raw tx
186+
raw_tx = Bytes(signed_tx_obj.raw_transaction)
187+
tx_hash = eth_rpc.send_raw_transaction(raw_tx)
188+
logger.info(f"\nSuccess!\nTx Hash: {tx_hash}")

packages/testing/src/execution_testing/cli/pytest_commands/plugins/help/help.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ def pytest_addoption(parser: pytest.Parser) -> None:
6363
default=False,
6464
help="Show help options specific to the execute's command eth_config and exit.",
6565
)
66+
help_group.addoption(
67+
"--execute-blob-sender-help",
68+
action="store_true",
69+
dest="show_execute_blob_sender_help",
70+
default=False,
71+
help="Show help options for the execute blob-sender command and exit.",
72+
)
6673

6774

6875
@pytest.hookimpl(tryfirst=True)
@@ -113,6 +120,7 @@ def pytest_configure(config: pytest.Config) -> None:
113120
"sender key fixtures",
114121
"remote seed sender",
115122
"chain configuration",
123+
"blob sender",
116124
],
117125
)
118126
elif config.getoption("show_execute_hive_help"):
@@ -148,6 +156,20 @@ def pytest_configure(config: pytest.Config) -> None:
148156
"chain configuration",
149157
],
150158
)
159+
elif config.getoption("show_execute_blob_sender_help"):
160+
show_specific_help(
161+
config,
162+
"pytest-execute-blob-sender.ini",
163+
[
164+
"blob_sender",
165+
"fork range",
166+
"remote RPC configuration",
167+
"pre-allocation behavior during test execution",
168+
"sender key fixtures",
169+
"remote seed sender",
170+
"chain configuration",
171+
],
172+
)
151173

152174

153175
def show_specific_help(
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[pytest]
2+
console_output_style = count
3+
minversion = 7.0
4+
python_files = test_*.py
5+
testpaths = tests/
6+
addopts =
7+
-p execution_testing.cli.pytest_commands.plugins.execute.blob_sender.blob_sender
8+
-p execution_testing.cli.pytest_commands.plugins.execute.execute_flags.execute_flags
9+
-p execution_testing.cli.pytest_commands.plugins.execute.sender
10+
-p execution_testing.cli.pytest_commands.plugins.execute.execute
11+
-p execution_testing.cli.pytest_commands.plugins.shared.transaction_fixtures
12+
-p execution_testing.cli.pytest_commands.plugins.execute.rpc.remote_seed_sender
13+
-p execution_testing.cli.pytest_commands.plugins.execute.rpc.remote
14+
-p execution_testing.cli.pytest_commands.plugins.forks.forks
15+
-p execution_testing.cli.pytest_commands.plugins.help.help
16+
-p execution_testing.cli.pytest_commands.plugins.custom_logging.plugin_logging
17+
--tb short
18+
--dist loadscope
19+
--ignore tests/cancun/eip4844_blobs/point_evaluation_vectors/
20+
--ignore tests/json_infra
21+
--ignore tests/evm_tools

0 commit comments

Comments
 (0)