Skip to content

Commit

Permalink
Str-998: Implement different transaction types for the load testing. (#…
Browse files Browse the repository at this point in the history
…658)

* Minor fixes.

* Add EthTransaction load job.

* Prettify: add structured logging and some comments/docstrings.

* Better logging.

* Fix lints and codespell.

* Disable basic_load test for now.

* Allow waiting for confirmation of SC calls. Also, formatting of solidity code.
  • Loading branch information
evgenyzdanovich authored Feb 10, 2025
1 parent 1e2b41a commit ed8d602
Show file tree
Hide file tree
Showing 19 changed files with 869 additions and 123 deletions.
6 changes: 4 additions & 2 deletions functional-tests/entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from utils import *
from utils.constants import *
from load.cfg import RethLoadConfigBuilder
from load.jobs import EthJob
from load.reth import BasicRethBlockJob, BasicRethTxJob

TEST_DIR: str = "tests"

Expand Down Expand Up @@ -103,7 +103,9 @@ def main(argv):
}

reth_load_env = testenv.LoadEnvConfig()
reth_load_env.with_load_builder(RethLoadConfigBuilder().with_jobs([EthJob]).with_rate(15))
reth_load_env.with_load_builder(
RethLoadConfigBuilder().with_jobs([BasicRethBlockJob, BasicRethTxJob]).with_rate(30)
)

global_envs = {
# Basic env is the default env for all tests.
Expand Down
2 changes: 1 addition & 1 deletion functional-tests/envs/testenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ def init(self, ctx: flexitest.EnvContext) -> flexitest.LiveEnv:

# TODO: Maybe, we need to make it dynamic to enhance any EnvConfig with load testing capabilities.
class LoadEnvConfig(BasicEnvConfig):
_load_cfgs: list[Callable[[dict[str, flexitest.Service]], LoadConfig]] = []
_load_cfgs: list[LoadConfigBuilder] = []

def with_load_builder(self, builder: LoadConfigBuilder):
self._load_cfgs.append(builder)
Expand Down
3 changes: 1 addition & 2 deletions functional-tests/factory/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,11 +414,10 @@ def create_simple_loadgen(

datadir = ctx.make_service_dir(name)
rpc_port = self.next_port()
logfile = os.path.join(datadir, "service.log")

rpc_url = f"ws://localhost:{rpc_port}"

svc = LoadGeneratorService(logfile, load_cfg)
svc = LoadGeneratorService(datadir, load_cfg)
svc.start()
_inject_service_create_rpc(svc, rpc_url, name)
return svc
Expand Down
4 changes: 0 additions & 4 deletions functional-tests/load/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,6 @@ def __call__(self, svcs) -> LoadConfig:
raise Exception("LoadConfigBuilder: load jobs list is empty")

host = self.host_url(svcs)
# Patch jobs by the host.
for job in self.jobs:
job.host = host

return LoadConfig(self.jobs, host, self.spawn_rate)

def host_url(self, _svcs: dict[str, flexitest.Service]) -> str:
Expand Down
71 changes: 23 additions & 48 deletions functional-tests/load/job.py
Original file line number Diff line number Diff line change
@@ -1,65 +1,40 @@
import web3
import web3.middleware
from locust import HttpUser

from utils import setup_load_job_logger


class StrataLoadJob(HttpUser):
"""
A common layer for all the load jobs in the load tests.
"""

pass
def on_start(self):
super().on_start()

# Setup a separate logger with its own file for each load job.
self._logger = setup_load_job_logger(self.environment._datadir, type(self).__name__)

# TODO(load): configure the structured logging as we do in the tests.
class BaseRethLoadJob(StrataLoadJob):
fund_amount: int = 1_000_000_000_000_000_000_000 # 1000 ETH
# Technically, before_start and after_start can be merged.
# It's done to separate initialization logic (aka constructor) from "run-it-once" logic.
# Also, with that in mind, the "on_start" is a bit misleading.
self._logger.info("Before start:")
self.before_start()
self._logger.info("Before start completed.")

def on_start(self):
root_w3, genesis_acc = self.w3_with_genesis_acc()
self._root_w3 = root_w3
self._genesis_acc = genesis_acc
self._logger.info("After start:")
self.after_start()
self._logger.info("After start completed.")

def w3_with_genesis_acc(self):
def before_start(self):
"""
Return w3 with prefunded "root" account as specified in the chain config.
Called right before a job starts running.
A good place for the subclass to initialize the state.
"""
return self._init_w3(
lambda w3: w3.eth.account.from_key(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
)
)
pass

def w3_with_new_acc(self):
def after_start(self):
"""
Return w3 with a fresh account.
Also, funds this account, so it's able to sign and send some txns.
Called right before a job starts running, but after `before_start`.
A good place for the subclass to perform some actions once (before the job actually starts).
"""
w3, new_acc = self._init_w3(lambda w3: w3.eth.account.create())
self._fund_account(new_acc.address)

return w3, new_acc

def _init_w3(self, init):
# Reuse the http session by locust internals, so the stats are measured correctly.
w3 = web3.Web3(web3.Web3.HTTPProvider(self.host, session=self.client))
# Init the account according to lambda
account = init(w3)
# Set the account onto web3 and init the signing middleware.
w3.address = account.address
w3.middleware_onion.add(web3.middleware.SignAndSendRawMiddlewareBuilder.build(account))

return w3, account

def _fund_account(self, acc):
print(f"FUNDING ACCOUNT {acc}")
source = self._root_w3.address
tx_hash = self._root_w3.eth.send_transaction(
{"to": acc, "value": hex(self.fund_amount), "gas": hex(100000), "from": source}
)

tx_receipt = self._root_w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
print(f"FUNDING SUCCESS: {tx_receipt}")

def _balance(self, acc):
return self._root_w3.eth.get_balance(acc)
pass
1 change: 0 additions & 1 deletion functional-tests/load/jobs/__init__.py

This file was deleted.

53 changes: 0 additions & 53 deletions functional-tests/load/jobs/reth.py

This file was deleted.

1 change: 1 addition & 0 deletions functional-tests/load/reth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .jobs import *
120 changes: 120 additions & 0 deletions functional-tests/load/reth/account.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import web3
from gevent.lock import Semaphore
from web3.middleware.signing import SignAndSendRawMiddlewareBuilder

from load.job import StrataLoadJob


class AbstractAccount:
"""
Abstract Ethereum-like account on RETH in fntests.
"""

_nonce: int = 0
"""
Nonce of the account w3 is initialized with.
"""

_nonce_lock = Semaphore()
"""
Gevent synchronization primitive on the nonce.
The reason is twofold:
- to avoid fetching the current nonce before each transaction.
- to avoid races on the nonce when different green threads use the same account.
"""

@property
def w3(self) -> web3.Web3:
raise NotImplementedError("w3 should be implemented by subclasses")

@property
def account(self):
raise NotImplementedError("account should be implemented by subclasses")

@property
def nonce(self):
with self._nonce_lock:
nonce = self._nonce
self._nonce += 1
return nonce

@property
def address(self):
return self.account.address

@property
def balance(self):
return self.w3.eth.account.get_balance(self.address)


class GenesisAccount:
"""
Prefunded account according to the genesis config of RETH in fntests.
"""

nonce: int = 0
nonce_lock = Semaphore()

def __init__(self, job: StrataLoadJob):
w3 = web3.Web3(web3.Web3.HTTPProvider(job.host, session=job.client))
# Init the prefunded account as specified in the chain config.
account = w3.eth.account.from_key(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
)
# Set the account onto web3 and init the signing middleware.
w3.address = account.address
w3.middleware_onion.add(SignAndSendRawMiddlewareBuilder.build(account))
self._w3 = w3
self._account = account

def fund_address(self, account_address, amount) -> bool:
# We use class attribute here (rather than object attribute) to have
# the same nonce lock even if multiple instances of GenesisAccount are used.
nonce = GenesisAccount._inc_nonce()
tx_hash = self._w3.eth.send_transaction(
{
"to": account_address,
"value": hex(amount),
"gas": hex(100000),
"from": self._account.address,
"nonce": nonce,
}
)

# Block on this transaction to make sure funding is successful before proceeding further.
tx_receipt = self._w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
return tx_receipt["status"] == 1

@classmethod
def _inc_nonce(cls):
with cls.nonce_lock:
nonce = cls.nonce
cls.nonce += 1
return nonce


class FundedAccount(AbstractAccount):
"""
Fresh Ethereum-like account with no funds.
"""

def __init__(self, job: StrataLoadJob):
w3 = web3.Web3(web3.Web3.HTTPProvider(job.host, session=job.client))
# Init the new account.
account = w3.eth.account.create()
# Set the account onto web3 and init the signing middleware.
w3.address = account.address
w3.middleware_onion.add(SignAndSendRawMiddlewareBuilder.build(account))
self._w3 = w3
self._account = account

def fund_me(self, genesis_acc: GenesisAccount, amount=1_000_000_000_000_000_000_000):
genesis_acc.fund_address(self.address, amount)

@property
def w3(self):
return self._w3

@property
def account(self):
return self._account
24 changes: 24 additions & 0 deletions functional-tests/load/reth/contracts/Counter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Counter {
uint256 private count;

event CounterIncremented(uint256 newValue);
event CounterDecremented(uint256 newValue);

function increment() public {
count += 1;
emit CounterIncremented(count);
}

function decrement() public {
require(count > 0, "Counter cannot be negative");
count -= 1;
emit CounterDecremented(count);
}

function getCount() public view returns (uint256) {
return count;
}
}
Loading

0 comments on commit ed8d602

Please sign in to comment.