Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Str-998: Implement different transaction types for the load testing. #658

Merged
merged 7 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading