diff --git a/.cicd/defaults.json b/.cicd/defaults.json
index ea2c077..ce19d86 100644
--- a/.cicd/defaults.json
+++ b/.cicd/defaults.json
@@ -1,11 +1,11 @@
{
"leap-dev":{
- "target":"4",
- "prerelease":false
+ "target":"5",
+ "prerelease":true
},
"leap":{
- "target":"4",
- "prerelease":false
+ "target":"5",
+ "prerelease":true
},
"cdt":{
"target":"3.1.0",
diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml
index ea9f89c..7fb8749 100644
--- a/.github/workflows/node.yml
+++ b/.github/workflows/node.yml
@@ -110,45 +110,6 @@ jobs:
push: true
tags: ${{fromJSON(needs.d.outputs.p)[matrix.platform].image}}
file: ${{fromJSON(needs.d.outputs.p)[matrix.platform].dockerfile}}
-
- build:
- name: EOS EVM Node Build
- needs: [d, build-platforms]
- if: always() && needs.d.result == 'success' && (needs.build-platforms.result == 'success' || needs.build-platforms.result == 'skipped')
- strategy:
- fail-fast: false
- matrix:
- platform: [ ubuntu22 ]
- runs-on: ubuntu-latest
- container: ${{fromJSON(needs.d.outputs.p)[matrix.platform].image}}
-
- steps:
- - name: Authenticate
- id: auth
- uses: AntelopeIO/github-app-token-action@v1
- with:
- app_id: ${{ secrets.TRUSTEVM_CI_APP_ID }}
- private_key: ${{ secrets.TRUSTEVM_CI_APP_KEY }}
-
- - name: Checkout Repo
- uses: actions/checkout@v3
- with:
- fetch-depth: 0
- submodules: 'recursive'
- token: ${{ steps.auth.outputs.token }}
-
- - name: Build EOS EVM Node
- run: .github/workflows/build-node.sh
- env:
- CC: gcc-11
- CXX: g++-11
-
- - name: Upload Artifacts
- uses: actions/upload-artifact@v3
- with:
- name: build.tar.gz
- path: build.tar.gz
-
versions:
name: Determine Versions
runs-on: ubuntu-latest
@@ -206,6 +167,85 @@ jobs:
if [[ "${{inputs.override-eos-evm-miner}}" != "" ]]; then
echo eos-evm-miner-target=${{inputs.override-eos-evm-miner}} >> $GITHUB_OUTPUT
fi
+
+ build:
+ name: EOS EVM Node Build
+ needs: [d, build-platforms, versions]
+ if: always() && needs.d.result == 'success' && (needs.build-platforms.result == 'success' || needs.build-platforms.result == 'skipped')
+ strategy:
+ fail-fast: false
+ matrix:
+ platform: [ ubuntu22 ]
+ runs-on: ubuntu-latest
+ container: ${{fromJSON(needs.d.outputs.p)[matrix.platform].image}}
+
+ steps:
+ - name: Update Package Index & Upgrade Packages
+ run: |
+ apt-get update
+ apt-get upgrade -y
+ apt update
+ apt upgrade -y
+
+ - name: Download leap-dev binary
+ uses: AntelopeIO/asset-artifact-download-action@v3
+ with:
+ owner: AntelopeIO
+ repo: leap
+ target: '${{needs.versions.outputs.leap-dev-target}}'
+ prereleases: ${{fromJSON(needs.versions.outputs.leap-dev-prerelease)}}
+ file: 'leap-dev.*${{matrix.platform}}.*(x86_64|amd64).deb'
+ container-package: experimental-binaries
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - if: needs.versions.outputs.leap-target != 'DEFAULT' && (startsWith(needs.versions.outputs.leap-target, 3) || startsWith(needs.versions.outputs.leap-target, 4))
+ name: Download leap binary
+ uses: AntelopeIO/asset-artifact-download-action@v3
+ with:
+ owner: AntelopeIO
+ repo: leap
+ target: '${{needs.versions.outputs.leap-target}}'
+ prereleases: ${{fromJSON(needs.versions.outputs.leap-prerelease)}}
+ file: 'leap.*${{matrix.platform}}.*(x86_64|amd64).deb'
+ token: ${{ secrets.GITHUB_TOKEN }}
+ - if: needs.versions.outputs.leap-target == 'DEFAULT' || !(startsWith(needs.versions.outputs.leap-target, 3) && startsWith(needs.versions.outputs.leap-target, 4))
+ name: Download Prev Leap Version
+ uses: AntelopeIO/asset-artifact-download-action@v3
+ with:
+ owner: AntelopeIO
+ repo: leap
+ target: '${{needs.versions.outputs.leap-target}}'
+ prereleases: ${{fromJSON(needs.versions.outputs.leap-prerelease)}}
+ file: 'leap.*amd64.deb'
+
+ - name: Install Leap
+ run: |
+ apt-get install -y ./leap*.deb
+ - name: Authenticate
+ id: auth
+ uses: AntelopeIO/github-app-token-action@v1
+ with:
+ app_id: ${{ secrets.TRUSTEVM_CI_APP_ID }}
+ private_key: ${{ secrets.TRUSTEVM_CI_APP_KEY }}
+
+ - name: Checkout Repo
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ submodules: 'recursive'
+ token: ${{ steps.auth.outputs.token }}
+
+ - name: Build EOS EVM Node
+ run: .github/workflows/build-node.sh
+ env:
+ CC: gcc-11
+ CXX: g++-11
+
+ - name: Upload Artifacts
+ uses: actions/upload-artifact@v3
+ with:
+ name: build.tar.gz
+ path: build.tar.gz
integration-test:
name: EOS EVM Integration Tests
@@ -250,11 +290,12 @@ jobs:
repo: leap
target: '${{needs.versions.outputs.leap-dev-target}}'
prereleases: ${{fromJSON(needs.versions.outputs.leap-dev-prerelease)}}
- file: 'leap-dev.*(x86_64|amd64).deb'
+ file: 'leap-dev.*${{matrix.platform}}.*(x86_64|amd64).deb'
container-package: experimental-binaries
token: ${{ secrets.GITHUB_TOKEN }}
- - name: Download leap binary
+ - if: needs.versions.outputs.leap-target != 'DEFAULT' && (startsWith(needs.versions.outputs.leap-target, 3) || startsWith(needs.versions.outputs.leap-target, 4))
+ name: Download leap binary
uses: AntelopeIO/asset-artifact-download-action@v3
with:
owner: AntelopeIO
@@ -263,6 +304,15 @@ jobs:
prereleases: ${{fromJSON(needs.versions.outputs.leap-prerelease)}}
file: 'leap.*${{matrix.platform}}.*(x86_64|amd64).deb'
token: ${{ secrets.GITHUB_TOKEN }}
+ - if: needs.versions.outputs.leap-target == 'DEFAULT' || !(startsWith(needs.versions.outputs.leap-target, 3) && startsWith(needs.versions.outputs.leap-target, 4))
+ name: Download Prev Leap Version
+ uses: AntelopeIO/asset-artifact-download-action@v3
+ with:
+ owner: AntelopeIO
+ repo: leap
+ target: '${{needs.versions.outputs.leap-target}}'
+ prereleases: ${{fromJSON(needs.versions.outputs.leap-prerelease)}}
+ file: 'leap.*amd64.deb'
- name: Install Leap
run: |
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 04d2925..b277fec 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -1,4 +1,5 @@
add_subdirectory( nodeos_eos_evm_server )
+add_subdirectory( trx_generator )
configure_file(antelope_name.py . COPYONLY)
configure_file(nodeos_eos_evm_server.py . COPYONLY)
diff --git a/tests/trx_generator/CMakeLists.txt b/tests/trx_generator/CMakeLists.txt
new file mode 100644
index 0000000..750d423
--- /dev/null
+++ b/tests/trx_generator/CMakeLists.txt
@@ -0,0 +1,9 @@
+find_package(eosio)
+
+set(SILKWORM_LIBRARIES silkworm_core )
+
+add_executable(trx_generator main.cpp trx_generator.cpp trx_provider.cpp)
+
+target_include_directories(trx_generator PUBLIC ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR})
+
+target_link_libraries(trx_generator PRIVATE EosioChain Boost::program_options Boost::beast ${SILKWORM_LIBRARIES} ${CMAKE_DL_LIBS} ${PLATFORM_SPECIFIC_LIBS})
diff --git a/tests/trx_generator/README.md b/tests/trx_generator/README.md
new file mode 100644
index 0000000..f05cb15
--- /dev/null
+++ b/tests/trx_generator/README.md
@@ -0,0 +1,65 @@
+# Transaction Generator
+
+The Transaction Generator is a program built to create and send transactions at a specified rate in order to generate load on a blockchain. It is comprised of 3 main components: Transaction Generator, Transaction Provider, and Performance Monitor.
+
+The `trx_generator.[hpp, cpp]` is currently specialized to be a `transfer_trx_generator` primarily focused on generating token transfer transactions. The transactions are then provided to the network by the `trx_provider.[hpp, cpp]` which is currently aimed at the P2P network protocol in the `p2p_trx_provider`. The third component, the `tps_performance_monitor`, allows the Transaction Generator to monitor its own performance and take action to notify and exit if it is unable to keep up with the requested transaction generation rate.
+
+The Transaction Generator logs each transaction's id and sent timestamp at the moment the Transaction Provider sends the transaction. Logs are written to the configured log directory and will follow the naming convention `trx_data_output_10744.txt` where `10744` is the transaction generator instance's process ID.
+
+## Configuration Options
+`./build/tests/trx_generator/trx_generator` can be configured using the following command line arguments:
+
+
+ Expand Argument List
+
+* `--generator-id arg` (=0) Id for the transaction generator.
+ Allowed range (0-960). Defaults to 0.
+* `--chain-id arg` set the chain id
+* `--contract-owner-account arg` Account name of the contract account
+ for the transaction actions
+* `--accounts arg` comma-separated list of accounts that
+ will be used for transfers. Minimum
+ required accounts: 2.
+* `--priv-keys arg` comma-separated list of private keys in
+ same order of accounts list that will
+ be used to sign transactions. Minimum
+ required: 2.
+* `--trx-expiration arg` (=3600) transaction expiration time in seconds.
+ Defaults to 3,600. Maximum allowed:
+ 3,600
+* `--trx-gen-duration arg` (=60) Transaction generation duration
+ (seconds). Defaults to 60 seconds.
+* `--target-tps arg` (=1) Target transactions per second to
+ generate/send. Defaults to 1
+ transaction per second.
+* `--last-irreversible-block-id arg` Current last-irreversible-block-id (LIB
+ ID) to use for transactions.
+* `--monitor-spinup-time-us arg` (=1000000)
+ Number of microseconds to wait before
+ monitoring TPS. Defaults to 1000000
+ (1s).
+* `--monitor-max-lag-percent arg` (=5) Max percentage off from expected
+ transactions sent before being in
+ violation. Defaults to 5.
+* `--monitor-max-lag-duration-us arg` (=1000000)
+ Max microseconds that transaction
+ generation can be in violation before
+ quitting. Defaults to 1000000 (1s).
+* `--log-dir arg` set the logs directory
+* `--stop-on-trx-failed arg` (=1) stop transaction generation if sending
+ fails.
+* `--abi-file arg` The path to the contract abi file to
+ use for the supplied transaction action
+ data
+* `--actions-data arg` The json actions data file or json
+ actions data description string to use
+* `--actions-auths arg` The json actions auth file or json
+ actions auths description string to
+ use, containting authAcctName to
+ activePrivateKey pairs.
+* `--peer-endpoint arg` (=127.0.0.1) set the peer endpoint to send
+ transactions to
+* `--port arg` (=9876) set the peer endpoint port to send
+ transactions to
+* `-h [ --help ]` print this list
+
diff --git a/tests/trx_generator/http_client_async.hpp b/tests/trx_generator/http_client_async.hpp
new file mode 100644
index 0000000..7aad239
--- /dev/null
+++ b/tests/trx_generator/http_client_async.hpp
@@ -0,0 +1,167 @@
+#pragma once
+
+// The majority of the code here are derived from boost source
+// libs/beast/example/http/client/async/http_client_async.cpp
+// with minimum modification and yet reusable.
+//
+//
+// Copyright (c) 2016-2019 Vinnie Falco (vinnie dot falco at gmail dot com)
+//
+// Distributed under the Boost Software License, Version 1.0. (See accompanying
+// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
+//
+// Official repository: https://github.com/boostorg/beast
+//
+
+//------------------------------------------------------------------------------
+//
+// Example: HTTP client, asynchronous
+//
+//------------------------------------------------------------------------------
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+//------------------------------------------------------------------------------
+
+namespace eosio {
+namespace http_client_async {
+
+namespace beast = boost::beast; // from
+namespace http = beast::http; // from
+namespace net = boost::asio; // from
+using tcp = boost::asio::ip::tcp; // from
+
+using response_callback_t = std::function)>;
+
+namespace details {
+
+// Report a failure
+inline void fail(beast::error_code ec, char const* what) { std::cerr << what << ": " << ec.message() << "\n"; }
+
+// Performs an HTTP GET and prints the response
+class session : public std::enable_shared_from_this {
+ tcp::resolver resolver_;
+ beast::tcp_stream stream_;
+ beast::flat_buffer buffer_; // (Must persist between reads)
+ http::request req_;
+ http::response res_;
+ response_callback_t response_callback_;
+
+ public:
+ // Objects are constructed with a strand to
+ // ensure that handlers do not execute concurrently.
+ explicit session(net::io_context& ioc, const response_callback_t& response_callback)
+ : resolver_(net::make_strand(ioc))
+ , stream_(net::make_strand(ioc))
+ , response_callback_(response_callback) {}
+
+ // Start the asynchronous operation
+ void run(const std::string& host, const unsigned short port, const std::string& target, int version,
+ const std::string& content_type, std::string&& request_body) {
+ // Set up an HTTP GET request message
+ req_.version(version);
+ req_.method(http::verb::post);
+ req_.target(target);
+ req_.set(http::field::host, host);
+ req_.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING);
+ req_.set(http::field::content_type, content_type);
+ req_.body() = std::move(request_body);
+ // current implementation does not reuse socket, disable keep_alive
+ req_.set(http::field::connection, "close");
+ req_.keep_alive(false);
+ req_.prepare_payload();
+
+ // Look up the domain name
+ resolver_.async_resolve(
+ host, std::to_string(port), [self = this->shared_from_this()](beast::error_code ec, auto res) { self->on_resolve(ec, res); });
+ }
+
+ void on_resolve(beast::error_code ec, tcp::resolver::results_type results) {
+ if (ec) {
+ response_callback_(ec, {});
+ return fail(ec, "resolve");
+ }
+
+ // Set a timeout on the operation
+ stream_.expires_after(std::chrono::seconds(30));
+
+ // Make the connection on the IP address we get from a lookup
+ stream_.async_connect(results, [self = this->shared_from_this()](beast::error_code ec, auto endpt) {
+ self->on_connect(ec, endpt);
+ });
+ }
+
+ void on_connect(beast::error_code ec, tcp::resolver::results_type::endpoint_type) {
+ if (ec) {
+ response_callback_(ec, {});
+ return fail(ec, "connect");
+ }
+
+ // Set a timeout on the operation
+ stream_.expires_after(std::chrono::seconds(30));
+
+ // Send the HTTP request to the remote host
+ http::async_write(stream_, req_, [self = this->shared_from_this()](beast::error_code ec, auto bytes_transferred) {
+ self->on_write(ec, bytes_transferred);
+ });
+ }
+
+ void on_write(beast::error_code ec, std::size_t bytes_transferred) {
+ boost::ignore_unused(bytes_transferred);
+
+ if (ec) {
+ response_callback_(ec, {});
+ return fail(ec, "write");
+ }
+
+ // Receive the HTTP response
+ http::async_read(stream_, buffer_, res_,
+ [self = this->shared_from_this()](beast::error_code ec, auto bytes_transferred) {
+ self->on_read(ec, bytes_transferred);
+ });
+ }
+
+ void on_read(beast::error_code ec, std::size_t bytes_transferred) {
+ boost::ignore_unused(bytes_transferred);
+
+ // Write the response message to the callback
+ response_callback_(ec, res_);
+
+ // Gracefully close the socket
+ stream_.socket().shutdown(tcp::socket::shutdown_both, ec);
+ stream_.close();
+
+ // not_connected happens sometimes so don't bother reporting it.
+ if (ec && ec != beast::errc::not_connected)
+ return fail(ec, "shutdown");
+
+ // If we get here then the connection is closed gracefully
+ }
+};
+} // namespace details
+
+struct http_request_params {
+ net::io_context& ioc;
+ const std::string host;
+ const unsigned short port;
+ const std::string target;
+ int version;
+ const std::string content_type;
+};
+
+inline void async_http_request(http_request_params& req_params, std::string&& request_body,
+ const response_callback_t& response_callback) {
+ std::make_shared(req_params.ioc, response_callback)
+ ->run(req_params.host, req_params.port, req_params.target, req_params.version, req_params.content_type,
+ std::move(request_body));
+};
+} // namespace http_client_async
+} // namespace eosio
diff --git a/tests/trx_generator/main.cpp b/tests/trx_generator/main.cpp
new file mode 100644
index 0000000..381fe5a
--- /dev/null
+++ b/tests/trx_generator/main.cpp
@@ -0,0 +1,237 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace bpo = boost::program_options;
+namespace et = eosio::testing;
+
+enum return_codes {
+ TERMINATED_EARLY = -3,
+ OTHER_FAIL = -2,
+ INITIALIZE_FAIL = -1,
+ SUCCESS = 0,
+ BAD_ALLOC = 1,
+ DATABASE_DIRTY = 2,
+ FIXED_REVERSIBLE = SUCCESS,
+ EXTRACTED_GENESIS = SUCCESS,
+ NODE_MANAGEMENT_SUCCESS = 5
+};
+
+int main(int argc, char** argv) {
+ et::provider_base_config provider_config;
+ et::trx_generator_base_config trx_gen_base_config;
+ et::accounts_config accts_config;
+ et::trx_tps_tester_config tester_config;
+
+ const int64_t trx_expiration_max = 3600;
+ const uint16_t generator_id_max = 960;
+ bpo::variables_map vmap;
+ bpo::options_description cli("Transaction Generator command line options.");
+ std::string chain_id_in;
+ std::string contract_owner_account_in;
+ std::string miner_account_in;
+ std::string miner_p_key;
+ std::string lib_id_str;
+ std::string accts;
+ std::string nonces;
+ std::string p_keys;
+ int64_t spinup_time_us = 1000000;
+ uint32_t max_lag_per = 5;
+ int64_t max_lag_duration_us = 1000000;
+ int64_t trx_expr = 3600;
+
+ cli.add_options()
+ ("generator-id", bpo::value(&trx_gen_base_config._generator_id)->default_value(0), "Id for the transaction generator. Allowed range (0-960). Defaults to 0.")
+ ("chain-id", bpo::value(&chain_id_in), "set the eos chain id")
+ ("evm-chain-id", bpo::value(&trx_gen_base_config._evm_chain_id)->default_value(15555), "set the eos evm chain id")
+ ("contract-owner-account", bpo::value(&contract_owner_account_in), "Account name of the contract account for the transaction actions.")
+ ("miner-account", bpo::value(&miner_account_in), "Account name of the miner account.")
+ ("miner-priv-keys", bpo::value(&miner_p_key), "Miner's private key")
+ ("accounts", bpo::value(&accts), "comma-separated list of accounts as uint64_t that will be used for transfers. Minimum required accounts: 2.")
+ ("starting-nonces", bpo::value(&nonces), "comma-separated list of starting nonces for each account that will be used for transactions. Minimum required nonces: 2, one per account.")
+ ("priv-keys", bpo::value(&p_keys), "comma-separated list of private keys in same order of accounts list that will be used to sign transactions. Minimum required: 2.")
+ ("gas-price", bpo::value(&trx_gen_base_config._gas_price)->default_value(150'000'000'000), "Gas price for transaction. Defaults to 150 gwei (150'000'000'000).")
+ ("trx-expiration", bpo::value(&trx_expr)->default_value(3600), "transaction expiration time in seconds. Defaults to 3,600. Maximum allowed: 3,600")
+ ("trx-gen-duration", bpo::value(&tester_config._gen_duration_seconds)->default_value(60), "Transaction generation duration (seconds). Defaults to 60 seconds.")
+ ("target-tps", bpo::value(&tester_config._target_tps)->default_value(1), "Target transactions per second to generate/send. Defaults to 1 transaction per second.")
+ ("last-irreversible-block-id", bpo::value(&lib_id_str), "Current last-irreversible-block-id (LIB ID) to use for transactions.")
+ ("monitor-spinup-time-us", bpo::value(&spinup_time_us)->default_value(1000000), "Number of microseconds to wait before monitoring TPS. Defaults to 1000000 (1s).")
+ ("monitor-max-lag-percent", bpo::value(&max_lag_per)->default_value(5), "Max percentage off from expected transactions sent before being in violation. Defaults to 5.")
+ ("monitor-max-lag-duration-us", bpo::value(&max_lag_duration_us)->default_value(1000000), "Max microseconds that transaction generation can be in violation before quitting. Defaults to 1000000 (1s).")
+ ("log-dir", bpo::value(&trx_gen_base_config._log_dir), "set the logs directory")
+ ("stop-on-trx-failed", bpo::value(&trx_gen_base_config._stop_on_trx_failed)->default_value(true), "stop transaction generation if sending fails.")
+ ("api-endpoint", bpo::value(&provider_config._api_endpoint), "The api endpoint to direct transactions to. Defaults to: '/v1/chain/send_transaction2'")
+ ("peer-endpoint-type", bpo::value(&provider_config._peer_endpoint_type)->default_value("p2p"), "Identify the peer endpoint api type to determine how to send transactions. Allowable 'p2p' and 'http'. Default: 'p2p'")
+ ("peer-endpoint", bpo::value(&provider_config._peer_endpoint)->default_value("127.0.0.1"), "set the peer endpoint to send transactions to")
+ ("port", bpo::value(&provider_config._port)->default_value(9876), "set the peer endpoint port to send transactions to")
+ ("help,h", "print this list")
+ ;
+
+ try {
+ bpo::store(bpo::parse_command_line(argc, argv, cli), vmap);
+ bpo::notify(vmap);
+
+ if(vmap.count("help") > 0) {
+ cli.print(std::cerr);
+ return SUCCESS;
+ }
+
+ if(chain_id_in.empty()) {
+ ilog("Initialization error: missing chain-id");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else {
+ trx_gen_base_config._chain_id = eosio::chain::chain_id_type(chain_id_in);
+ }
+
+ if(trx_gen_base_config._log_dir.empty()) {
+ ilog("Initialization error: missing log-dir");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ }
+
+ if(lib_id_str.empty()) {
+ ilog("Initialization error: missing last-irreversible-block-id");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else {
+ trx_gen_base_config._last_irr_block_id = fc::variant(lib_id_str).as();
+ }
+
+ if(contract_owner_account_in.empty()) {
+ ilog("Initialization error: missing contract-owner-account");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else {
+ trx_gen_base_config._contract_owner_account = eosio::chain::name(contract_owner_account_in);
+ }
+
+ if(miner_account_in.empty()) {
+ ilog("Initialization error: missing miner-account");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else {
+ trx_gen_base_config._miner_account = eosio::chain::name(miner_account_in);
+ }
+
+ if(miner_p_key.empty()) {
+ ilog("Initialization error: did not specify miner account's private key.");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else {
+ ilog("Initializing miner private key. Attempt to create private_key for ${key} : gen key ${newKey}", ("key", miner_p_key)("newKey", fc::crypto::private_key(miner_p_key)));
+ trx_gen_base_config._miner_p_key = fc::crypto::private_key(miner_p_key);
+ }
+
+ std::vector account_str_vector;
+ boost::split(account_str_vector, accts, boost::is_any_of(","));
+ if(account_str_vector.size() < 2) {
+ ilog("Initialization error: did not specify transfer accounts. Auto transfer transaction generation requires at minimum 2 transfer accounts.");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else if (!accts.empty() && !account_str_vector.empty()) {
+ for(const std::string& account_name: account_str_vector) {
+ ilog("Initializing accounts. Attempt to create name for ${acct}", ("acct", account_name));
+ accts_config._acct_name_vec.emplace_back(boost::lexical_cast(account_name));
+ }
+ }
+
+ std::vector nonces_str_vector;
+ boost::split(nonces_str_vector, nonces, boost::is_any_of(","));
+ if(nonces_str_vector.size() < 2) {
+ ilog("Initialization error: did not specify account nonces. Auto transfer transaction generation requires at minimum 2 initial nonces for transfer accounts.");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else if (!nonces.empty() && !nonces_str_vector.empty()) {
+ for(const std::string& nonce: nonces_str_vector) {
+ ilog("Initializing nonces. Attempt to create nonces for ${nonce}", ("nonce", nonce));
+ accts_config._nonces.emplace_back(boost::lexical_cast(nonce));
+ }
+ }
+
+ std::vector private_keys_str_vector;
+ boost::split(private_keys_str_vector, p_keys, boost::is_any_of(","));
+ if(private_keys_str_vector.size() < 2) {
+ ilog("Initialization error: did not specify accounts' private keys. Auto transfer transaction generation requires at minimum 2 private keys.");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else if (!p_keys.empty() && !private_keys_str_vector.empty()) {
+ for(auto private_key : private_keys_str_vector) {
+ ilog("Initializing private keys. Attempt to create private_key for ${key}", ("key", private_key));
+ std::array tmp;
+ std::memcpy(tmp.data(), private_key.data(), tmp.size());
+ ilog("Initializing private keys. Attempt to create private_key for ${key} : gen key ${newKey}", ("key", private_key)("newKey", tmp));
+ accts_config._priv_keys_vec.emplace_back(std::move(tmp));
+ }
+ }
+
+ if(trx_gen_base_config._generator_id > generator_id_max) {
+ ilog("Initialization error: Exceeded max value for generator id. Value must be less than ${max}.", ("max", generator_id_max));
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ }
+
+ if(trx_expr > trx_expiration_max) {
+ ilog("Initialization error: Exceeded max value for transaction expiration. Value must be less than ${max}.", ("max", trx_expiration_max));
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ } else {
+ trx_gen_base_config._trx_expiration_us = fc::seconds(trx_expr);
+ }
+
+ if(spinup_time_us < 0) {
+ ilog("Initialization error: spinup-time-us cannot be negative");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ }
+
+ if(max_lag_duration_us < 0) {
+ ilog("Initialization error: max-lag-duration-us cannot be negative");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ }
+
+ if(max_lag_per > 100) {
+ ilog("Initialization error: max-lag-percent must be between 0 and 100");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ }
+
+ if (!(provider_config._peer_endpoint_type == "p2p" || provider_config._peer_endpoint_type == "http")) {
+ ilog("Initialization error: peer-endpoint-type must be either 'p2p', or 'http'");
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ }
+ } catch(bpo::unknown_option& ex) {
+ std::cerr << ex.what() << std::endl;
+ cli.print(std::cerr);
+ return INITIALIZE_FAIL;
+ }
+
+ ilog("Initial Trx Generator config: ${config}", ("config", trx_gen_base_config.to_string()));
+ ilog("Initial Provider config: ${config}", ("config", provider_config.to_string()));
+ ilog("Initial Accounts config: ${config}", ("config", accts_config.to_string()));
+ ilog("Transaction TPS Tester config: ${config}", ("config", tester_config.to_string()));
+
+
+ std::shared_ptr monitor;
+ auto generator = std::make_shared(trx_gen_base_config, provider_config, accts_config);
+
+ monitor = std::make_shared(spinup_time_us, max_lag_per, max_lag_duration_us);
+ et::trx_tps_tester tester{generator, monitor, tester_config};
+
+ if (!tester.run()) {
+ return OTHER_FAIL;
+ }
+
+ if (monitor->terminated_early()) {
+ return TERMINATED_EARLY;
+ }
+
+ return SUCCESS;
+
+}
diff --git a/tests/trx_generator/simple_rest_server.hpp b/tests/trx_generator/simple_rest_server.hpp
new file mode 100644
index 0000000..f82492d
--- /dev/null
+++ b/tests/trx_generator/simple_rest_server.hpp
@@ -0,0 +1,234 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace eosio { namespace rest {
+
+ // The majority of the code here are derived from boost source
+ // libs/beast/example/http/server/async/http_server_async.cpp
+ // with minimum modification and yet reusable.
+
+ namespace beast = boost::beast; // from
+ namespace http = beast::http; // from
+ namespace net = boost::asio; // from
+ using tcp = boost::asio::ip::tcp; // from
+ template
+ class simple_server {
+ T* self() { return static_cast(this); }
+
+ void fail(beast::error_code ec, char const* what) { self()->log_error(what, ec.message()); }
+ // Return a response for the given request.
+ http::response handle_request(http::request&& req) {
+ auto server_header = self()->server_header();
+ // Returns a bad request response
+ auto const bad_request = [&req, &server_header](std::string_view why) {
+ http::response res{ http::status::bad_request, req.version() };
+ res.set(http::field::server, server_header);
+ res.set(http::field::content_type, "text/plain");
+ res.keep_alive(req.keep_alive());
+ res.body() = std::string(why);
+ res.prepare_payload();
+ return res;
+ };
+
+ // Returns a not found response
+ auto const not_found = [&req, &server_header](std::string_view target) {
+ http::response res{ http::status::not_found, req.version() };
+ res.set(http::field::server, server_header);
+ res.set(http::field::content_type, "text/plain");
+ res.keep_alive(req.keep_alive());
+ res.body() = "The resource '" + std::string(target) + "' was not found.";
+ res.prepare_payload();
+ return res;
+ };
+
+ // Returns a server error response
+ auto const server_error = [&req, &server_header](std::string_view what) {
+ http::response res{ http::status::internal_server_error, req.version() };
+ res.set(http::field::server, server_header);
+ res.set(http::field::content_type, "text/plain");
+ res.keep_alive(req.keep_alive());
+ res.body() = "An error occurred: '" + std::string(what) + "'";
+ res.prepare_payload();
+ return res;
+ };
+
+ // Make sure we can handle the method
+ if (!self()->allow_method(req.method()))
+ return bad_request("Unknown HTTP-method");
+
+ // Request path must be absolute and not contain "..".
+ std::string_view target{req.target().data(), req.target().size()};
+ if (target.empty() || target[0] != '/' || target.find("..") != std::string_view::npos)
+ return bad_request("Illegal request-target");
+
+ try {
+ auto res = self()->on_request(std::move(req));
+ if (!res)
+ not_found(target);
+ return *res;
+ } catch (std::exception& ex) { return server_error(ex.what()); }
+ }
+
+ class session : public std::enable_shared_from_this {
+ tcp::socket socket_;
+ boost::asio::io_context::strand strand_;
+ beast::flat_buffer buffer_;
+ http::request req_;
+ simple_server* server_;
+ std::shared_ptr> res_;
+
+ public:
+ // Take ownership of the stream
+ session(net::io_context& ioc, tcp::socket&& socket, simple_server* server)
+ : socket_(std::move(socket)), strand_(ioc), server_(server) {}
+
+ // Start the asynchronous operation
+ void run() { do_read(); }
+
+ void do_read() {
+ // Make the request empty before reading,
+ // otherwise the operation behavior is undefined.
+ req_ = {};
+
+ // Read a request
+ http::async_read(
+ socket_, buffer_, req_,
+ boost::asio::bind_executor(strand_, [self = this->shared_from_this()](beast::error_code ec,
+ std::size_t bytes_transferred) {
+ self->on_read(ec, bytes_transferred);
+ }));
+ }
+
+ void on_read(beast::error_code ec, std::size_t bytes_transferred) {
+ boost::ignore_unused(bytes_transferred);
+
+ // This means they closed the connection
+ if (ec == http::error::end_of_stream)
+ return do_close();
+
+ if (ec)
+ return server_->fail(ec, "read");
+
+ // Send the response
+ send_response(server_->handle_request(std::move(req_)));
+ }
+
+ void send_response(http::response&& msg) {
+ // The lifetime of the message has to extend
+ // for the duration of the async operation so
+ // we use a shared_ptr to manage it.
+ res_ = std::make_shared>(std::move(msg));
+
+ // Write the response
+ http::async_write(socket_, *res_,
+ boost::asio::bind_executor(socket_.get_executor(),
+ [self = this->shared_from_this(), close = res_->need_eof()](
+ beast::error_code ec, std::size_t bytes_transferred) {
+ self->on_write(ec, bytes_transferred, close);
+ }));
+ }
+
+ void on_write(boost::system::error_code ec, std::size_t bytes_transferred, bool close) {
+ boost::ignore_unused(bytes_transferred);
+
+ if (ec)
+ return server_->fail(ec, "write");
+
+ if (close) {
+ // This means we should close the connection, usually because
+ // the response indicated the "Connection: close" semantic.
+ return do_close();
+ }
+
+ // We're done with the response so delete it
+ res_ = nullptr;
+
+ // Read another request
+ do_read();
+ }
+
+ void do_close() {
+ // Send a TCP shutdown
+ beast::error_code ec;
+ socket_.shutdown(tcp::socket::shutdown_send, ec);
+
+ // At this point the connection is closed gracefully
+ }
+ };
+
+ //------------------------------------------------------------------------------
+
+ // Accepts incoming connections and launches the sessions
+ class listener : public std::enable_shared_from_this {
+ net::io_context& ioc_;
+ tcp::acceptor acceptor_;
+ tcp::socket socket_;
+ simple_server* server_;
+
+ public:
+ listener(net::io_context& ioc, tcp::endpoint endpoint, simple_server* server)
+ : ioc_(ioc), acceptor_(ioc), socket_(ioc), server_(server) {
+ boost::system::error_code ec;
+
+ // Open the acceptor
+ acceptor_.open(endpoint.protocol(), ec);
+ if (ec) {
+ server_->fail(ec, "open");
+ return;
+ }
+
+ // Allow address reuse
+ acceptor_.set_option(net::socket_base::reuse_address(true), ec);
+ if (ec) {
+ server_->fail(ec, "set_option");
+ return;
+ }
+
+ // Bind to the server address
+ acceptor_.bind(endpoint, ec);
+ if (ec) {
+ server_->fail(ec, "bind");
+ return;
+ }
+
+ // Start listening for connections
+ acceptor_.listen(net::socket_base::max_listen_connections, ec);
+ if (ec) {
+ server_->fail(ec, "listen");
+ return;
+ }
+ }
+
+ // Start accepting incoming connections
+ void run() {
+ if (!acceptor_.is_open())
+ return;
+ do_accept();
+ }
+
+ private:
+ void do_accept() {
+ acceptor_.async_accept(
+ socket_, [self = this->shared_from_this()](boost::system::error_code ec) { self->on_accept(ec); });
+ }
+
+ void on_accept(boost::system::error_code ec) {
+ if (ec) {
+ server_->fail(ec, "accept");
+ } else {
+ // Create the session and run it
+ std::make_shared(ioc_, std::move(socket_), server_)->run();
+ }
+
+ // Accept another connection
+ do_accept();
+ }
+ };
+
+ public:
+ void run(net::io_context& ioc, tcp::endpoint endpoint) { std::make_shared(ioc, endpoint, this)->run(); }
+ };
+}} // namespace eosio::rest
\ No newline at end of file
diff --git a/tests/trx_generator/trx_generator.cpp b/tests/trx_generator/trx_generator.cpp
new file mode 100644
index 0000000..e6d7ef8
--- /dev/null
+++ b/tests/trx_generator/trx_generator.cpp
@@ -0,0 +1,225 @@
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace eosio::testing {
+ namespace chain = eosio::chain;
+ using mvo = fc::mutable_variant_object;
+
+ void trx_generator_base::set_transaction_headers(chain::transaction& trx, const chain::block_id_type& last_irr_block_id, const fc::microseconds& expiration, uint32_t delay_sec) {
+ trx.expiration = fc::time_point_sec{fc::time_point::now() + expiration};
+ trx.set_reference_block(last_irr_block_id);
+
+ trx.max_net_usage_words = 0;// No limit
+ trx.max_cpu_usage_ms = 0; // No limit
+ trx.delay_sec = delay_sec;
+ }
+
+ void trx_generator_base::push_transaction(evm_and_signed_transactions_w_signers& trx) {
+ update_resign_transaction(trx);
+ if (_txcount == 0) {
+ log_first_trx(_config._log_dir, trx._trx);
+ }
+ _provider.send(trx._trx);
+ }
+
+ void trx_generator_base::stop_generation() {
+ ilog("Stopping transaction generation");
+
+ if (_txcount) {
+ ilog("${d} transactions executed, ${t}us / transaction", ("d", _txcount)("t", _total_us / (double) _txcount));
+ _txcount = _total_us = 0;
+ }
+ }
+
+ bool trx_generator_base::stop_on_trx_fail() {
+ return _config._stop_on_trx_failed;
+ }
+
+ trx_generator_base::trx_generator_base(const trx_generator_base_config& trx_gen_base_config, const provider_base_config& provider_config)
+ : _config(trx_gen_base_config), _provider(provider_config) {}
+
+ transfer_trx_generator::transfer_trx_generator(const trx_generator_base_config& trx_gen_base_config, const provider_base_config& provider_config,
+ const accounts_config& accts_config)
+ : trx_generator_base(trx_gen_base_config, provider_config), _accts_config(accts_config) {}
+
+ bool transfer_trx_generator::setup() {
+
+ ilog("Stop Generation (form potential ongoing generation in preparation for starting new generation run).");
+ stop_generation();
+
+ ilog("Create All Initial Swap Transactions/Reaction Pairs (acct 1 -> acct 2, acct 2 -> acct 1) between all provided accounts.");
+ create_initial_swap_transactions();
+
+ ilog("Setup p2p transaction provider");
+
+ ilog("Update each trx to qualify as unique and fresh timestamps, re-sign trx, and send each updated transactions via p2p transaction provider");
+
+ _provider.setup();
+ return true;
+ }
+
+ bool trx_generator_base::tear_down() {
+ _provider.teardown();
+ _provider.log_trxs(_config._log_dir);
+
+ ilog("Sent transactions: ${cnt}", ("cnt", _txcount));
+ ilog("Tear down p2p transaction provider");
+
+ //Stop & Cleanup
+ ilog("Stop Generation.");
+ stop_generation();
+ return true;
+ }
+
+ bool trx_generator_base::generate_and_send() {
+ try {
+ if (_trxs.size()) {
+ size_t index_to_send = _txcount % _trxs.size();
+ push_transaction(_trxs.at(index_to_send));
+ ++_txcount;
+ } else {
+ elog("no transactions available to send");
+ return false;
+ }
+ } catch (const std::exception &e) {
+ elog("${e}", ("e", e.what()));
+ return false;
+ } catch (...) {
+ elog("unknown exception");
+ return false;
+ }
+
+ return true;
+ }
+
+ void trx_generator_base::log_first_trx(const std::string& log_dir, const chain::signed_transaction& trx) {
+ std::ostringstream fileName;
+ fileName << log_dir << "/first_trx_" << getpid() << ".txt";
+ std::ofstream out(fileName.str());
+
+ out << std::string(trx.id()) << "\n";
+ out.close();
+ }
+
+ uint64_t trx_generator_base::next_nonce(size_t nonce_index) {
+ return _nonce++;
+ }
+
+ uint64_t transfer_trx_generator::next_nonce(size_t nonce_index) {
+ return _accts_config._nonces.at(nonce_index)++;
+ }
+
+ void trx_generator_base::update_resign_transaction(evm_and_signed_transactions_w_signers& trx) {
+ sign_evm_trx(trx._evm_trx, next_nonce(trx._evm_nonce_index), trx._evm_signer);
+
+ silkworm::Bytes rlp;
+ silkworm::rlp::encode(rlp, trx._evm_trx);
+
+ eosio::chain::bytes rlp_bytes;
+ rlp_bytes.resize(rlp.size());
+ memcpy(rlp_bytes.data(), rlp.data(), rlp.size());
+
+ chain::action act = chain::action(std::vector{{_config._miner_account, chain::config::active_name}}, _config._evm_account_name, "pushtx"_n, fc::raw::pack(_config._miner_account, rlp_bytes));
+
+ trx._trx.actions.clear();
+ trx._trx.actions.emplace_back( std::move(act) );
+ set_transaction_headers( trx._trx, _config._last_irr_block_id, _config._trx_expiration_us );
+ trx._trx.context_free_actions.clear();
+ trx._trx.context_free_actions.emplace_back(std::vector(), chain::config::null_account_name, chain::name("nonce"),
+ fc::raw::pack(std::to_string(_config._generator_id) + ":" + std::to_string(fc::time_point::now().time_since_epoch().count())));
+
+ trx._trx.signatures.clear();
+ trx._trx.sign(_config._miner_p_key, _config._chain_id);
+ }
+
+ chain::action trx_generator_base::get_action(eosio::chain::name code, eosio::chain::name acttype, std::vector auths,
+ const chain::bytes &data) const
+ {
+ chain::action act;
+ act.account = code;
+ act.name = acttype;
+ act.authorization = auths;
+ act.data = data;
+ return act;
+ }
+
+ void transfer_trx_generator::create_evm_and_signed_transactions_w_signers(evmc::address& from, evmc::address& to,
+ size_t from_nonce_index, std::array& from_priv_key) {
+ //create the actions here
+ ilog("create_initial_swap_transactions: creating swap from ${acctA} to ${acctB}",
+ ("acctA", evmc::hex(static_cast(from)))("acctB", evmc::hex(static_cast(to))));
+ silkworm::Transaction a_to_b = silkworm::Transaction();
+ a_to_b.type = silkworm::TransactionType::kLegacy;
+ a_to_b.max_priority_fee_per_gas = _config._gas_price;
+ a_to_b.max_fee_per_gas = _config._gas_price;
+ a_to_b.gas_limit = _config._gas_limit;
+ a_to_b.to = to;
+ a_to_b.value = _config._transfer_value;
+
+ sign_evm_trx(a_to_b, _accts_config._nonces.at(from_nonce_index), from_priv_key);
+
+ silkworm::Bytes rlp;
+ silkworm::rlp::encode(rlp, a_to_b);
+
+ eosio::chain::bytes rlp_bytes;
+ rlp_bytes.resize(rlp.size());
+ memcpy(rlp_bytes.data(), rlp.data(), rlp.size());
+
+ chain::action act = get_action(_config._evm_account_name, "pushtx"_n, std::vector{{_config._miner_account, chain::config::active_name}}, fc::raw::pack(_config._miner_account, rlp_bytes));
+
+ eosio::chain::signed_transaction trx;
+ trx.actions.emplace_back( std::move(act) );
+ set_transaction_headers( trx, _config._last_irr_block_id, _config._trx_expiration_us );
+ trx.context_free_actions.emplace_back(std::vector(), chain::config::null_account_name, chain::name("nonce"), fc::raw::pack(std::to_string(_config._generator_id) + ":" + std::to_string(fc::time_point::now().time_since_epoch().count())));
+
+ trx.sign(_config._miner_p_key, _config._chain_id);
+ _trxs.emplace_back(a_to_b, from_priv_key, from_nonce_index, trx, _config._miner_p_key);
+ }
+
+ void transfer_trx_generator::create_initial_swap_transactions() {
+
+ for (size_t i = 0; i < _accts_config._acct_name_vec.size(); ++i) {
+ for (size_t j = i + 1; j < _accts_config._acct_name_vec.size(); ++j) {
+ //create the actions here
+ ilog("create_initial_swap_transactions: creating swap from ${acctA} to ${acctB}",
+ ("acctA", evmc::hex(static_cast(_accts_config._acct_name_vec.at(i))))("acctB", evmc::hex(static_cast(_accts_config._acct_name_vec.at(j)))));
+ create_evm_and_signed_transactions_w_signers(_accts_config._acct_name_vec.at(i), _accts_config._acct_name_vec.at(j), i, _accts_config._priv_keys_vec.at(i));
+
+ ilog("create_initial_swap_transactions: creating swap from ${acctB} to ${acctA}",
+ ("acctB", evmc::hex(static_cast(_accts_config._acct_name_vec.at(j))))("acctA", evmc::hex(static_cast(_accts_config._acct_name_vec.at(i)))));
+ create_evm_and_signed_transactions_w_signers(_accts_config._acct_name_vec.at(j), _accts_config._acct_name_vec.at(i), j, _accts_config._priv_keys_vec.at(j));
+ }
+ }
+ ilog("create_initial_swap_transactions: total action pairs created: ${pairs}", ("pairs", _trxs.size()));
+ }
+
+ void trx_generator_base::sign_evm_trx(silkworm::Transaction &trx, uint64_t nonce, std::array &private_key)
+ {
+ silkworm::Bytes rlp;
+ trx.chain_id = _config._evm_chain_id;
+ trx.nonce = nonce;
+ silkworm::rlp::encode(rlp, trx, false);
+ ethash::hash256 hash{silkworm::keccak256(rlp)};
+
+ secp256k1_ecdsa_recoverable_signature sig;
+ secp256k1_context* ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
+ if(!secp256k1_ecdsa_sign_recoverable(ctx, &sig, hash.bytes, private_key.data(), NULL, NULL)) {
+ elog("sign_evm_trx -- secp256k1_ecdsa_sign_recoverable FAILED");
+ }
+ uint8_t r_and_s[64];
+ int recid;
+ secp256k1_ecdsa_recoverable_signature_serialize_compact(ctx, r_and_s, &recid, &sig);
+
+ trx.r = intx::be::unsafe::load(r_and_s);
+ trx.s = intx::be::unsafe::load(r_and_s + 32);
+ trx.odd_y_parity = recid;
+ }
+}
diff --git a/tests/trx_generator/trx_generator.hpp b/tests/trx_generator/trx_generator.hpp
new file mode 100644
index 0000000..dbb5b9c
--- /dev/null
+++ b/tests/trx_generator/trx_generator.hpp
@@ -0,0 +1,145 @@
+#pragma once
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace eosio::testing {
+ using namespace chain::literals;
+ using namespace evmc::literals;
+ using namespace intx;
+
+ inline constexpr intx::uint256 operator"" _gwei(const char *s)
+ {
+ return intx::from_string(s) * intx::exp(10_u256, 9_u256);
+ }
+
+ struct evm_and_signed_transactions_w_signers {
+ evm_and_signed_transactions_w_signers(silkworm::Transaction evm_trx, std::array evm_signer, size_t evm_nonce_index, eosio::chain::signed_transaction trx, fc::crypto::private_key key)
+ : _evm_trx(std::move(evm_trx)), _evm_signer(evm_signer), _evm_nonce_index(evm_nonce_index), _trx(std::move(trx)), _signer(key) {}
+
+ silkworm::Transaction _evm_trx;
+ std::array _evm_signer;
+ size_t _evm_nonce_index;
+ eosio::chain::signed_transaction _trx;
+ fc::crypto::private_key _signer;
+ };
+
+ struct trx_generator_base_config {
+ uint16_t _generator_id = 0;
+ eosio::chain::chain_id_type _chain_id = eosio::chain::chain_id_type::empty_chain_id();
+ uint64_t _evm_chain_id = 15555;
+ static constexpr eosio::chain::name _evm_account_name = "evm"_n;
+ eosio::chain::name _contract_owner_account = eosio::chain::name();
+ eosio::chain::name _miner_account = eosio::chain::name();
+ fc::crypto::private_key _miner_p_key;
+ fc::microseconds _trx_expiration_us = fc::seconds(3600);
+ eosio::chain::block_id_type _last_irr_block_id = eosio::chain::block_id_type();
+ std::string _log_dir = ".";
+ bool _stop_on_trx_failed = true;
+ uint64_t _gas_price = 150'000'000'000;
+ intx::uint256 _transfer_value = 1_gwei;
+ uint64_t _gas_limit = 21000;
+
+
+ std::string to_string() const {
+ std::ostringstream ss;
+ ss << " generator id: " << _generator_id << " chain id: " << std::string(_chain_id) << " contract owner account: "
+ << _contract_owner_account << " trx expiration seconds: " << _trx_expiration_us.to_seconds() << " lib id: " << std::string(_last_irr_block_id)
+ << " log dir: " << _log_dir << " stop on trx failed: " << _stop_on_trx_failed << " base gas price " << _gas_price ;
+ return std::move(ss).str();
+ };
+ };
+
+ struct accounts_config {
+ std::vector _acct_name_vec;
+ std::vector> _priv_keys_vec;
+ std::vector _nonces;
+
+ std::string to_string() const {
+ std::ostringstream ss;
+ ss << "Accounts Specified: accounts: [ ";
+ for(size_t i = 0; i < _acct_name_vec.size(); ++i) {
+ ss << evmc::hex(static_cast(_acct_name_vec.at(i)));
+ if(i < _acct_name_vec.size() - 1) {
+ ss << ", ";
+ }
+ }
+ ss << " ] keys: [ ";
+ for(size_t i = 0; i < _priv_keys_vec.size(); ++i) {
+ for (auto c : _priv_keys_vec.at(i)) {
+ ss << c;
+ }
+ if(i < _priv_keys_vec.size() - 1) {
+ ss << ", ";
+ }
+ }
+ ss << " ] nonces: [ ";
+ for(size_t i = 0; i < _nonces.size(); ++i) {
+ ss << _nonces.at(i);
+ if(i < _nonces.size() - 1) {
+ ss << ", ";
+ }
+ }
+ ss << " ]";
+
+ return std::move(ss).str();
+ };
+ };
+
+ struct trx_generator_base {
+ const trx_generator_base_config& _config;
+ trx_provider _provider;
+
+ uint64_t _total_us = 0;
+ uint64_t _txcount = 0;
+ uint64_t _nonce = 0;
+
+ std::vector _trxs;
+
+ trx_generator_base(const trx_generator_base_config& trx_gen_base_config, const provider_base_config& provider_config);
+
+ virtual ~trx_generator_base() = default;
+
+ virtual uint64_t next_nonce(size_t nonce_index);
+ virtual void update_resign_transaction(evm_and_signed_transactions_w_signers& trx);
+ virtual void sign_evm_trx(silkworm::Transaction &trx, uint64_t nonce, std::array &private_key);
+
+ chain::action get_action(eosio::chain::name code, eosio::chain::name acttype, std::vector auths, const chain::bytes &data) const;
+ void push_transaction(evm_and_signed_transactions_w_signers& trx);
+
+ void set_transaction_headers(eosio::chain::transaction& trx, const eosio::chain::block_id_type& last_irr_block_id, const fc::microseconds& expiration, uint32_t delay_sec = 0);
+
+ void log_first_trx(const std::string& log_dir, const eosio::chain::signed_transaction& trx);
+
+ bool generate_and_send();
+ bool tear_down();
+ void stop_generation();
+ bool stop_on_trx_fail();
+ };
+
+ struct transfer_trx_generator : public trx_generator_base {
+ accounts_config _accts_config;
+
+ transfer_trx_generator(const trx_generator_base_config& trx_gen_base_config, const provider_base_config& provider_config, const accounts_config& accts_config);
+
+ virtual uint64_t next_nonce(size_t nonce_index);
+ void create_initial_swap_transactions();
+
+ void create_evm_and_signed_transactions_w_signers(evmc::address &from, evmc::address &to, size_t from_nonce_index, std::array &from_priv_key);
+ void sign_swap(silkworm::Transaction &trx, uint64_t evm_chain_id, std::array &private_key);
+
+ bool setup();
+ };
+}
diff --git a/tests/trx_generator/trx_provider.cpp b/tests/trx_generator/trx_provider.cpp
new file mode 100644
index 0000000..190c6b4
--- /dev/null
+++ b/tests/trx_generator/trx_provider.cpp
@@ -0,0 +1,292 @@
+#include
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace eosio::testing {
+ using namespace boost::asio;
+ using namespace std::literals::string_literals;
+ using ip::tcp;
+
+ constexpr auto message_header_size = sizeof(uint32_t);
+ constexpr uint32_t packed_trx_which = 8; // this is the "which" for packed_transaction in the net_message variant
+
+ static send_buffer_type create_send_buffer( const chain::packed_transaction& m ) {
+ const uint32_t which_size = fc::raw::pack_size(chain::unsigned_int(packed_trx_which));
+ const uint32_t payload_size = which_size + fc::raw::pack_size( m );
+ const size_t buffer_size = message_header_size + payload_size;
+
+ const char* const header = reinterpret_cast(&payload_size); // avoid variable size encoding of uint32_t
+
+
+ auto send_buffer = std::make_shared>(buffer_size);
+ fc::datastream ds( send_buffer->data(), buffer_size);
+ ds.write( header, message_header_size );
+ fc::raw::pack( ds, fc::unsigned_int(packed_trx_which));
+ fc::raw::pack( ds, m );
+
+ return send_buffer;
+ }
+
+ void provider_connection::init_and_connect() {
+ _connection_thread_pool.start(1, {});
+ connect();
+ };
+
+ void provider_connection::cleanup_and_disconnect() {
+ disconnect();
+ _connection_thread_pool.stop();
+ };
+
+ fc::time_point provider_connection::get_trx_ack_time(const eosio::chain::transaction_id_type& trx_id) {
+ fc::time_point time_acked;
+ std::lock_guard lock(_trx_ack_map_lock);
+ auto search = _trxs_ack_time_map.find(trx_id);
+ if (search != _trxs_ack_time_map.end()) {
+ time_acked = search->second;
+ } else {
+ elog("get_trx_ack_time - Transaction acknowledge time not found for transaction with id: ${id}",
+ ("id", trx_id));
+ time_acked = fc::time_point::min();
+ }
+ return time_acked;
+ }
+
+ void provider_connection::trx_acknowledged(const eosio::chain::transaction_id_type& trx_id,
+ const fc::time_point& ack_time) {
+ std::lock_guard lock(_trx_ack_map_lock);
+ _trxs_ack_time_map[trx_id] = ack_time;
+ }
+
+ void p2p_connection::connect() {
+ ilog("Attempting P2P connection to ${ip}:${port}.", ("ip", _config._peer_endpoint)("port", _config._port));
+ tcp::resolver r(_connection_thread_pool.get_executor());
+ auto i = r.resolve(tcp::v4(), _config._peer_endpoint, std::to_string(_config._port));
+ boost::asio::connect(_p2p_socket, i);
+ ilog("Connected to ${ip}:${port}.", ("ip", _config._peer_endpoint)("port", _config._port));
+ }
+
+ void p2p_connection::disconnect() {
+ ilog("Closing socket.");
+ _p2p_socket.close();
+ ilog("Socket closed.");
+ }
+
+ void p2p_connection::send_transaction(const chain::packed_transaction& trx) {
+ send_buffer_type msg = create_send_buffer(trx);
+ _p2p_socket.send(boost::asio::buffer(*msg));
+ trx_acknowledged(trx.id(), fc::time_point::min()); //using min to identify ack time as not applicable for p2p
+ }
+
+ acked_trx_trace_info p2p_connection::get_acked_trx_trace_info(const eosio::chain::transaction_id_type& trx_id) {
+ return {};
+ }
+
+ void http_connection::connect() {}
+
+ void http_connection::disconnect() {
+ int max = 30;
+ int waited = 0;
+ while (_sent.load() != _acknowledged.load() && waited < max) {
+ ilog("http_connection::disconnect waiting on ack - sent ${s} | acked ${a} | waited ${w}",
+ ("s", _sent.load())("a", _acknowledged.load())("w", waited));
+ sleep(1);
+ ++waited;
+ }
+ if (waited == max) {
+ elog("http_connection::disconnect failed to receive all acks in time - sent ${s} | acked ${a} | waited ${w}",
+ ("s", _sent.load())("a", _acknowledged.load())("w", waited));
+ }
+ }
+
+ bool http_connection::needs_response_trace_info() {
+ return is_read_only_transaction();
+ }
+
+ bool http_connection::is_read_only_transaction() {
+ return _config._api_endpoint == "/v1/chain/send_read_only_transaction";
+ }
+
+ void http_connection::send_transaction(const chain::packed_transaction& trx) {
+ const int http_version = 11;
+ const std::string content_type = "application/json"s;
+
+ bool retry = false;
+ bool tx_rtn_failure_trace = true;
+ auto to_send = fc::mutable_variant_object()("return_failure_trace", tx_rtn_failure_trace)("retry_trx", retry)("transaction", trx);
+ std::string msg_body = fc::json::to_string(to_send, fc::time_point::maximum());
+
+ http_client_async::http_request_params params{_connection_thread_pool.get_executor(),
+ _config._peer_endpoint,
+ _config._port,
+ _config._api_endpoint,
+ http_version,
+ content_type};
+ http_client_async::async_http_request(
+ params, std::move(msg_body),
+ [this, trx_id = trx.id()](boost::beast::error_code ec,
+ boost::beast::http::response response) {
+ trx_acknowledged(trx_id, fc::time_point::now());
+ if (ec) {
+ elog("http error: ${c}: ${m}", ("c", ec.value())("m", ec.message()));
+ throw std::runtime_error(ec.message());
+ }
+
+ if (this->needs_response_trace_info() && response.result() == boost::beast::http::status::ok) {
+ try {
+ fc::variant resp_json = fc::json::from_string(response.body());
+ if (resp_json.is_object() && resp_json.get_object().contains("processed")) {
+ const auto& processed = resp_json["processed"];
+ const auto& block_num = processed["block_num"].as_uint64();
+ const auto& block_time = processed["block_time"].as_string();
+ const auto& elapsed_time = processed["elapsed"].as_uint64();
+ std::string status = "failed";
+ uint32_t net = 0;
+ uint32_t cpu = 0;
+ if (processed.get_object().contains("receipt")) {
+ const auto& receipt = processed["receipt"];
+ if (receipt.is_object()) {
+ status = receipt["status"].as_string();
+ net = receipt["net_usage_words"].as_uint64() * 8;
+ cpu = receipt["cpu_usage_us"].as_uint64();
+ }
+ if (status == "executed") {
+ record_trx_info(trx_id, block_num, this->is_read_only_transaction() ? elapsed_time : cpu, net, block_time);
+ } else {
+ elog("async_http_request Transaction receipt status not executed: ${string}",
+ ("string", response.body()));
+ }
+ } else {
+ elog("async_http_request Transaction failed, no receipt: ${string}",
+ ("string", response.body()));
+ }
+ } else {
+ elog("async_http_request Transaction failed, transaction not processed: ${string}",
+ ("string", response.body()));
+ }
+ }
+ EOS_RETHROW_EXCEPTIONS(chain::json_parse_exception, "Fail to parse JSON from string: ${string}",
+ ("string", response.body()));
+ }
+
+ if (!(response.result() == boost::beast::http::status::accepted ||
+ response.result() == boost::beast::http::status::ok)) {
+ elog("async_http_request Failed with response http status code: ${status}",
+ ("status", response.result_int()));
+ }
+ ++this->_acknowledged;
+ });
+ ++_sent;
+ }
+
+ void http_connection::record_trx_info(const eosio::chain::transaction_id_type& trx_id, uint32_t block_num,
+ uint32_t cpu_usage_us, uint32_t net_usage_words,
+ const std::string& block_time) {
+ std::lock_guard lock(_trx_info_map_lock);
+ _acked_trx_trace_info_map.insert({trx_id, {true, block_num, cpu_usage_us, net_usage_words, block_time}});
+ }
+
+ acked_trx_trace_info http_connection::get_acked_trx_trace_info(const eosio::chain::transaction_id_type& trx_id) {
+ acked_trx_trace_info info;
+ std::lock_guard lock(_trx_info_map_lock);
+ auto search = _acked_trx_trace_info_map.find(trx_id);
+ if (search != _acked_trx_trace_info_map.end()) {
+ info = search->second;
+ } else {
+ elog("get_acked_trx_trace_info - Acknowledged transaction trace info not found for transaction with id: ${id}", ("id", trx_id));
+ }
+ return info;
+ }
+
+ trx_provider::trx_provider(const provider_base_config& provider_config) {
+ if (provider_config._peer_endpoint_type == "http") {
+ _conn.emplace(provider_config);
+ _peer_connection = &std::get(_conn);
+ } else {
+ _conn.emplace(provider_config);
+ _peer_connection = &std::get(_conn);
+ }
+ }
+
+ void trx_provider::setup() { _peer_connection->init_and_connect(); }
+
+ void trx_provider::send(const chain::signed_transaction& trx) {
+ chain::packed_transaction pt(trx);
+ _peer_connection->send_transaction(pt);
+ _sent_trx_data.push_back(logged_trx_data(trx.id()));
+ }
+
+ void trx_provider::log_trxs(const std::string& log_dir) {
+ std::ostringstream fileName;
+ fileName << log_dir << "/trx_data_output_" << getpid() << ".txt";
+ std::ofstream out(fileName.str());
+
+ for (const logged_trx_data& data : _sent_trx_data) {
+ fc::time_point acked = _peer_connection->get_trx_ack_time(data._trx_id);
+ std::string acked_str;
+ fc::microseconds ack_round_trip_us;
+ if (fc::time_point::min() == acked) {
+ acked_str = "NA";
+ ack_round_trip_us = fc::microseconds(-1);
+ } else {
+ acked_str = acked.to_iso_string();
+ ack_round_trip_us = acked - data._timestamp;
+ }
+ out << std::string(data._trx_id) << "," << data._timestamp.to_iso_string() << "," << acked_str << ","
+ << ack_round_trip_us.count();
+
+ acked_trx_trace_info info = _peer_connection->get_acked_trx_trace_info(data._trx_id);
+ if (info._valid) {
+ out << "," << info._block_num << "," << info._cpu_usage_us << "," << info._net_usage_words << "," << info._block_time;
+ }
+ out << "\n";
+ }
+ out.close();
+ }
+
+ void trx_provider::teardown() {
+ _peer_connection->cleanup_and_disconnect();
+ }
+
+ bool tps_performance_monitor::monitor_test(const tps_test_stats &stats) {
+ if ((!stats.expected_sent) || (stats.last_run - stats.start_time < _spin_up_time)) {
+ return true;
+ }
+
+ int32_t trxs_behind = stats.expected_sent - stats.trxs_sent;
+ if (trxs_behind < 1) {
+ return true;
+ }
+
+ uint32_t per_off = (100*trxs_behind) / stats.expected_sent;
+
+ if (per_off > _max_lag_per) {
+ if (_violation_start_time.has_value()) {
+ auto lag_duration_us = stats.last_run - _violation_start_time.value();
+ if (lag_duration_us > _max_lag_duration_us) {
+ elog("Target tps lagging outside of defined limits. Terminating test");
+ elog("Expected=${expected}, Sent=${sent}, Percent off=${per_off}, Violation start=${vstart} ",
+ ("expected", stats.expected_sent)
+ ("sent", stats.trxs_sent)
+ ("per_off", per_off)
+ ("vstart", _violation_start_time));
+ _terminated_early = true;
+ return false;
+ }
+ } else {
+ _violation_start_time.emplace(stats.last_run);
+ }
+ } else {
+ if (_violation_start_time.has_value()) {
+ _violation_start_time.reset();
+ }
+ }
+ return true;
+ }
+}
diff --git a/tests/trx_generator/trx_provider.hpp b/tests/trx_generator/trx_provider.hpp
new file mode 100644
index 0000000..c7debb4
--- /dev/null
+++ b/tests/trx_generator/trx_provider.hpp
@@ -0,0 +1,248 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace std::chrono_literals;
+
+namespace eosio::testing {
+ using send_buffer_type = std::shared_ptr>;
+
+ struct logged_trx_data {
+ eosio::chain::transaction_id_type _trx_id;
+ fc::time_point _timestamp;
+
+ explicit logged_trx_data(eosio::chain::transaction_id_type trx_id, fc::time_point time_of_interest=fc::time_point::now()) :
+ _trx_id(trx_id), _timestamp(time_of_interest) {}
+ };
+
+ struct provider_base_config {
+ std::string _peer_endpoint_type = "p2p";
+ std::string _peer_endpoint = "127.0.0.1";
+ unsigned short _port = 9876;
+ // Api endpoint not truly used for p2p connections as transactions are streamed directly to p2p endpoint
+ std::string _api_endpoint = "/v1/chain/send_transaction2";
+
+ std::string to_string() const {
+ std::ostringstream ss;
+ ss << "Provider base config endpoint type: " << _peer_endpoint_type << " peer_endpoint: " << _peer_endpoint
+ << " port: " << _port << " api endpoint: " << _api_endpoint;
+ return ss.str();
+ }
+ };
+
+ struct acked_trx_trace_info {
+ bool _valid = false;
+ uint32_t _block_num = 0;
+ uint32_t _cpu_usage_us = 0;
+ uint32_t _net_usage_words = 0;
+ std::string _block_time = "";
+
+ std::string to_string() const {
+ std::ostringstream ss;
+ ss << "Acked Transaction Trace Info "
+ << "valid: " << _valid << " block num: " << _block_num << " cpu usage us: " << _cpu_usage_us
+ << " net usage words: " << _net_usage_words << " block time: " << _block_time;
+ return ss.str();
+ }
+ };
+
+ struct provider_connection {
+ const provider_base_config& _config;
+ eosio::chain::named_thread_pool _connection_thread_pool;
+
+ std::mutex _trx_ack_map_lock;
+ std::map _trxs_ack_time_map;
+
+ explicit provider_connection(const provider_base_config& provider_config)
+ : _config(provider_config) {}
+
+ virtual ~provider_connection() = default;
+
+ void init_and_connect();
+ void cleanup_and_disconnect();
+ fc::time_point get_trx_ack_time(const eosio::chain::transaction_id_type& trx_id);
+ void trx_acknowledged(const eosio::chain::transaction_id_type& trx_id, const fc::time_point& ack_time);
+
+ virtual acked_trx_trace_info get_acked_trx_trace_info(const eosio::chain::transaction_id_type& trx_id) = 0;
+ virtual void send_transaction(const chain::packed_transaction& trx) = 0;
+
+ private:
+ virtual void connect() = 0;
+ virtual void disconnect() = 0;
+ };
+
+ struct http_connection : public provider_connection {
+ std::mutex _trx_info_map_lock;
+ std::map _acked_trx_trace_info_map;
+
+ std::atomic _acknowledged{0};
+ std::atomic _sent{0};
+
+ explicit http_connection(const provider_base_config& provider_config)
+ : provider_connection(provider_config) {}
+
+ void send_transaction(const chain::packed_transaction& trx) final;
+ void record_trx_info(const eosio::chain::transaction_id_type& trx_id, uint32_t block_num, uint32_t cpu_usage_us,
+ uint32_t net_usage_words, const std::string& block_time);
+ acked_trx_trace_info get_acked_trx_trace_info(const eosio::chain::transaction_id_type& trx_id) override final;
+
+ private:
+ void connect() override final;
+ void disconnect() override final;
+ bool needs_response_trace_info();
+ bool is_read_only_transaction();
+ };
+
+ struct p2p_connection : public provider_connection {
+ boost::asio::ip::tcp::socket _p2p_socket;
+
+ explicit p2p_connection(const provider_base_config& provider_config)
+ : provider_connection(provider_config)
+ , _p2p_socket(_connection_thread_pool.get_executor()) {}
+
+ void send_transaction(const chain::packed_transaction& trx) final;
+
+ acked_trx_trace_info get_acked_trx_trace_info(const eosio::chain::transaction_id_type& trx_id) override final;
+
+ private:
+ void connect() override final;
+ void disconnect() override final;
+ };
+
+ struct trx_provider {
+ explicit trx_provider(const provider_base_config& provider_config);
+
+ void setup();
+ void send(const chain::signed_transaction& trx);
+ void log_trxs(const std::string& log_dir);
+ void teardown();
+
+ private:
+ std::variant _conn;
+ provider_connection* _peer_connection;
+ std::vector _sent_trx_data;
+ };
+
+ struct tps_test_stats {
+ uint32_t total_trxs = 0;
+ uint32_t trxs_left = 0;
+ uint32_t trxs_sent = 0;
+ fc::time_point start_time;
+ fc::time_point expected_end_time;
+ fc::time_point last_run;
+ fc::time_point next_run;
+ int64_t time_to_next_trx_us = 0;
+ fc::microseconds trx_interval;
+ uint32_t expected_sent;
+ };
+
+ constexpr int64_t min_sleep_us = 1;
+ constexpr int64_t default_spin_up_time_us = std::chrono::microseconds(1s).count();
+ constexpr uint32_t default_max_lag_per = 5;
+ constexpr int64_t default_max_lag_duration_us = std::chrono::microseconds(1s).count();
+
+ struct null_tps_monitor {
+ bool monitor_test(const tps_test_stats& stats) {return true;}
+ };
+
+ struct tps_performance_monitor {
+ fc::microseconds _spin_up_time;
+ uint32_t _max_lag_per;
+ fc::microseconds _max_lag_duration_us;
+ bool _terminated_early;
+ std::optional _violation_start_time;
+
+ explicit tps_performance_monitor(int64_t spin_up_time=default_spin_up_time_us, uint32_t max_lag_per=default_max_lag_per,
+ int64_t max_lag_duration_us=default_max_lag_duration_us) : _spin_up_time(spin_up_time),
+ _max_lag_per(max_lag_per), _max_lag_duration_us(max_lag_duration_us), _terminated_early(false) {}
+
+ bool monitor_test(const tps_test_stats& stats);
+ bool terminated_early() {return _terminated_early;}
+ };
+
+ struct trx_tps_tester_config {
+ uint32_t _gen_duration_seconds;
+ uint32_t _target_tps;
+
+ std::string to_string() const {
+ std::ostringstream ss;
+ ss << "Trx Tps Tester Config: duration: " << _gen_duration_seconds << " target tps: " << _target_tps;
+ return ss.str();
+ };
+ };
+
+ template
+ struct trx_tps_tester {
+ std::shared_ptr _generator;
+ std::shared_ptr _monitor;
+ trx_tps_tester_config _config;
+
+ explicit trx_tps_tester(std::shared_ptr generator, std::shared_ptr monitor, const trx_tps_tester_config& tester_config) :
+ _generator(generator), _monitor(monitor), _config(tester_config) {
+ }
+
+ bool run() {
+ if ((_config._target_tps) < 1 || (_config._gen_duration_seconds < 1)) {
+ elog("target tps (${tps}) and duration (${dur}) must both be 1+", ("tps", _config._target_tps)("dur", _config._gen_duration_seconds));
+ return false;
+ }
+
+ if (!_generator->setup()) {
+ return false;
+ }
+
+ tps_test_stats stats;
+ stats.trx_interval = fc::microseconds(std::chrono::microseconds(1s).count() / _config._target_tps);
+
+ stats.total_trxs = _config._gen_duration_seconds * _config._target_tps;
+ stats.trxs_left = stats.total_trxs;
+ stats.start_time = fc::time_point::now();
+ stats.expected_end_time = stats.start_time + fc::microseconds{_config._gen_duration_seconds * std::chrono::microseconds(1s).count()};
+ stats.time_to_next_trx_us = 0;
+
+ bool keep_running = true;
+
+ while (keep_running) {
+ stats.last_run = fc::time_point::now();
+ stats.next_run = stats.start_time + fc::microseconds(stats.trx_interval.count() * (stats.trxs_sent+1));
+
+ if (_generator->generate_and_send()) {
+ stats.trxs_sent++;
+ } else {
+ elog("generator unable to create/send a transaction");
+ if (_generator->stop_on_trx_fail()) {
+ elog("generator stopping due to trx failure to send.");
+ break;
+ }
+ }
+
+ stats.expected_sent = ((stats.last_run - stats.start_time).count() / stats.trx_interval.count()) +1;
+ stats.trxs_left--;
+
+ keep_running = ((_monitor == nullptr || _monitor->monitor_test(stats)) && stats.trxs_left);
+
+ if (keep_running) {
+ fc::microseconds time_to_sleep{stats.next_run - fc::time_point::now()};
+ if (time_to_sleep.count() >= min_sleep_us) {
+ std::this_thread::sleep_for(std::chrono::microseconds(time_to_sleep.count()));
+ }
+ stats.time_to_next_trx_us = time_to_sleep.count();
+ }
+ }
+
+ _generator->tear_down();
+
+ return true;
+ }
+ };
+}
\ No newline at end of file