Skip to content

Commit

Permalink
add testnet benchmark scripts
Browse files Browse the repository at this point in the history
temp

testnet command

changelog
  • Loading branch information
yihuang committed Oct 28, 2024
1 parent 7dd3362 commit 26ae49e
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 36 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## UNRELEASED

### Improvements

* [#]() Add testnet benchmark command.

*Oct 24, 2024*

## v1.4.0-rc2
Expand Down
18 changes: 18 additions & 0 deletions testground/benchmark/benchmark/cosmostx.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,23 @@ class TxRaw(ProtoEntity):


class MsgEthereumTx(ProtoEntity):
MSG_URL = "/ethermint.evm.v1.MsgEthereumTx"

data = Field(ProtoAny, 1)
deprecated_hash = Field("string", 3)
from_ = Field("bytes", 5)
raw = Field("bytes", 6)


class LegacyTx(ProtoEntity):
MSG_URL = "/ethermint.evm.v1.LegacyTx"

nonce = Field("uint64", 1)
gas_price = Field("string", 2)
gas = Field("uint64", 3)
to = Field("string", 4)
value = Field("string", 5)
data = Field("bytes", 6)
v = Field("bytes", 7)
r = Field("bytes", 8)
s = Field("bytes", 9)
121 changes: 121 additions & 0 deletions testground/benchmark/benchmark/testnet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import asyncio
import json
import sys
import time
from pathlib import Path

import click
import requests
import web3
from hexbytes import HexBytes

from .transaction import EthTx, build_cosmos_tx, gen, json_rpc_send_body, send
from .utils import gen_account, split_batch

# arbitrarily picked for testnet, to not conflict with devnet benchmark accounts.
GLOBAL_SEQ = 999
GAS_PRICE = 5050000000000
CHAIN_ID = 338
TESTNET_JSONRPC = "https://evm-t3.cronos.org"
TESTNET_RPC = "https://rpc-t3.cronos.org"
TESTNET_EVM_DENOM = "basetcro"


@click.group()
def cli():
pass


@cli.command()
@click.option("--json-rpc", default=TESTNET_JSONRPC)
@click.option("--rpc", default=TESTNET_RPC)
@click.option("--batch-size", default=200)
@click.argument("start", type=int)
@click.argument("end", type=int)
def fund(json_rpc, rpc, batch_size, start, end):
w3 = web3.Web3(web3.HTTPProvider(json_rpc))
fund_account = gen_account(GLOBAL_SEQ, 0)
fund_address = HexBytes(fund_account.address)
nonce = w3.eth.get_transaction_count(fund_account.address)

batches = split_batch(end - start + 1, batch_size)
for begin, end in batches:
begin += start
end += start
txs = []
for i in range(begin, end):
tx = {
"to": gen_account(GLOBAL_SEQ, i).address,
"value": 10 * 10**18,
"nonce": nonce,
"gas": 21000,
"gasPrice": GAS_PRICE,
"chainId": CHAIN_ID,
}
txs.append(
EthTx(
tx, fund_account.sign_transaction(tx).rawTransaction, fund_address
)
)
nonce += 1
raw = build_cosmos_tx(*txs, msg_version="1.3", evm_denom=TESTNET_EVM_DENOM)
rsp = requests.post(
rpc, json=json_rpc_send_body(raw, method="broadcast_tx_sync")
).json()
if rsp["result"]["code"] != 0:
print(rsp["result"]["log"])
break

# wait for nonce to change
while True:
if w3.eth.get_transaction_count(fund_account.address) >= nonce:
break
time.sleep(1)

print("sent", begin, end)


@cli.command()
@click.option("--json-rpc", default=TESTNET_JSONRPC)
@click.argument("start", type=int)
@click.argument("end", type=int)
def check(json_rpc, start, end):
w3 = web3.Web3(web3.HTTPProvider(json_rpc))
for i in range(start, end + 1):
addr = gen_account(GLOBAL_SEQ, i).address
nonce = w3.eth.get_transaction_count(addr)
balance = int(w3.eth.get_balance(addr))
print(i, addr, nonce, balance)


@cli.command()
@click.argument("start", type=int)
@click.argument("end", type=int)
@click.option("--num-txs", default=1)
@click.option("--nonce", default=0)
@click.option("--msg-version", default="1.3")
def gen_txs(start, end, num_txs, nonce, msg_version):
num_accounts = end - start + 1
txs = gen(
GLOBAL_SEQ,
num_accounts,
num_txs,
"simple-transfer",
1,
start_account=start,
nonce=nonce,
msg_version=msg_version,
)
json.dump(txs, sys.stdout)


@cli.command()
@click.argument("path", type=str)
@click.option("--rpc", default=TESTNET_RPC)
def send_txs(path, rpc):
txs = json.loads(Path(path).read_text())
asyncio.run(send(txs, rpc))


if __name__ == "__main__":
cli()
134 changes: 98 additions & 36 deletions testground/benchmark/benchmark/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import backoff
import eth_abi
import ujson
from eth_account._utils.legacy_transactions import Transaction
from hexbytes import HexBytes

from . import cosmostx
Expand All @@ -21,6 +22,21 @@
CONNECTION_POOL_SIZE = 1024
TXS_DIR = "txs"

Job = namedtuple(
"Job",
[
"chunk",
"global_seq",
"num_txs",
"tx_type",
"create_tx",
"batch",
"nonce",
"msg_version",
],
)
EthTx = namedtuple("EthTx", ["tx", "raw", "sender"])


def simple_transfer_tx(sender: str, nonce: int):
return {
Expand Down Expand Up @@ -53,11 +69,48 @@ def erc20_transfer_tx(sender: str, nonce: int):
}


Job = namedtuple(
"Job",
["chunk", "global_seq", "num_accounts", "num_txs", "tx_type", "create_tx", "batch"],
)
EthTx = namedtuple("EthTx", ["tx", "raw", "sender"])
def build_evm_msg_1_3(tx: EthTx):
"""
build cronos v1.3 version of MsgEthereumTx
"""
txn = Transaction.from_bytes(tx.raw)
return cosmostx.build_any(
cosmostx.MsgEthereumTx.MSG_URL,
cosmostx.MsgEthereumTx(
data=cosmostx.build_any(
cosmostx.LegacyTx.MSG_URL,
cosmostx.LegacyTx(
nonce=txn.nonce,
gas_price=str(txn.gasPrice),
gas=txn.gas,
to=txn.to.hex(),
value=str(txn.value),
data=txn.data,
v=txn.v.to_bytes(32, byteorder="big"),
r=txn.r.to_bytes(32, byteorder="big"),
s=txn.s.to_bytes(32, byteorder="big"),
),
),
deprecated_hash=txn.hash().hex(),
from_=tx.sender,
),
)


def build_evm_msg_1_4(tx: EthTx):
return cosmostx.build_any(
cosmostx.MsgEthereumTx.MSG_URL,
cosmostx.MsgEthereumTx(
from_=tx.sender,
raw=tx.raw,
),
)


MSG_VERSIONS = {
"1.3": build_evm_msg_1_3,
"1.4": build_evm_msg_1_4,
}


def _do_job(job: Job):
Expand All @@ -67,7 +120,7 @@ def _do_job(job: Job):
for acct in accounts:
txs = []
for i in range(job.num_txs):
tx = job.create_tx(acct.address, i)
tx = job.create_tx(acct.address, job.nonce + i)
raw = acct.sign_transaction(tx).rawTransaction
txs.append(EthTx(tx, raw, HexBytes(acct.address)))
total += 1
Expand All @@ -76,19 +129,37 @@ def _do_job(job: Job):

# to keep it simple, only build batch inside the account
txs = [
build_cosmos_tx(*txs[start:end])
build_cosmos_tx(*txs[start:end], msg_version=job.msg_version)
for start, end in split_batch(len(txs), job.batch)
]
acct_txs.append(txs)
return acct_txs


def gen(global_seq, num_accounts, num_txs, tx_type: str, batch: int) -> [str]:
def gen(
global_seq,
num_accounts,
num_txs,
tx_type: str,
batch: int,
nonce: int = 0,
start_account: int = 0,
msg_version: str = "1.4",
) -> [str]:
chunks = split(num_accounts, os.cpu_count())
create_tx = TX_TYPES[tx_type]
jobs = [
Job(chunk, global_seq, num_accounts, num_txs, tx_type, create_tx, batch)
for chunk in chunks
Job(
(start + start_account, end + start_account),
global_seq,
num_txs,
tx_type,
create_tx,
batch,
nonce,
msg_version,
)
for start, end in chunks
]

with multiprocessing.Pool() as pool:
Expand Down Expand Up @@ -119,20 +190,12 @@ def load(datadir: Path, global_seq: int) -> [str]:
return ujson.load(f)


def build_cosmos_tx(*txs: EthTx) -> str:
def build_cosmos_tx(*txs: EthTx, msg_version="1.4", evm_denom=DEFAULT_DENOM) -> str:
"""
return base64 encoded cosmos tx, support batch
"""
msgs = [
cosmostx.build_any(
"/ethermint.evm.v1.MsgEthereumTx",
cosmostx.MsgEthereumTx(
from_=tx.sender,
raw=tx.raw,
),
)
for tx in txs
]
build_msg = MSG_VERSIONS[msg_version]
msgs = [build_msg(tx) for tx in txs]
fee = sum(tx.tx["gas"] * tx.tx["gasPrice"] for tx in txs)
gas = sum(tx.tx["gas"] for tx in txs)
body = cosmostx.TxBody(
Expand All @@ -143,7 +206,7 @@ def build_cosmos_tx(*txs: EthTx) -> str:
)
auth_info = cosmostx.AuthInfo(
fee=cosmostx.Fee(
amount=[cosmostx.Coin(denom=DEFAULT_DENOM, amount=str(fee))],
amount=[cosmostx.Coin(denom=evm_denom, amount=str(fee))],
gas_limit=gas,
)
)
Expand All @@ -154,31 +217,30 @@ def build_cosmos_tx(*txs: EthTx) -> str:
).decode()


def json_rpc_send_body(raw, method="broadcast_tx_async"):
return {
"jsonrpc": "2.0",
"method": method,
"params": {"tx": raw},
"id": 1,
}


@backoff.on_predicate(backoff.expo, max_time=60, max_value=5)
@backoff.on_exception(backoff.expo, aiohttp.ClientError, max_time=60, max_value=5)
async def async_sendtx(session, raw):
async with session.post(
LOCAL_RPC,
json={
"jsonrpc": "2.0",
"method": "broadcast_tx_async",
"params": {
"tx": raw,
},
"id": 1,
},
) as rsp:
async def async_sendtx(session, raw, rpc):
async with session.post(rpc, json=json_rpc_send_body(raw)) as rsp:
data = await rsp.json()
if "error" in data:
print("send tx error, will retry,", data["error"])
return False
return True


async def send(txs):
async def send(txs, rpc=LOCAL_RPC):
connector = aiohttp.TCPConnector(limit=CONNECTION_POOL_SIZE)
async with aiohttp.ClientSession(
connector=connector, json_serialize=ujson.dumps
) as session:
tasks = [asyncio.ensure_future(async_sendtx(session, raw)) for raw in txs]
tasks = [asyncio.ensure_future(async_sendtx(session, raw, rpc)) for raw in txs]
await asyncio.gather(*tasks)
4 changes: 4 additions & 0 deletions testground/benchmark/flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
type = "app";
program = "${pkgs.benchmark-testcase}/bin/stateless-testcase";
};
testnet = {
type = "app";
program = "${pkgs.benchmark-testcase}/bin/testnet";
};
};
devShells.default = pkgs.mkShell {
buildInputs = [ pkgs.benchmark-testcase-env ];
Expand Down
1 change: 1 addition & 0 deletions testground/benchmark/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry.scripts]
stateless-testcase = "benchmark.stateless:cli"
testnet = "benchmark.testnet:cli"

[tool.black]
line-length = 88
Expand Down

0 comments on commit 26ae49e

Please sign in to comment.