diff --git a/cmd/capi/execute.cpp b/cmd/capi/execute.cpp index 00e2336d06..46640b29d4 100644 --- a/cmd/capi/execute.cpp +++ b/cmd/capi/execute.cpp @@ -396,7 +396,8 @@ int start_rpcdaemon(SilkwormHandle handle, const rpc::DaemonSettings& /*settings .exclusive = true}; ::mdbx::env_managed env{silkworm::db::open_env(config)}; - const int status_code{silkworm_start_rpcdaemon(handle, &*env)}; + SilkwormRpcSettings settings{}; + const int status_code{silkworm_start_rpcdaemon(handle, &*env, &settings)}; if (status_code != SILKWORM_OK) { SILK_ERROR << "silkworm_start_rpcdaemon failed [code=" << std::to_string(status_code) << "]"; } diff --git a/silkworm/capi/common.cpp b/silkworm/capi/common.cpp new file mode 100644 index 0000000000..eca76bbe0f --- /dev/null +++ b/silkworm/capi/common.cpp @@ -0,0 +1,66 @@ +/* + Copyright 2024 The Silkworm Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include "common.hpp" + +#include + +namespace log = silkworm::log; + +//! Build Silkworm log level from its C representation +static log::Level make_log_level(const SilkwormLogLevel c_log_level) { + log::Level verbosity{}; + switch (c_log_level) { + case SilkwormLogLevel::NONE: + verbosity = log::Level::kNone; + break; + case SilkwormLogLevel::CRITICAL: + verbosity = log::Level::kCritical; + break; + case SilkwormLogLevel::ERROR: + verbosity = log::Level::kError; + break; + case SilkwormLogLevel::WARNING: + verbosity = log::Level::kWarning; + break; + case SilkwormLogLevel::INFO: + verbosity = log::Level::kInfo; + break; + case SilkwormLogLevel::DEBUG: + verbosity = log::Level::kDebug; + break; + case SilkwormLogLevel::TRACE: + verbosity = log::Level::kTrace; + break; + } + return verbosity; +} + +std::filesystem::path parse_path(const char data_dir_path[SILKWORM_PATH_SIZE]) { + // Treat as char8_t so that filesystem::path assumes UTF-8 encoding of the input path + auto begin = reinterpret_cast(data_dir_path); + size_t len = strnlen(data_dir_path, SILKWORM_PATH_SIZE); + return std::filesystem::path{begin, begin + len}; +} + +log::Settings make_log_settings(const SilkwormLogLevel c_log_level) { + return { + .log_utc = false, // display local time + .log_timezone = false, // no timezone ID + .log_trim = true, // compact rendering (i.e. no whitespaces) + .log_verbosity = make_log_level(c_log_level), + }; +} diff --git a/silkworm/capi/common.hpp b/silkworm/capi/common.hpp new file mode 100644 index 0000000000..fca231ee12 --- /dev/null +++ b/silkworm/capi/common.hpp @@ -0,0 +1,29 @@ +/* + Copyright 2024 The Silkworm Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include + +#include + +#include "silkworm.h" + +//! Build a file system path from its C null-terminated upper-bounded representation +std::filesystem::path parse_path(const char path[SILKWORM_PATH_SIZE]); + +//! Build log configuration matching Erigon log format w/ custom verbosity level +silkworm::log::Settings make_log_settings(SilkwormLogLevel c_log_level); diff --git a/silkworm/capi/instance.hpp b/silkworm/capi/instance.hpp index cee1920d73..48fd764244 100644 --- a/silkworm/capi/instance.hpp +++ b/silkworm/capi/instance.hpp @@ -23,10 +23,12 @@ #include #include +#include #include #include struct SilkwormInstance { + silkworm::log::Settings log_settings; silkworm::concurrency::ContextPoolSettings context_pool_settings; std::filesystem::path data_dir_path; std::unique_ptr snapshot_repository; diff --git a/silkworm/capi/rpcdaemon.cpp b/silkworm/capi/rpcdaemon.cpp new file mode 100644 index 0000000000..8ab639ecbc --- /dev/null +++ b/silkworm/capi/rpcdaemon.cpp @@ -0,0 +1,154 @@ +/* + Copyright 2024 The Silkworm Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include +#include + +#include "common.hpp" +#include "instance.hpp" +#include "silkworm.h" + +using namespace silkworm; +using namespace silkworm::rpc; +using silkworm::concurrency::ContextPoolSettings; + +//! Build interface log settings for ETH JSON-RPC from their C representation +static InterfaceLogSettings make_eth_ifc_log_settings(const struct SilkwormRpcInterfaceLogSettings settings) { + InterfaceLogSettings eth_ifc_log_settings{.ifc_name = "eth_rpc_api"}; + eth_ifc_log_settings.enabled = settings.enabled; + eth_ifc_log_settings.container_folder = parse_path(settings.container_folder); + eth_ifc_log_settings.max_file_size_mb = settings.max_file_size_mb; + eth_ifc_log_settings.max_files = settings.max_files; + eth_ifc_log_settings.dump_response = settings.dump_response; + return eth_ifc_log_settings; +} + +//! Build JSON-RPC endpoint from C settings +static std::string parse_end_point(const char (&c_host)[SILKWORM_RPC_SETTINGS_HOST_SIZE], int port, const std::string& default_end_point) { + auto host = std::string{c_host}; + if (host.empty() && port == 0) { + return kDefaultEth1EndPoint; + } + const auto semicolon_position{default_end_point.find(':')}; + SILKWORM_ASSERT(semicolon_position != std::string::npos); + if (host.empty()) { + host = default_end_point.substr(0, semicolon_position); + } + if (port == 0) { + port = std::stoi(default_end_point.substr(semicolon_position + 1)); + } + std::string eth_end_point{host + ":" + std::to_string(port)}; + return eth_end_point; +} + +//! Build list of CORS domains from their C representation +static std::vector parse_cors_domains( + const char (&c_cors_domains)[SILKWORM_RPC_SETTINGS_CORS_DOMAINS_MAX][SILKWORM_RPC_SETTINGS_CORS_DOMAIN_SIZE]) { + std::vector cors_domains; + for (const auto& c_domain : c_cors_domains) { + std::string_view domain_str = c_domain; + if (domain_str.empty()) break; + cors_domains.emplace_back(domain_str); + } + return cors_domains; +} + +//! Build whole RPC daemon settings from their C representation +static DaemonSettings make_daemon_settings(SilkwormHandle handle, const struct SilkwormRpcSettings& settings) { + const auto jwt_path{parse_path(settings.jwt_file_path)}; + return { + .log_settings = handle->log_settings, + .eth_ifc_log_settings = make_eth_ifc_log_settings(settings.eth_if_log_settings), + .context_pool_settings = handle->context_pool_settings, + .eth_end_point = parse_end_point(settings.eth_api_host, settings.eth_api_port, kDefaultEth1EndPoint), + .engine_end_point = "", // disable end-point for Engine RPC API + .eth_api_spec = std::string{settings.eth_api_spec}, + .num_workers = settings.num_workers > 0 ? settings.num_workers : kDefaultNumWorkers, + .cors_domain = parse_cors_domains(settings.cors_domains), + .jwt_secret_file = jwt_path.empty() ? std::nullopt : std::make_optional(jwt_path.string()), + .skip_protocol_check = settings.skip_internal_protocol_check, + .erigon_json_rpc_compatibility = settings.erigon_json_rpc_compatibility, + .use_websocket = settings.ws_enabled, + .ws_compression = settings.ws_compression, + .http_compression = settings.http_compression, + }; +} + +SILKWORM_EXPORT int silkworm_start_rpcdaemon(SilkwormHandle handle, MDBX_env* env, const struct SilkwormRpcSettings* settings) SILKWORM_NOEXCEPT { + if (!handle) { + return SILKWORM_INVALID_HANDLE; + } + if (handle->rpcdaemon) { + return SILKWORM_SERVICE_ALREADY_STARTED; + } + if (!env) { + return SILKWORM_INVALID_MDBX_ENV; + } + if (!settings) { + return SILKWORM_INVALID_SETTINGS; + } + + auto daemon_settings = make_daemon_settings(handle, *settings); + db::EnvUnmanaged unmanaged_env{env}; + + // Create the one-and-only Silkrpc daemon + handle->rpcdaemon = std::make_unique(daemon_settings, std::make_optional(unmanaged_env)); + + // Check protocol version compatibility with Core Services + if (!daemon_settings.skip_protocol_check) { + SILK_INFO << "[Silkworm RPC] Checking protocol version compatibility with core services..."; + + const auto checklist = handle->rpcdaemon->run_checklist(); + for (const auto& protocol_check : checklist.protocol_checklist) { + SILK_INFO << protocol_check.result; + } + checklist.success_or_throw(); + } else { + SILK_TRACE << "[Silkworm RPC] Skip protocol version compatibility check with core services"; + } + + SILK_INFO << "[Silkworm RPC] Starting ETH API at " << daemon_settings.eth_end_point; + try { + handle->rpcdaemon->start(); + } catch (const std::exception& ex) { + SILK_ERROR << "[Silkworm RPC] Cannot start RPC daemon due to: " << ex.what(); + return SILKWORM_INTERNAL_ERROR; + } + + return SILKWORM_OK; +} + +SILKWORM_EXPORT int silkworm_stop_rpcdaemon(SilkwormHandle handle) SILKWORM_NOEXCEPT { + if (!handle) { + return SILKWORM_INVALID_HANDLE; + } + if (!handle->rpcdaemon) { + return SILKWORM_OK; + } + + try { + handle->rpcdaemon->stop(); + SILK_INFO << "[Silkworm RPC] Exiting..."; + handle->rpcdaemon->join(); + SILK_INFO << "[Silkworm RPC] Stopped"; + handle->rpcdaemon.reset(); + } catch (const std::exception& ex) { + SILK_ERROR << "[Silkworm RPC] Cannot stop RPC daemon due to: " << ex.what(); + return SILKWORM_INTERNAL_ERROR; + } + + return SILKWORM_OK; +} diff --git a/silkworm/capi/silkworm.cpp b/silkworm/capi/silkworm.cpp index e3cc1141ba..e041bb4a0d 100644 --- a/silkworm/capi/silkworm.cpp +++ b/silkworm/capi/silkworm.cpp @@ -42,11 +42,12 @@ #include #include #include -#include #include +#include #include #include +#include "common.hpp" #include "instance.hpp" using namespace std::chrono_literals; @@ -56,12 +57,6 @@ static MemoryMappedRegion make_region(const SilkwormMemoryMappedFile& mmf) { return {mmf.memory_address, mmf.memory_length}; } -//! Log configuration matching Erigon log format -static log::Settings kLogSettingsLikeErigon{ - .log_utc = false, // display local time - .log_timezone = false, // no timezone ID - .log_trim = true, // compact rendering (i.e. no whitespaces) -}; static constexpr size_t kMaxBlockBufferSize{100}; static constexpr size_t kAnalysisCacheSize{5'000}; static constexpr size_t kMaxPrefetchedBlocks{10'240}; @@ -140,13 +135,6 @@ static log::Args log_args_for_exec_commit(StopWatch::Duration elapsed, const std std::to_string(Directory{db_path}.size())}; } -static std::filesystem::path make_path(const char data_dir_path[SILKWORM_PATH_SIZE]) { - // treat as char8_t so that filesystem::path assumes UTF-8 encoding of the input path - auto begin = reinterpret_cast(data_dir_path); - size_t len = strnlen(data_dir_path, SILKWORM_PATH_SIZE); - return std::filesystem::path{begin, begin + len}; -} - //! Generate log arguments for execution progress at specified block static log::Args log_args_for_exec_progress(ExecutionProgress& progress, uint64_t current_block) { static auto float_to_string = [](float f) -> std::string { @@ -212,7 +200,8 @@ SILKWORM_EXPORT int silkworm_init(SilkwormHandle* handle, const struct SilkwormS is_initialized = true; - log::init(kLogSettingsLikeErigon); + log::Settings log_settings{make_log_settings(settings->log_verbosity)}; + log::init(log_settings); log::Info{"Silkworm build info", log_args_for_version()}; // NOLINT(*-unused-raii) auto snapshot_repository = std::make_unique(); @@ -220,12 +209,15 @@ SILKWORM_EXPORT int silkworm_init(SilkwormHandle* handle, const struct SilkwormS // NOLINTNEXTLINE(bugprone-unhandled-exception-at-new) *handle = new SilkwormInstance{ - {}, // context_pool_settings - make_path(settings->data_dir_path), - std::move(snapshot_repository), - {}, // rpcdaemon unique_ptr - {}, // sentry_thread unique_ptr - {}, // sentry_stop_signal + .log_settings = std::move(log_settings), + .context_pool_settings = { + .num_contexts = settings->num_contexts > 0 ? settings->num_contexts : concurrency::kDefaultNumContexts, + }, + .data_dir_path = parse_path(settings->data_dir_path), + .snapshot_repository = std::move(snapshot_repository), + .rpcdaemon = {}, + .sentry_thread = {}, + .sentry_stop_signal = {}, }; return SILKWORM_OK; } @@ -390,62 +382,6 @@ SILKWORM_EXPORT const char* silkworm_libmdbx_version() SILKWORM_NOEXCEPT { return ::mdbx::get_version().git.describe; } -SILKWORM_EXPORT int silkworm_start_rpcdaemon(SilkwormHandle handle, MDBX_env* env) SILKWORM_NOEXCEPT { - if (!handle) { - return SILKWORM_INVALID_HANDLE; - } - if (handle->rpcdaemon) { - return SILKWORM_SERVICE_ALREADY_STARTED; - } - - db::EnvUnmanaged unmanaged_env{env}; - - // TODO(canepat) add RPC options in API and convert them - rpc::DaemonSettings settings{ - .engine_end_point = "", // disable end-point for Engine RPC API - .skip_protocol_check = true, - .erigon_json_rpc_compatibility = true, - }; - - // Create the one-and-only Silkrpc daemon - handle->rpcdaemon = std::make_unique(settings, std::make_optional(unmanaged_env)); - - // Check protocol version compatibility with Core Services - if (!settings.skip_protocol_check) { - SILK_INFO << "[Silkworm RPC] Checking protocol version compatibility with core services..."; - - const auto checklist = handle->rpcdaemon->run_checklist(); - for (const auto& protocol_check : checklist.protocol_checklist) { - SILK_INFO << protocol_check.result; - } - checklist.success_or_throw(); - } else { - SILK_TRACE << "[Silkworm RPC] Skip protocol version compatibility check with core services"; - } - - SILK_INFO << "[Silkworm RPC] Starting ETH API at " << settings.eth_end_point; - handle->rpcdaemon->start(); - - return SILKWORM_OK; -} - -SILKWORM_EXPORT int silkworm_stop_rpcdaemon(SilkwormHandle handle) SILKWORM_NOEXCEPT { - if (!handle) { - return SILKWORM_INVALID_HANDLE; - } - if (!handle->rpcdaemon) { - return SILKWORM_OK; - } - - handle->rpcdaemon->stop(); - SILK_INFO << "[Silkworm RPC] Exiting..."; - handle->rpcdaemon->join(); - SILK_INFO << "[Silkworm RPC] Stopped"; - handle->rpcdaemon.reset(); - - return SILKWORM_OK; -} - class BlockProvider { static constexpr size_t kTxnRefreshThreshold{100}; diff --git a/silkworm/capi/silkworm.h b/silkworm/capi/silkworm.h index f40eda52b4..de7de03472 100644 --- a/silkworm/capi/silkworm.h +++ b/silkworm/capi/silkworm.h @@ -98,7 +98,24 @@ struct SilkwormChainSnapshot { #define SILKWORM_PATH_SIZE 260 #define SILKWORM_GIT_VERSION_SIZE 32 +//! Silkworm library logging level +//! \note using anonymous enum seems the only way to obtain enum type in Cgo generated code +typedef enum : uint8_t { + NONE, + CRITICAL, + ERROR, + WARNING, + INFO, + DEBUG, + TRACE +} SilkwormLogLevel; + +//! Silkworm library general configuration options struct SilkwormSettings { + //! Log verbosity level + SilkwormLogLevel log_verbosity; + //! Number of I/O contexts to use in concurrency mode + uint32_t num_contexts; //! Data directory path in UTF-8. char data_dir_path[SILKWORM_PATH_SIZE]; //! libmdbx version string in git describe format. @@ -138,13 +155,56 @@ SILKWORM_EXPORT int silkworm_add_snapshot(SilkwormHandle handle, struct Silkworm */ SILKWORM_EXPORT const char* silkworm_libmdbx_version() SILKWORM_NOEXCEPT; +#define SILKWORM_RPC_SETTINGS_HOST_SIZE 128 +#define SILKWORM_RPC_SETTINGS_API_NAMESPACE_SPEC_SIZE 256 +#define SILKWORM_RPC_SETTINGS_CORS_DOMAINS_MAX 20 +#define SILKWORM_RPC_SETTINGS_CORS_DOMAIN_SIZE 256 + +//! Silkworm RPC interface log options +struct SilkwormRpcInterfaceLogSettings { + bool enabled; + char container_folder[SILKWORM_PATH_SIZE]; + uint16_t max_file_size_mb; + uint16_t max_files; + bool dump_response; +}; + +//! Silkworm RPC configuration options +struct SilkwormRpcSettings { + //! Configuration options for interface log of ETH JSON-RPC end-point + struct SilkwormRpcInterfaceLogSettings eth_if_log_settings; + //! Host address for ETH JSON-RPC end-point + char eth_api_host[SILKWORM_RPC_SETTINGS_HOST_SIZE]; + //! Listening port number for ETH JSON-RPC end-point + uint16_t eth_api_port; + //! ETH JSON-RPC namespace specification (comma-separated list of API namespaces) + char eth_api_spec[SILKWORM_RPC_SETTINGS_API_NAMESPACE_SPEC_SIZE]; + //! Number of threads in worker pool (for long-run tasks) + uint32_t num_workers; + //! Array of CORS domains + char cors_domains[SILKWORM_RPC_SETTINGS_CORS_DOMAINS_MAX][SILKWORM_RPC_SETTINGS_CORS_DOMAIN_SIZE]; + //! Path to the JWT file in UTF-8. + char jwt_file_path[SILKWORM_PATH_SIZE]; + //! Flag indicating if JSON-RPC strict compatibility w/ Erigon is supported + bool erigon_json_rpc_compatibility; + //! Flag indicating if WebSocket support is enabled + bool ws_enabled; + //! Flag indicating if compression of WebSocket messages is supported + bool ws_compression; + //! Flag indicating if compression of HTTP messages is supported + bool http_compression; + //! Flag indicating if version check on internal gRPC protocols should be skipped + bool skip_internal_protocol_check; +}; + /** * \brief Start Silkworm RPC daemon. - * \param[in] handle A valid Silkworm instance handle, got with silkworm_init.Must not be zero. + * \param[in] handle A valid Silkworm instance handle, got with silkworm_init. Must not be zero. * \param[in] env An valid MDBX environment. Must not be zero. + * \param[in] settings The RPC daemon configuration settings. Must not be zero. * \return SILKWORM_OK (=0) on success, a non-zero error value on failure. */ -SILKWORM_EXPORT int silkworm_start_rpcdaemon(SilkwormHandle handle, MDBX_env* env) SILKWORM_NOEXCEPT; +SILKWORM_EXPORT int silkworm_start_rpcdaemon(SilkwormHandle handle, MDBX_env* env, const struct SilkwormRpcSettings* settings) SILKWORM_NOEXCEPT; /** * \brief Stop Silkworm RPC daemon and wait for its termination. @@ -159,6 +219,7 @@ SILKWORM_EXPORT int silkworm_stop_rpcdaemon(SilkwormHandle handle) SILKWORM_NOEX #define SILKWORM_SENTRY_SETTINGS_PEERS_MAX 128 #define SILKWORM_SENTRY_SETTINGS_PEER_URL_SIZE 200 +//! Silkworm Sentry configuration options struct SilkwormSentrySettings { char client_id[SILKWORM_SENTRY_SETTINGS_CLIENT_ID_SIZE]; uint16_t api_port; diff --git a/silkworm/capi/silkworm_test.cpp b/silkworm/capi/silkworm_test.cpp index a925f6d059..2de45ee39d 100644 --- a/silkworm/capi/silkworm_test.cpp +++ b/silkworm/capi/silkworm_test.cpp @@ -31,7 +31,6 @@ #include #include #include -#include #include namespace silkworm { @@ -40,14 +39,7 @@ namespace snapshot_test = snapshots::test_util; struct CApiTest : public rpc::test::TestDatabaseContext { TemporaryDirectory tmp_dir; - - private: - // TODO(canepat) remove test_util::StreamSwap objects when C API settings include log level - std::stringstream string_cout, string_cerr; - test_util::StreamSwap cout_swap{std::cout, string_cout}; - test_util::StreamSwap cerr_swap{std::cerr, string_cerr}; - - test_util::SetLogVerbosityGuard log_guard{log::Level::kNone}; + SilkwormSettings settings{.log_verbosity = SilkwormLogLevel::NONE}; }; //! Utility to copy `src` C-string to `dst` fixed-size char array @@ -72,14 +64,12 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_libmdbx_version: OK", "[silkworm][capi } TEST_CASE_METHOD(CApiTest, "CAPI silkworm_init: empty settings", "[silkworm][capi]") { - SilkwormSettings settings{}; SilkwormHandle handle{nullptr}; CHECK(silkworm_init(&handle, &settings) == SILKWORM_INVALID_PATH); CHECK(!handle); } TEST_CASE_METHOD(CApiTest, "CAPI silkworm_init: empty data folder path", "[silkworm][capi]") { - SilkwormSettings settings{}; copy_path(settings.data_dir_path, ""); copy_git_version(settings.libmdbx_version, silkworm_libmdbx_version()); SilkwormHandle handle{nullptr}; @@ -88,7 +78,6 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_init: empty data folder path", "[silkw } TEST_CASE_METHOD(CApiTest, "CAPI silkworm_init: empty MDBX version", "[silkworm][capi]") { - SilkwormSettings settings{}; copy_path(settings.data_dir_path, db.get_path().string().c_str()); copy_git_version(settings.libmdbx_version, ""); SilkwormHandle handle{nullptr}; @@ -97,7 +86,6 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_init: empty MDBX version", "[silkworm] } TEST_CASE_METHOD(CApiTest, "CAPI silkworm_init: incompatible MDBX version", "[silkworm][capi]") { - SilkwormSettings settings{}; copy_path(settings.data_dir_path, db.get_path().string().c_str()); copy_git_version(settings.libmdbx_version, "v0.1.0"); SilkwormHandle handle{nullptr}; @@ -106,7 +94,6 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_init: incompatible MDBX version", "[si } TEST_CASE_METHOD(CApiTest, "CAPI silkworm_init: OK", "[silkworm][capi]") { - SilkwormSettings settings{}; copy_path(settings.data_dir_path, db.get_path().string().c_str()); copy_git_version(settings.libmdbx_version, silkworm_libmdbx_version()); SilkwormHandle handle{nullptr}; @@ -121,7 +108,6 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_fini: not initialized", "[silkworm][ca } TEST_CASE_METHOD(CApiTest, "CAPI silkworm_fini: OK", "[silkworm][capi]") { - SilkwormSettings settings{}; copy_path(settings.data_dir_path, db.get_path().string().c_str()); copy_git_version(settings.libmdbx_version, silkworm_libmdbx_version()); SilkwormHandle handle{nullptr}; @@ -133,7 +119,7 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_fini: OK", "[silkworm][capi]") { //! \note This is useful for tests that do *not* specifically play with silkworm_init/silkworm_fini or invalid handles struct SilkwormLibrary { explicit SilkwormLibrary(const std::filesystem::path& db_path) { - SilkwormSettings settings{}; + SilkwormSettings settings{.log_verbosity = SilkwormLogLevel::NONE}; copy_path(settings.data_dir_path, db_path.string().c_str()); copy_git_version(settings.libmdbx_version, silkworm_libmdbx_version()); silkworm_init(&handle_, &settings); @@ -155,7 +141,7 @@ struct SilkwormLibrary { uint64_t batch_size, bool write_change_sets, bool write_receipts, - bool write_call_traces) { + bool write_call_traces) const { ExecutionResult result; result.execute_block_result = silkworm_execute_blocks_ephemeral(handle_, txn, @@ -172,7 +158,7 @@ struct SilkwormLibrary { uint64_t batch_size, bool write_change_sets, bool write_receipts, - bool write_call_traces) { + bool write_call_traces) const { ExecutionResult result; result.execute_block_result = silkworm_execute_blocks_perpetual(handle_, env, @@ -182,10 +168,18 @@ struct SilkwormLibrary { return result; } - int add_snapshot(SilkwormChainSnapshot* snapshot) { + int add_snapshot(SilkwormChainSnapshot* snapshot) const { return silkworm_add_snapshot(handle_, snapshot); } + int start_rpcdaemon(MDBX_env* env, const SilkwormRpcSettings* settings) const { + return silkworm_start_rpcdaemon(handle_, env, settings); + } + + int stop_rpcdaemon() const { + return silkworm_stop_rpcdaemon(handle_); + } + private: SilkwormHandle handle_{nullptr}; }; @@ -792,4 +786,79 @@ TEST_CASE_METHOD(CApiTest, "CAPI silkworm_add_snapshot", "[silkworm][capi]") { } } +static SilkwormRpcSettings make_rpc_settings_for_test(uint16_t api_listening_port) { + SilkwormRpcSettings settings{ + .eth_if_log_settings = { + .enabled = false, + .max_file_size_mb = 1, + .max_files = 1, + .dump_response = false, + }, + .eth_api_port = api_listening_port, + .num_workers = 0, + .erigon_json_rpc_compatibility = false, + .ws_enabled = false, + .ws_compression = false, + .http_compression = false, + // We must skip internal protocol check here (would block because gRPC server not present) + .skip_internal_protocol_check = true, + }; + (void)std::snprintf(settings.eth_if_log_settings.container_folder, SILKWORM_PATH_SIZE, "logs"); + (void)std::snprintf(settings.eth_api_host, SILKWORM_RPC_SETTINGS_HOST_SIZE, "localhost"); + (void)std::snprintf(settings.eth_api_spec, SILKWORM_RPC_SETTINGS_API_NAMESPACE_SPEC_SIZE, "eth,ots"); + for (auto& domain : settings.cors_domains) { + domain[0] = 0; + } + (void)std::snprintf(settings.cors_domains[0], SILKWORM_RPC_SETTINGS_CORS_DOMAIN_SIZE, "*"); + (void)std::snprintf(settings.jwt_file_path, SILKWORM_PATH_SIZE, "jwt.hex"); + return settings; +} + +static const SilkwormRpcSettings kInvalidRpcSettings{make_rpc_settings_for_test(10)}; +static const SilkwormRpcSettings kValidRpcSettings{make_rpc_settings_for_test(8545)}; + +TEST_CASE_METHOD(CApiTest, "CAPI silkworm_start_rpcdaemon", "[silkworm][capi]") { + SECTION("invalid handle") { + // We purposely do not call silkworm_init to provide a null handle + SilkwormHandle handle{nullptr}; + CHECK(silkworm_start_rpcdaemon(handle, db, &kValidRpcSettings) == SILKWORM_INVALID_HANDLE); + } + + // Use Silkworm as a library with silkworm_init/silkworm_fini automated by RAII + SilkwormLibrary silkworm_lib{db.get_path()}; + + SECTION("invalid settings") { + CHECK(silkworm_lib.start_rpcdaemon(db, nullptr) == SILKWORM_INVALID_SETTINGS); + } + + SECTION("test settings: invalid port") { + CHECK(silkworm_lib.start_rpcdaemon(db, &kInvalidRpcSettings) == SILKWORM_INTERNAL_ERROR); + } + + SECTION("test settings: valid port") { + CHECK(silkworm_lib.start_rpcdaemon(db, &kValidRpcSettings) == SILKWORM_OK); + REQUIRE(silkworm_lib.stop_rpcdaemon() == SILKWORM_OK); + } +} + +TEST_CASE_METHOD(CApiTest, "CAPI silkworm_stop_rpcdaemon", "[silkworm][capi]") { + SECTION("invalid handle") { + // We purposely do not call silkworm_init to provide a null handle + SilkwormHandle handle{nullptr}; + CHECK(silkworm_stop_rpcdaemon(handle) == SILKWORM_INVALID_HANDLE); + } + + // Use Silkworm as a library with silkworm_init/silkworm_fini automated by RAII + SilkwormLibrary silkworm_lib{db.get_path()}; + + SECTION("not yet started") { + CHECK(silkworm_lib.stop_rpcdaemon() == SILKWORM_OK); + } + + SECTION("already started") { + REQUIRE(silkworm_lib.start_rpcdaemon(db, &kValidRpcSettings) == SILKWORM_OK); + CHECK(silkworm_lib.stop_rpcdaemon() == SILKWORM_OK); + } +} + } // namespace silkworm diff --git a/silkworm/infra/concurrency/context_pool_settings.hpp b/silkworm/infra/concurrency/context_pool_settings.hpp index ec48049d43..c6fd7dcaa8 100644 --- a/silkworm/infra/concurrency/context_pool_settings.hpp +++ b/silkworm/infra/concurrency/context_pool_settings.hpp @@ -20,10 +20,13 @@ namespace silkworm::concurrency { +//! Default number of threads to use for I/O tasks +inline const auto kDefaultNumContexts{std::thread::hardware_concurrency() / 2}; + //! The configuration settings for \refitem ContextPool struct ContextPoolSettings { - uint32_t num_contexts{std::thread::hardware_concurrency() / 2}; // The number of execution contexts to activate - WaitMode wait_mode{WaitMode::blocking}; // The waiting strategy when context has no work + uint32_t num_contexts{kDefaultNumContexts}; // The number of execution contexts to activate + WaitMode wait_mode{WaitMode::blocking}; // The waiting strategy when context has no work }; } // namespace silkworm::concurrency diff --git a/silkworm/rpc/common/worker_pool.hpp b/silkworm/rpc/common/worker_pool.hpp new file mode 100644 index 0000000000..70643e0f68 --- /dev/null +++ b/silkworm/rpc/common/worker_pool.hpp @@ -0,0 +1,31 @@ +/* + Copyright 2024 The Silkworm Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include + +#include + +namespace silkworm::rpc { + +//! Default number of threads in worker pool (i.e. dedicated to heavier tasks) +inline const auto kDefaultNumWorkers{std::thread::hardware_concurrency() / 2}; + +// TODO(canepat) replace boost::asio::thread_pool with WorkerPool +// using WorkerPool = boost::asio::thread_pool; + +} // namespace silkworm::rpc diff --git a/silkworm/rpc/settings.hpp b/silkworm/rpc/settings.hpp index ff64c34fd5..1e95a35839 100644 --- a/silkworm/rpc/settings.hpp +++ b/silkworm/rpc/settings.hpp @@ -24,6 +24,7 @@ #include #include #include +#include namespace silkworm::rpc { @@ -37,7 +38,7 @@ struct DaemonSettings { std::string engine_end_point{kDefaultEngineEndPoint}; std::string eth_api_spec{kDefaultEth1ApiSpec}; std::string private_api_addr{kDefaultPrivateApiAddr}; - uint32_t num_workers{std::thread::hardware_concurrency() / 2}; + uint32_t num_workers{kDefaultNumWorkers}; std::vector cors_domain; std::optional jwt_secret_file; bool skip_protocol_check{false};