Kagome
Polkadot Runtime Engine in C++17
app_configuration_impl.cpp
Go to the documentation of this file.
1 
7 
8 #include <limits>
9 #include <regex>
10 #include <string>
11 
12 #include <rapidjson/document.h>
13 #include <rapidjson/filereadstream.h>
14 #include <boost/algorithm/string.hpp>
15 #include <boost/filesystem.hpp>
16 #include <boost/program_options.hpp>
17 #include <boost/uuid/uuid_generators.hpp>
18 #include <boost/uuid/uuid_io.hpp>
19 #include <charconv>
20 
21 #include "api/transport/tuner.hpp"
23 #include "assets/assets.hpp"
24 #include "chain_spec_impl.hpp"
25 #include "common/hexutil.hpp"
26 #include "common/uri.hpp"
29 
30 namespace {
31  namespace fs = kagome::filesystem;
32 
33  template <typename T, typename Func>
34  inline void find_argument(boost::program_options::variables_map &vm,
35  char const *name,
36  Func &&f) {
37  assert(nullptr != name);
38  if (auto it = vm.find(name); it != vm.end()) {
39  if (it->second.defaulted()) {
40  return;
41  }
42  std::forward<Func>(f)(it->second.as<T>());
43  }
44  }
45 
46  template <typename T>
47  inline std::optional<T> find_argument(
48  boost::program_options::variables_map &vm, const std::string &name) {
49  if (auto it = vm.find(name); it != vm.end()) {
50  if (!it->second.defaulted()) {
51  return it->second.as<T>();
52  }
53  }
54  return std::nullopt;
55  }
56 
57  const std::string def_rpc_http_host = "0.0.0.0";
58  const std::string def_rpc_ws_host = "0.0.0.0";
59  const std::string def_openmetrics_http_host = "0.0.0.0";
60  const uint16_t def_rpc_http_port = 9933;
61  const uint16_t def_rpc_ws_port = 9944;
62  const uint16_t def_openmetrics_http_port = 9615;
63  const uint32_t def_ws_max_connections = 500;
64  const uint16_t def_p2p_port = 30363;
65  const bool def_dev_mode = false;
66  const kagome::network::Roles def_roles = [] {
68  roles.flags.full = 1;
69  return roles;
70  }();
71  const auto def_sync_method =
73  const auto def_runtime_exec_method =
75  const auto def_use_wavm_cache_ = false;
76  const auto def_purge_wavm_cache_ = false;
77  const auto def_offchain_worker_mode =
79  const bool def_enable_offchain_indexing = false;
80  const bool def_subcommand_chain_info = false;
81  const std::optional<kagome::primitives::BlockId> def_block_to_recover =
82  std::nullopt;
83  const auto def_offchain_worker = "WhenValidating";
84  const uint32_t def_out_peers = 25;
85  const uint32_t def_in_peers = 25;
86  const uint32_t def_in_peers_light = 100;
87  const auto def_lucky_peers = 4;
88  const uint32_t def_random_walk_interval = 15;
89  const auto def_full_sync = "Full";
90  const auto def_wasm_execution = "Interpreted";
91 
96  const std::string &randomNodeName() {
97  static std::string name;
98  if (name.empty()) {
99  auto uuid = boost::uuids::random_generator()();
100  name = boost::uuids::to_string(uuid);
101  }
103  if (name.length() > max_len) {
104  name = name.substr(0, max_len);
105  }
106  return name;
107  }
108 
109  std::optional<kagome::application::AppConfiguration::SyncMethod>
110  str_to_sync_method(std::string_view str) {
112  if (str == "Full") {
113  return SM::Full;
114  }
115  if (str == "Fast") {
116  return SM::Fast;
117  }
118  return std::nullopt;
119  }
120 
121  std::optional<kagome::application::AppConfiguration::RuntimeExecutionMethod>
122  str_to_runtime_exec_method(std::string_view str) {
124  if (str == "Interpreted") {
125  return REM::Interpret;
126  }
127  if (str == "Compiled") {
128  return REM::Compile;
129  }
130  return std::nullopt;
131  }
132 
133  std::optional<kagome::application::AppConfiguration::OffchainWorkerMode>
134  str_to_offchain_worker_mode(std::string_view str) {
136  if (str == "Always") {
137  return Mode::Always;
138  }
139  if (str == "Newer") {
140  return Mode::Never;
141  }
142  if (str == "WhenValidating") {
143  return Mode::WhenValidating;
144  }
145  return std::nullopt;
146  }
147 
148  std::optional<kagome::primitives::BlockId> str_to_recovery_state(
149  std::string_view str) {
152  if (res.has_value()) {
153  return {{res.value()}};
154  }
155 
156  auto result = std::from_chars(str.data(), str.data() + str.size(), bn);
157  if (result.ec != std::errc::invalid_argument && std::to_string(bn) == str) {
158  return {{bn}};
159  }
160 
161  return std::nullopt;
162  }
163 
164  auto &devAccounts() {
165  static auto &dev = kagome::crypto::DevMnemonicPhrase::get();
166  using Account =
167  std::tuple<const char *, std::string_view, std::string_view>;
168  static const std::array<Account, 6> accounts{
169  Account{"alice", "Alice", dev.alice},
170  Account{"bob", "Bob", dev.bob},
171  Account{"charlie", "Charlie", dev.charlie},
172  Account{"dave", "Dave", dev.dave},
173  Account{"eve", "Eve", dev.eve},
174  Account{"ferdie", "Ferdie", dev.ferdie},
175  };
176  return accounts;
177  }
178 } // namespace
179 
180 namespace kagome::application {
181 
183  : logger_(std::move(logger)),
184  roles_(def_roles),
185  save_node_key_(false),
186  is_telemetry_enabled_(true),
187  p2p_port_(def_p2p_port),
188  max_blocks_in_response_(kAbsolutMaxBlocksInResponse),
189  rpc_http_host_(def_rpc_http_host),
190  rpc_ws_host_(def_rpc_ws_host),
191  openmetrics_http_host_(def_openmetrics_http_host),
192  rpc_http_port_(def_rpc_http_port),
193  rpc_ws_port_(def_rpc_ws_port),
194  openmetrics_http_port_(def_openmetrics_http_port),
195  out_peers_(def_out_peers),
196  in_peers_(def_in_peers),
197  in_peers_light_(def_in_peers_light),
198  lucky_peers_(def_lucky_peers),
199  dev_mode_(def_dev_mode),
200  node_name_(randomNodeName()),
201  node_version_(buildVersion()),
202  max_ws_connections_(def_ws_max_connections),
203  random_walk_interval_(def_random_walk_interval),
204  sync_method_{def_sync_method},
205  runtime_exec_method_{def_runtime_exec_method},
206  use_wavm_cache_(def_use_wavm_cache_),
207  purge_wavm_cache_(def_purge_wavm_cache_),
208  offchain_worker_mode_{def_offchain_worker_mode},
209  enable_offchain_indexing_{def_enable_offchain_indexing},
210  subcommand_chain_info_{def_subcommand_chain_info},
211  recovery_state_{def_block_to_recover} {
212  SL_INFO(logger_, "Soramitsu Kagome started. Version: {} ", buildVersion());
213  }
214 
216  return chain_spec_path_.native();
217  }
218 
219  boost::filesystem::path AppConfigurationImpl::runtimeCacheDirPath() const {
220  return boost::filesystem::temp_directory_path() / "kagome/runtimes-cache";
221  }
222 
223  boost::filesystem::path AppConfigurationImpl::runtimeCachePath(
224  std::string runtime_hash) const {
225  return runtimeCacheDirPath() / runtime_hash;
226  }
227 
228  boost::filesystem::path AppConfigurationImpl::chainPath(
229  std::string chain_id) const {
230  return base_path_ / chain_id;
231  }
232 
233  fs::path AppConfigurationImpl::databasePath(std::string chain_id) const {
234  return chainPath(chain_id) / "db";
235  }
236 
237  fs::path AppConfigurationImpl::keystorePath(std::string chain_id) const {
238  if (keystore_path_) return *keystore_path_ / chain_id / "keystore";
239  return chainPath(chain_id) / "keystore";
240  }
241 
243  const std::string &filepath) {
244  assert(!filepath.empty());
245  return AppConfigurationImpl::FilePtr(std::fopen(filepath.c_str(), "r"),
246  &std::fclose);
247  }
248 
249  bool AppConfigurationImpl::load_ms(const rapidjson::Value &val,
250  char const *name,
251  std::vector<std::string> &target) {
252  for (auto it = val.FindMember(name); it != val.MemberEnd(); ++it) {
253  auto &value = it->value;
254  target.emplace_back(value.GetString(), value.GetStringLength());
255  }
256  return not target.empty();
257  }
258 
260  const rapidjson::Value &val,
261  char const *name,
262  std::vector<libp2p::multi::Multiaddress> &target) {
263  for (auto it = val.FindMember(name); it != val.MemberEnd(); ++it) {
264  auto &value = it->value;
265  auto ma_res = libp2p::multi::Multiaddress::create(
266  std::string(value.GetString(), value.GetStringLength()));
267  if (not ma_res) {
268  return false;
269  }
270  target.emplace_back(std::move(ma_res.value()));
271  }
272  return not target.empty();
273  }
274 
276  const rapidjson::Value &val,
277  char const *name,
278  std::vector<telemetry::TelemetryEndpoint> &target) {
279  auto it = val.FindMember(name);
280  if (it != val.MemberEnd() and it->value.IsArray()) {
281  for (auto &v : it->value.GetArray()) {
282  if (v.IsString()) {
283  auto result = parseTelemetryEndpoint(v.GetString());
284  if (result.has_value()) {
285  target.emplace_back(std::move(result.value()));
286  continue;
287  }
288  }
289  return false;
290  } // for
291  }
292  return true;
293  }
294 
295  bool AppConfigurationImpl::load_str(const rapidjson::Value &val,
296  char const *name,
297  std::string &target) {
298  auto m = val.FindMember(name);
299  if (val.MemberEnd() != m && m->value.IsString()) {
300  target.assign(m->value.GetString(), m->value.GetStringLength());
301  return true;
302  }
303  return false;
304  }
305 
306  bool AppConfigurationImpl::load_bool(const rapidjson::Value &val,
307  char const *name,
308  bool &target) {
309  auto m = val.FindMember(name);
310  if (val.MemberEnd() != m && m->value.IsBool()) {
311  target = m->value.GetBool();
312  return true;
313  }
314  return false;
315  }
316 
317  bool AppConfigurationImpl::load_u16(const rapidjson::Value &val,
318  char const *name,
319  uint16_t &target) {
320  uint32_t i;
321  if (load_u32(val, name, i)
322  && (i & ~std::numeric_limits<uint16_t>::max()) == 0) {
323  target = static_cast<uint16_t>(i);
324  return true;
325  }
326  return false;
327  }
328 
329  bool AppConfigurationImpl::load_u32(const rapidjson::Value &val,
330  char const *name,
331  uint32_t &target) {
332  if (auto m = val.FindMember(name);
333  val.MemberEnd() != m && m->value.IsInt()) {
334  const auto v = m->value.GetInt();
335  if ((v & (1u << 31u)) == 0) {
336  target = static_cast<uint32_t>(v);
337  return true;
338  }
339  }
340  return false;
341  }
342 
343  bool AppConfigurationImpl::load_i32(const rapidjson::Value &val,
344  char const *name,
345  int32_t &target) {
346  if (auto m = val.FindMember(name);
347  val.MemberEnd() != m && m->value.IsInt()) {
348  target = m->value.GetInt();
349  return true;
350  }
351  return false;
352  }
353 
355  const rapidjson::Value &val) {
356  bool validator_mode = false;
357  load_bool(val, "validator", validator_mode);
358  if (validator_mode) {
359  roles_.flags.full = 0;
360  roles_.flags.authority = 1;
361  }
362 
363  load_ms(val, "log", logger_tuning_config_);
364  }
365 
367  const rapidjson::Value &val) {
368  std::string chain_spec_path_str;
369  load_str(val, "chain", chain_spec_path_str);
370  chain_spec_path_ = fs::path(chain_spec_path_str);
371  }
372 
374  const rapidjson::Value &val) {
375  std::string base_path_str;
376  load_str(val, "base-path", base_path_str);
377  base_path_ = fs::path(base_path_str);
378 
379  std::string database_engine_str;
380  if (load_str(val, "database", database_engine_str)) {
381  if ("rocksdb" == database_engine_str) {
383  } else {
384  SL_ERROR(logger_,
385  "Unsupported database backend was specified {}, "
386  "available options are [rocksdb]",
387  database_engine_str);
388  exit(EXIT_FAILURE);
389  }
390  }
391  }
392 
394  const rapidjson::Value &val) {
395  load_ma(val, "listen-addr", listen_addresses_);
396  load_ma(val, "public-addr", public_addresses_);
397  load_ma(val, "bootnodes", boot_nodes_);
398  load_u16(val, "port", p2p_port_);
399  load_str(val, "rpc-host", rpc_http_host_);
400  load_u16(val, "rpc-port", rpc_http_port_);
401  load_str(val, "ws-host", rpc_ws_host_);
402  load_u16(val, "ws-port", rpc_ws_port_);
403  load_u32(val, "ws-max-connections", max_ws_connections_);
404  load_str(val, "prometheus-host", openmetrics_http_host_);
405  load_u16(val, "prometheus-port", openmetrics_http_port_);
406  load_str(val, "name", node_name_);
407  load_u32(val, "out-peers", out_peers_);
408  load_u32(val, "in-peers", in_peers_);
409  load_u32(val, "in-peers-light", in_peers_light_);
410  load_i32(val, "lucky-peers", lucky_peers_);
411  load_telemetry_uris(val, "telemetry-endpoints", telemetry_endpoints_);
412  load_u32(val, "random-walk-interval", random_walk_interval_);
413  }
414 
416  const rapidjson::Value &val) {
417  load_u32(val, "max-blocks-in-response", max_blocks_in_response_);
418  load_bool(val, "dev", dev_mode_);
419  }
420 
422  if (not fs::exists(chain_spec_path_)) {
423  SL_ERROR(logger_,
424  "Chain path {} does not exist, "
425  "please specify a valid path with --chain option",
427  return false;
428  }
429 
431  SL_ERROR(logger_,
432  "Base path {} does not exist, "
433  "please specify a valid path with -d option",
434  base_path_);
435  return false;
436  }
437 
438  if (not listen_addresses_.empty()) {
439  SL_INFO(logger_,
440  "Listen addresses are set. The p2p port value would be ignored "
441  "then.");
442  } else if (p2p_port_ == 0) {
443  SL_ERROR(logger_,
444  "p2p port is 0, "
445  "please specify a valid path with -p option");
446  return false;
447  }
448 
449  if (rpc_ws_port_ == 0) {
450  SL_ERROR(logger_,
451  "RPC ws port is 0, "
452  "please specify a valid path with --ws-port option");
453  return false;
454  }
455 
456  if (rpc_http_port_ == 0) {
457  SL_ERROR(logger_,
458  "RPC http port is 0, "
459  "please specify a valid path with --rpc-port option");
460  return false;
461  }
462 
463  if (node_name_.length() > kNodeNameMaxLength) {
464  SL_ERROR(logger_,
465  "Node name exceeds the maximum length of {} characters",
467  return false;
468  }
469 
470  // pagination page size bounded [kAbsolutMinBlocksInResponse,
471  // kAbsolutMaxBlocksInResponse]
475  return true;
476  }
477 
479  const std::string &filepath) {
480  assert(!filepath.empty());
481 
482  auto file = open_file(filepath);
483  if (!file) {
484  SL_ERROR(logger_,
485  "Configuration file path is invalid: {}, "
486  "please specify a valid path with -c option",
487  filepath);
488  return;
489  }
490 
491  using FileReadStream = rapidjson::FileReadStream;
492  using Document = rapidjson::Document;
493 
494  std::array<char, 1024> buffer_size{};
495  FileReadStream input_stream(
496  file.get(), buffer_size.data(), buffer_size.size());
497 
498  Document document;
499  document.ParseStream(input_stream);
500  if (document.HasParseError()) {
501  SL_ERROR(logger_,
502  "Configuration file {} parse failed with error {}",
503  filepath,
504  document.GetParseError());
505  return;
506  }
507 
508  for (auto &handler : handlers_) {
509  auto it = document.FindMember(handler.segment_name);
510  if (document.MemberEnd() != it) {
511  handler.handler(it->value);
512  }
513  }
514  }
515 
516  boost::asio::ip::tcp::endpoint AppConfigurationImpl::getEndpointFrom(
517  std::string const &host, uint16_t port) const {
518  boost::asio::ip::tcp::endpoint endpoint;
519  boost::system::error_code err;
520 
521  endpoint.address(boost::asio::ip::address::from_string(host, err));
522  if (err.failed()) {
523  SL_ERROR(logger_, "RPC address '{}' is invalid", host);
524  exit(EXIT_FAILURE);
525  }
526 
527  endpoint.port(port);
528  return endpoint;
529  }
530 
531  outcome::result<boost::asio::ip::tcp::endpoint>
533  const libp2p::multi::Multiaddress &multiaddress) const {
534  using proto = libp2p::multi::Protocol::Code;
535  constexpr auto NOT_SUPPORTED = std::errc::address_family_not_supported;
536  constexpr auto BAD_ADDRESS = std::errc::bad_address;
537  auto host = multiaddress.getFirstValueForProtocol(proto::IP4);
538  if (not host) {
539  host = multiaddress.getFirstValueForProtocol(proto::IP6);
540  }
541  if (not host) {
542  SL_ERROR(logger_,
543  "Address cannot be used to bind to ({}). Only IPv4 and IPv6 "
544  "interfaces are supported",
545  multiaddress.getStringAddress());
546  return NOT_SUPPORTED;
547  }
548  auto port = multiaddress.getFirstValueForProtocol(proto::TCP);
549  if (not port) {
550  return NOT_SUPPORTED;
551  }
552  uint16_t port_number = 0;
553  try {
554  auto wide_port = std::stoul(port.value());
555  constexpr auto max_port = std::numeric_limits<uint16_t>::max();
556  if (wide_port > max_port or 0 == wide_port) {
557  SL_ERROR(
558  logger_,
559  "Port value ({}) cannot be zero or greater than {} (address {})",
560  wide_port,
561  max_port,
562  multiaddress.getStringAddress());
563  return BAD_ADDRESS;
564  }
565  port_number = static_cast<uint16_t>(wide_port);
566  } catch (...) {
567  // only std::out_of_range or std::invalid_argument are possible
568  SL_ERROR(logger_,
569  "Passed value {} is not a valid port number within address {}",
570  port.value(),
571  multiaddress.getStringAddress());
572  return BAD_ADDRESS;
573  }
574  return getEndpointFrom(host.value(), port_number);
575  }
576 
578  auto temp_context = std::make_shared<boost::asio::io_context>();
579  constexpr auto kZeroPortTolerance = 0;
580  for (const auto &addr : listen_addresses_) {
581  auto endpoint = getEndpointFrom(addr);
582  if (not endpoint) {
583  SL_ERROR(logger_,
584  "Endpoint cannot be constructed from address {}",
585  addr.getStringAddress());
586  return false;
587  }
588  try {
589  boost::system::error_code error_code;
590  auto acceptor = api::acceptOnFreePort(
591  temp_context, endpoint.value(), kZeroPortTolerance, logger_);
592  acceptor->cancel(error_code);
593  acceptor->close(error_code);
594  } catch (...) {
595  SL_ERROR(
596  logger_, "Unable to listen on address {}", addr.getStringAddress());
597  return false;
598  }
599  }
600  return true;
601  }
602 
603  std::optional<telemetry::TelemetryEndpoint>
605  const std::string &record) const {
606  /*
607  * The only form a telemetry endpoint could be specified is
608  * "<endpoint uri> <verbosity level>", where verbosity level is a single
609  * numeric character in a range from 0 to 9 inclusively.
610  */
611 
612  // check there is a space char preceding the verbosity level number
613  const auto len = record.length();
614  constexpr auto kSpaceChar = ' ';
615  if (kSpaceChar != record.at(len - 2)) {
616  SL_ERROR(logger_,
617  "record '{}' could not be parsed as a valid telemetry endpoint. "
618  "The desired format is '<endpoint uri> <verbosity: 0-9>'",
619  record);
620  return std::nullopt;
621  }
622 
623  // try to parse verbosity level
624  uint8_t verbosity_level{0};
625  try {
626  auto verbosity_char = record.substr(len - 1);
627  int verbosity_level_parsed = std::stoi(verbosity_char);
628  // the following check is left intentionally
629  // despite possible redundancy
630  if (verbosity_level_parsed < 0 or verbosity_level_parsed > 9) {
631  throw std::out_of_range("verbosity level value is out of range");
632  }
633  verbosity_level = static_cast<uint8_t>(verbosity_level_parsed);
634  } catch (std::invalid_argument const &e) {
635  SL_ERROR(logger_,
636  "record '{}' could not be parsed as a valid telemetry endpoint. "
637  "The desired format is '<endpoint uri> <verbosity: 0-9>'. "
638  "Verbosity level does not meet the format: {}",
639  record,
640  e.what());
641  return std::nullopt;
642  } catch (std::out_of_range const &e) {
643  SL_ERROR(logger_,
644  "record '{}' could not be parsed as a valid telemetry endpoint. "
645  "The desired format is '<endpoint uri> <verbosity: 0-9>'. "
646  "Verbosity level does not meet the format: {}",
647  record,
648  e.what());
649  return std::nullopt;
650  }
651 
652  // try to parse endpoint uri
653  auto uri_part = record.substr(0, len - 2);
654 
655  if (not uri_part.empty() and '/' == uri_part.at(0)) {
656  // assume endpoint specified as multiaddress
657  auto ma_res = libp2p::multi::Multiaddress::create(uri_part);
658  if (ma_res.has_error()) {
659  SL_ERROR(logger_,
660  "Telemetry endpoint '{}' cannot be interpreted as a valid "
661  "multiaddress and was skipped due to error: {}",
662  uri_part,
663  ma_res.error().message());
664  return std::nullopt;
665  }
666 
667  {
668  // transform multiaddr of telemetry endpoint into uri form
669  auto parts = ma_res.value().getProtocolsWithValues();
670  if (parts.size() != 3) {
671  SL_ERROR(logger_,
672  "Telemetry endpoint '{}' has unknown format and was skipped",
673  uri_part);
674  return std::nullopt;
675  }
676  auto host = parts[0].second;
677  auto schema = parts[2].first.name.substr(std::strlen("x-parity-"));
678  auto path = std::regex_replace(parts[2].second, std::regex("%2F"), "/");
679  uri_part = fmt::format("{}://{}{}", schema, host, path);
680  }
681  }
682 
683  auto uri = common::Uri::parse(uri_part);
684  if (uri.error().has_value()) {
685  SL_ERROR(logger_,
686  "record '{}' could not be parsed as a valid telemetry endpoint. "
687  "The desired format is '<endpoint uri> <verbosity: 0-9>'. "
688  "Endpoint URI parsing failed: {}",
689  record,
690  uri.error().value());
691  return std::nullopt;
692  }
693 
694  return telemetry::TelemetryEndpoint{std::move(uri), verbosity_level};
695  }
696 
697  bool AppConfigurationImpl::initializeFromArgs(int argc, const char **argv) {
698  namespace po = boost::program_options;
699 
700  // clang-format off
701  po::options_description desc("General options");
702  desc.add_options()
703  ("help,h", "show this help message")
704  ("log,l", po::value<std::vector<std::string>>(),
705  "Sets a custom logging filter. Syntax is `<target>=<level>`, e.g. -llibp2p=off.\n"
706  "Log levels (most to least verbose) are trace, debug, verbose, info, warn, error, critical, off. By default, all targets log `info`.\n"
707  "The global log level can be set with -l<level>.")
708  ("validator", "Enable validator node")
709  ("config-file,c", po::value<std::string>(), "Filepath to load configuration from.")
710  ;
711 
712  po::options_description blockhain_desc("Blockchain options");
713  blockhain_desc.add_options()
714  ("chain", po::value<std::string>(), "required, chainspec file path")
715  ("offchain-worker", po::value<std::string>()->default_value(def_offchain_worker),
716  "Should execute offchain workers on every block.\n"
717  "Possible values: Always, Never, WhenValidating. WhenValidating is used by default.")
718  ("chain-info", po::bool_switch(), "Print chain info as JSON")
719  ;
720 
721  po::options_description storage_desc("Storage options");
722  storage_desc.add_options()
723  ("base-path,d", po::value<std::string>(), "required, node base path (keeps storage and keys for known chains)")
724  ("keystore", po::value<std::string>(), "required, node keystore")
725  ("tmp", "Use temporary storage path")
726  ("database", po::value<std::string>()->default_value("rocksdb"), "Database backend to use [rocksdb]")
727  ("enable-offchain-indexing", po::value<bool>(), "enable Offchain Indexing API, which allow block import to write to offchain DB)")
728  ("recovery", po::value<std::string>(), "recovers block storage to state after provided block presented by number or hash, and stop after that")
729  ;
730 
731  po::options_description network_desc("Network options");
732  network_desc.add_options()
733  ("listen-addr", po::value<std::vector<std::string>>()->multitoken(), "multiaddresses the node listens for open connections on")
734  ("public-addr", po::value<std::vector<std::string>>()->multitoken(), "multiaddresses that other nodes use to connect to it")
735  ("node-key", po::value<std::string>(), "the secret key to use for libp2p networking")
736  ("node-key-file", po::value<std::string>(), "path to the secret key used for libp2p networking (raw binary or hex-encoded")
737  ("save-node-key", po::bool_switch(), "save generated libp2p networking key, key will be reused on node restart")
738  ("bootnodes", po::value<std::vector<std::string>>()->multitoken(), "multiaddresses of bootstrap nodes")
739  ("port,p", po::value<uint16_t>(), "port for peer to peer interactions")
740  ("rpc-host", po::value<std::string>(), "address for RPC over HTTP")
741  ("rpc-port", po::value<uint16_t>(), "port for RPC over HTTP")
742  ("ws-host", po::value<std::string>(), "address for RPC over Websocket protocol")
743  ("ws-port", po::value<uint16_t>(), "port for RPC over Websocket protocol")
744  ("ws-max-connections", po::value<uint32_t>(), "maximum number of WS RPC server connections")
745  ("prometheus-host", po::value<std::string>(), "address for OpenMetrics over HTTP")
746  ("prometheus-port", po::value<uint16_t>(), "port for OpenMetrics over HTTP")
747  ("out-peers", po::value<uint32_t>()->default_value(def_out_peers), "number of outgoing connections we're trying to maintain")
748  ("in-peers", po::value<uint32_t>()->default_value(def_in_peers), "maximum number of inbound full nodes peers")
749  ("in-peers-light", po::value<uint32_t>()->default_value(def_in_peers_light), "maximum number of inbound light nodes peers")
750  ("lucky-peers", po::value<int32_t>()->default_value(def_lucky_peers), "number of \"lucky\" peers (peers that are being gossiped to). -1 for broadcast." )
751  ("max-blocks-in-response", po::value<uint32_t>(), "max block per response while syncing")
752  ("name", po::value<std::string>(), "the human-readable name for this node")
753  ("no-telemetry", po::bool_switch(), "Disables telemetry broadcasting")
754  ("telemetry-url", po::value<std::vector<std::string>>()->multitoken(),
755  "the URL of the telemetry server to connect to and verbosity level (0-9),\n"
756  "e.g. --telemetry-url 'wss://foo/bar 0'")
757  ("random-walk-interval", po::value<uint32_t>()->default_value(def_random_walk_interval), "Kademlia random walk interval")
758  ;
759 
760  po::options_description development_desc("Additional options");
761  development_desc.add_options()
762  ("dev", "if node run in development mode")
763  ("dev-with-wipe", "if needed to wipe base path (only for dev mode)")
764  ("sync", po::value<std::string>()->default_value(def_full_sync),
765  "choose the desired sync method (Full, Fast). Full is used by default.")
766  ("wasm-execution", po::value<std::string>()->default_value(def_wasm_execution),
767  "choose the desired wasm execution method (Compiled, Interpreted)")
768  ("unsafe-cached-wavm-runtime", "use WAVM runtime cache")
769  ("purge-wavm-cache", "purge WAVM runtime cache")
770  ;
771 
772  // clang-format on
773 
774  for (auto &[flag, name, dev] : devAccounts()) {
775  development_desc.add_options()(flag, po::bool_switch());
776  }
777 
778  po::variables_map vm;
779  // first-run parse to read only general options and to lookup for "help"
780  // all the rest options are ignored
781  po::parsed_options parsed = po::command_line_parser(argc, argv)
782  .options(desc)
783  .allow_unregistered()
784  .run();
785  po::store(parsed, vm);
786  po::notify(vm);
787 
788  desc.add(blockhain_desc)
789  .add(storage_desc)
790  .add(network_desc)
791  .add(development_desc);
792 
793  if (vm.count("help") > 0) {
794  std::cout << desc << std::endl;
795  return false;
796  }
797 
798  try {
799  // second-run parse to gather all known options
800  // with reporting about any unrecognized input
801  po::store(po::parse_command_line(argc, argv, desc), vm);
802  po::store(parsed, vm);
803  po::notify(vm);
804  } catch (const std::exception &e) {
805  std::cerr << "Error: " << e.what() << '\n'
806  << "Try run with option '--help' for more information"
807  << std::endl;
808  return false;
809  }
810 
811  // Setup default development settings (and wipe if needed)
812  if (vm.count("dev") > 0 or vm.count("dev-with-wipe") > 0) {
813  constexpr auto with_kagome_embeddings =
814 #ifdef USE_KAGOME_EMBEDDINGS
815  true;
816 #else
817  false;
818 #endif // USE_KAGOME_EMBEDDINGS
819 
820  if constexpr (not with_kagome_embeddings) {
821  std::cerr << "Warning: developers mode is not available. "
822  "Application was built without developers embeddings "
823  "(EMBEDDINGS option is OFF)."
824  << std::endl;
825  return false;
826  } else {
827  dev_mode_ = true;
828 
829  auto dev_env_path = fs::temp_directory_path() / "kagome_dev";
830  chain_spec_path_ = dev_env_path / "chainspec.json";
831  base_path_ = dev_env_path / "base_path";
832 
833  // Wipe base directory on demand
834  if (vm.count("dev-with-wipe") > 0) {
835  boost::filesystem::remove_all(dev_env_path);
836  }
837 
838  if (not boost::filesystem::exists(chain_spec_path_)) {
839  boost::filesystem::create_directories(chain_spec_path_.parent_path());
840 
841  std::ofstream ofs;
842  ofs.open(chain_spec_path_.native(), std::ios::ate);
844  ofs.close();
845 
846  auto chain_spec = ChainSpecImpl::loadFrom(chain_spec_path_.native());
847  auto path = keystorePath(chain_spec.value()->id());
848 
849  if (not chain_spec.has_value()) {
850  std::cerr << "Warning: developers mode chain spec is corrupted."
851  << std::endl;
852  return false;
853  }
854 
855  if (chain_spec.value()->bootNodes().empty()) {
856  std::cerr
857  << "Warning: developers mode chain spec bootnodes is empty."
858  << std::endl;
859  return false;
860  }
861 
862  auto ma_res = chain_spec.value()->bootNodes()[0];
863  listen_addresses_.emplace_back(ma_res);
864 
865  boost::filesystem::create_directories(path);
866 
867  for (auto key_descr : kagome::assets::embedded_keys) {
868  ofs.open((path / key_descr.first).native(), std::ios::ate);
869  ofs << key_descr.second;
870  ofs.close();
871  }
872  }
873 
874  roles_.flags.full = 0;
875  roles_.flags.authority = 1;
876  p2p_port_ = def_p2p_port;
877  rpc_http_host_ = def_rpc_http_host;
878  rpc_ws_host_ = def_rpc_ws_host;
879  openmetrics_http_host_ = def_openmetrics_http_host;
880  rpc_http_port_ = def_rpc_http_port;
881  rpc_ws_port_ = def_rpc_ws_port;
882  openmetrics_http_port_ = def_openmetrics_http_port;
883  }
884  }
885 
886  std::optional<std::string> dev_account_flag;
887  for (auto &[flag, name, dev] : devAccounts()) {
888  if (auto val = find_argument<bool>(vm, flag); val && *val) {
889  if (dev_account_flag) {
890  SL_ERROR(
891  logger_, "--{} conflicts with --{}", flag, *dev_account_flag);
892  return false;
893  }
894  dev_account_flag = flag;
895  node_name_ = name;
896  dev_mnemonic_phrase_ = dev;
897  }
898  }
899 
900  find_argument<std::string>(vm, "config-file", [&](std::string const &path) {
901  if (dev_mode_) {
902  std::cerr << "Warning: config file has ignored because dev mode"
903  << std::endl;
904  } else {
905  read_config_from_file(path);
906  }
907  });
908 
909  if (vm.end() != vm.find("validator")) {
910  roles_.flags.full = 0;
911  roles_.flags.authority = 1;
912  }
913 
914  find_argument<std::string>(
915  vm, "chain", [&](const std::string &val) { chain_spec_path_ = val; });
916  if (not boost::filesystem::exists(chain_spec_path_)) {
917  std::cerr << "Specified chain spec " << chain_spec_path_
918  << " does not exist." << std::endl;
919  }
920 
921  if (vm.end() != vm.find("tmp")) {
922  base_path_ = (boost::filesystem::temp_directory_path()
923  / boost::filesystem::unique_path());
924  } else {
925  find_argument<std::string>(
926  vm, "base-path", [&](const std::string &val) { base_path_ = val; });
927  }
928 
929  find_argument<std::string>(
930  vm, "keystore", [&](const std::string &val) { keystore_path_ = val; });
931 
932  bool unknown_database_engine_is_set = false;
933  find_argument<std::string>(vm, "database", [&](const std::string &val) {
934  if ("rocksdb" == val) {
936  } else {
937  unknown_database_engine_is_set = true;
938  SL_ERROR(logger_,
939  "Unsupported database backend was specified {}, "
940  "available options are [rocksdb]",
941  val);
942  }
943  });
944  if (unknown_database_engine_is_set) {
945  return false;
946  }
947 
948  std::vector<std::string> boot_nodes;
949  find_argument<std::vector<std::string>>(
950  vm, "bootnodes", [&](const std::vector<std::string> &val) {
951  boot_nodes = val;
952  });
953  if (not boot_nodes.empty()) {
954  boot_nodes_.clear();
955  boot_nodes_.reserve(boot_nodes.size());
956  for (auto &addr_str : boot_nodes) {
957  auto ma_res = libp2p::multi::Multiaddress::create(addr_str);
958  if (not ma_res.has_value()) {
959  auto err_msg = "Bootnode '" + addr_str
960  + "' is invalid: " + ma_res.error().message();
961  SL_ERROR(logger_, "{}", err_msg);
962  std::cout << err_msg << std::endl;
963  return false;
964  }
965  auto peer_id_base58_opt = ma_res.value().getPeerId();
966  if (not peer_id_base58_opt) {
967  auto err_msg = "Bootnode '" + addr_str + "' has not peer_id";
968  SL_ERROR(logger_, "{}", err_msg);
969  std::cout << err_msg << std::endl;
970  return false;
971  }
972  boot_nodes_.emplace_back(std::move(ma_res.value()));
973  }
974  }
975 
976  std::optional<std::string> node_key;
977  find_argument<std::string>(
978  vm, "node-key", [&](const std::string &val) { node_key.emplace(val); });
979  if (node_key.has_value()) {
980  auto key_res = crypto::Ed25519PrivateKey::fromHex(node_key.value());
981  if (not key_res.has_value()) {
982  auto err_msg = "Node key '" + node_key.value()
983  + "' is invalid: " + key_res.error().message();
984  SL_ERROR(logger_, "{}", err_msg);
985  std::cout << err_msg << std::endl;
986  return false;
987  }
988  node_key_.emplace(std::move(key_res.value()));
989  }
990 
991  if (not node_key_.has_value()) {
992  find_argument<std::string>(
993  vm, "node-key-file", [&](const std::string &val) {
994  node_key_file_ = val;
995  });
996  }
997 
998  find_argument<bool>(
999  vm, "save-node-key", [&](bool val) { save_node_key_ = val; });
1000 
1001  find_argument<uint16_t>(vm, "port", [&](uint16_t val) { p2p_port_ = val; });
1002 
1003  auto parse_multiaddrs =
1004  [&](const std::string &param_name,
1005  std::vector<libp2p::multi::Multiaddress> &output_field) -> bool {
1006  std::vector<std::string> addrs;
1007  find_argument<std::vector<std::string>>(
1008  vm, param_name.c_str(), [&](const auto &val) { addrs = val; });
1009 
1010  if (not addrs.empty()) {
1011  output_field.clear();
1012  }
1013  for (auto &s : addrs) {
1014  auto ma_res = libp2p::multi::Multiaddress::create(s);
1015  if (not ma_res) {
1016  SL_ERROR(logger_,
1017  "Address {} passed as value to {} is invalid: {}",
1018  s,
1019  param_name,
1020  ma_res.error().message());
1021  return false;
1022  }
1023  output_field.emplace_back(std::move(ma_res.value()));
1024  }
1025  return true;
1026  };
1027 
1028  if (not parse_multiaddrs("listen-addr", listen_addresses_)) {
1029  return false; // just proxy erroneous case to the top level
1030  }
1031  if (not parse_multiaddrs("public-addr", public_addresses_)) {
1032  return false; // just proxy erroneous case to the top level
1033  }
1034 
1035  // this goes before transforming p2p port option to listen addresses,
1036  // because it does not make sense to announce 0.0.0.0 as a public address.
1037  // Moreover, this is ok to have empty set of public addresses at this point
1038  // of time
1039  if (public_addresses_.empty() and not listen_addresses_.empty()) {
1040  SL_INFO(logger_,
1041  "Public addresses are not specified. Using listen addresses as "
1042  "node's public addresses");
1044  }
1045 
1046  if (0 != p2p_port_ and listen_addresses_.empty()) {
1047  // IPv6
1048  {
1049  auto ma_res = libp2p::multi::Multiaddress::create(
1050  "/ip6/::/tcp/" + std::to_string(p2p_port_));
1051  if (not ma_res) {
1052  SL_ERROR(
1053  logger_,
1054  "Cannot construct IPv6 listen multiaddress from port {}. Error: "
1055  "{}",
1056  p2p_port_,
1057  ma_res.error().message());
1058  } else {
1059  SL_INFO(logger_,
1060  "Automatically added IPv6 listen address {}",
1061  ma_res.value().getStringAddress());
1062  listen_addresses_.emplace_back(std::move(ma_res.value()));
1063  }
1064  }
1065 
1066  // IPv4
1067  {
1068  auto ma_res = libp2p::multi::Multiaddress::create(
1069  "/ip4/0.0.0.0/tcp/" + std::to_string(p2p_port_));
1070  if (not ma_res) {
1071  SL_ERROR(
1072  logger_,
1073  "Cannot construct IPv4 listen multiaddress from port {}. Error: "
1074  "{}",
1075  p2p_port_,
1076  ma_res.error().message());
1077  } else {
1078  SL_INFO(logger_,
1079  "Automatically added IPv4 listen address {}",
1080  ma_res.value().getStringAddress());
1081  listen_addresses_.emplace_back(std::move(ma_res.value()));
1082  }
1083  }
1084  }
1085 
1086  if (not testListenAddresses()) {
1087  SL_ERROR(logger_,
1088  "One of configured listen addresses is unavailable, the node "
1089  "cannot start.");
1090  return false;
1091  }
1092 
1093  find_argument<uint32_t>(vm, "max-blocks-in-response", [&](uint32_t val) {
1095  });
1096 
1097  find_argument<std::vector<std::string>>(
1098  vm, "log", [&](const std::vector<std::string> &val) {
1099  logger_tuning_config_ = val;
1100  });
1101 
1102  find_argument<std::string>(
1103  vm, "rpc-host", [&](std::string const &val) { rpc_http_host_ = val; });
1104 
1105  find_argument<std::string>(
1106  vm, "ws-host", [&](std::string const &val) { rpc_ws_host_ = val; });
1107 
1108  find_argument<std::string>(
1109  vm, "prometheus-host", [&](std::string const &val) {
1110  openmetrics_http_host_ = val;
1111  });
1112 
1113  find_argument<uint16_t>(
1114  vm, "rpc-port", [&](uint16_t val) { rpc_http_port_ = val; });
1115 
1116  find_argument<uint16_t>(
1117  vm, "ws-port", [&](uint16_t val) { rpc_ws_port_ = val; });
1118 
1119  find_argument<uint16_t>(vm, "prometheus-port", [&](uint16_t val) {
1120  openmetrics_http_port_ = val;
1121  });
1122 
1123  find_argument<uint32_t>(
1124  vm, "out-peers", [&](uint32_t val) { out_peers_ = val; });
1125 
1126  find_argument<uint32_t>(
1127  vm, "in-peers", [&](uint32_t val) { in_peers_ = val; });
1128 
1129  find_argument<uint32_t>(
1130  vm, "in-peers-light", [&](uint32_t val) { in_peers_light_ = val; });
1131 
1132  find_argument<int32_t>(
1133  vm, "lucky-peers", [&](int32_t val) { lucky_peers_ = val; });
1134 
1135  find_argument<uint32_t>(vm, "ws-max-connections", [&](uint32_t val) {
1136  max_ws_connections_ = val;
1137  });
1138 
1139  find_argument<uint32_t>(vm, "random-walk-interval", [&](uint32_t val) {
1140  random_walk_interval_ = val;
1141  });
1142 
1147 
1148  find_argument<std::string>(
1149  vm, "name", [&](std::string const &val) { node_name_ = val; });
1150 
1151  auto parse_telemetry_urls =
1152  [&](const std::string &param_name,
1153  std::vector<telemetry::TelemetryEndpoint> &output_field) -> bool {
1154  std::vector<std::string> tokens;
1155  find_argument<std::vector<std::string>>(
1156  vm, param_name.c_str(), [&](const auto &val) { tokens = val; });
1157 
1158  for (const auto &token : tokens) {
1159  auto result = parseTelemetryEndpoint(token);
1160  if (result.has_value()) {
1161  telemetry_endpoints_.emplace_back(std::move(result.value()));
1162  } else {
1163  return false;
1164  }
1165  }
1166  return true;
1167  };
1168 
1169  find_argument<bool>(vm, "no-telemetry", [&](bool telemetry_disabled) {
1170  is_telemetry_enabled_ = not telemetry_disabled;
1171  });
1172 
1173  if (is_telemetry_enabled_) {
1174  if (not parse_telemetry_urls("telemetry-url", telemetry_endpoints_)) {
1175  return false; // just proxy erroneous case to the top level
1176  }
1177  }
1178 
1179  bool sync_method_value_error = false;
1180  find_argument<std::string>(
1181  vm, "sync", [this, &sync_method_value_error](std::string const &val) {
1182  auto sync_method_opt = str_to_sync_method(val);
1183  if (not sync_method_opt) {
1184  sync_method_value_error = true;
1185  SL_ERROR(logger_, "Invalid sync method specified: '{}'", val);
1186  } else {
1187  sync_method_ = sync_method_opt.value();
1188  }
1189  });
1190  if (sync_method_value_error) {
1191  return false;
1192  }
1193 
1194  bool exec_method_value_error = false;
1195  find_argument<std::string>(
1196  vm,
1197  "wasm-execution",
1198  [this, &exec_method_value_error](std::string const &val) {
1199  auto runtime_exec_method_opt = str_to_runtime_exec_method(val);
1200  if (not runtime_exec_method_opt) {
1201  exec_method_value_error = true;
1202  SL_ERROR(logger_,
1203  "Invalid runtime execution method specified: '{}'",
1204  val);
1205  } else {
1206  runtime_exec_method_ = runtime_exec_method_opt.value();
1207  }
1208  });
1209  if (exec_method_value_error) {
1210  return false;
1211  }
1212 
1213  if (vm.count("unsafe-cached-wavm-runtime") > 0) {
1214  use_wavm_cache_ = true;
1215  }
1216 
1217  if (vm.count("purge-wavm-cache") > 0) {
1218  purge_wavm_cache_ = true;
1219  if (fs::exists(runtimeCacheDirPath())) {
1220  boost::system::error_code ec;
1221  fs::remove_all(runtimeCacheDirPath(), ec);
1222  if (ec.failed()) {
1223  SL_ERROR(logger_,
1224  "Failed to purge cache in {} ['{}']",
1226  ec.message());
1227  }
1228  }
1229  }
1230 
1231  bool offchain_worker_value_error = false;
1232  find_argument<std::string>(
1233  vm,
1234  "offchain-worker",
1235  [this, &offchain_worker_value_error](std::string const &val) {
1236  auto offchain_worker_mode_opt = str_to_offchain_worker_mode(val);
1237  if (offchain_worker_mode_opt) {
1238  offchain_worker_mode_ = offchain_worker_mode_opt.value();
1239  } else {
1240  offchain_worker_value_error = true;
1241  SL_ERROR(
1242  logger_, "Invalid offchain worker mode specified: '{}'", val);
1243  }
1244  });
1245  if (offchain_worker_value_error) {
1246  return false;
1247  }
1248 
1249  if (vm.count("enable-offchain-indexing") > 0) {
1251  }
1252 
1253  find_argument<bool>(vm, "chain-info", [&](bool subcommand_chain_info) {
1254  subcommand_chain_info_ = subcommand_chain_info;
1255  });
1256 
1257  bool has_recovery = false;
1258  find_argument<std::string>(vm, "recovery", [&](const std::string &val) {
1259  has_recovery = true;
1260  recovery_state_ = str_to_recovery_state(val);
1261  if (not recovery_state_) {
1262  SL_ERROR(logger_, "Invalid recovery state specified: '{}'", val);
1263  }
1264  });
1265  if (has_recovery and not recovery_state_.has_value()) {
1266  return false;
1267  }
1268 
1269  // if something wrong with config print help message
1270  if (not validate_config()) {
1271  std::cout << desc << std::endl;
1272  return false;
1273  }
1274  return true;
1275  }
1276 } // namespace kagome::application
const std::vector< std::pair< const char *, const char * > > embedded_keys
boost::asio::ip::tcp::endpoint rpc_http_endpoint_
struct kagome::network::Roles::@11 flags
std::optional< primitives::BlockId > recovery_state_
bool load_u16(const rapidjson::Value &val, char const *name, uint16_t &target)
boost::asio::ip::tcp::endpoint openmetrics_http_endpoint_
static const DevMnemonicPhrase & get()
bool load_str(const rapidjson::Value &val, char const *name, std::string &target)
void parse_additional_segment(const rapidjson::Value &val)
boost::filesystem::path chainSpecPath() const override
std::string_view to_string(SlotType s)
Definition: slot.hpp:22
boost::filesystem::path keystorePath(std::string chain_id) const override
STL namespace.
bool load_i32(const rapidjson::Value &val, char const *name, int32_t &target)
static constexpr uint32_t kAbsolutMinBlocksInResponse
std::vector< libp2p::multi::Multiaddress > boot_nodes_
static outcome::result< std::shared_ptr< ChainSpecImpl > > loadFrom(const std::string &config_path)
std::optional< crypto::Ed25519PrivateKey > node_key_
void parse_network_segment(const rapidjson::Value &val)
std::optional< telemetry::TelemetryEndpoint > parseTelemetryEndpoint(const std::string &record) const
static Uri parse(std::string_view uri)
Definition: uri.cpp:42
std::optional< boost::filesystem::path > keystore_path_
uint32_t BlockNumber
Definition: common.hpp:18
FilePtr open_file(const std::string &filepath)
void parse_general_segment(const rapidjson::Value &val)
const char *const embedded_chainspec
bool createDirectoryRecursive(const path &path)
Definition: directories.hpp:18
static constexpr uint32_t kAbsolutMaxBlocksInResponse
std::vector< libp2p::multi::Multiaddress > listen_addresses_
std::unique_ptr< Acceptor > acceptOnFreePort(std::shared_ptr< boost::asio::io_context > context, Endpoint endpoint, uint16_t port_tolerance, const log::Logger &logger)
Definition: tuner.cpp:10
std::shared_ptr< soralog::Logger > Logger
Definition: logger.hpp:23
bool initializeFromArgs(int argc, const char **argv)
std::vector< libp2p::multi::Multiaddress > public_addresses_
boost::filesystem::path runtimeCacheDirPath() const override
const std::string & buildVersion()
bool load_u32(const rapidjson::Value &val, char const *name, uint32_t &target)
boost::filesystem::path runtimeCachePath(std::string runtime_hash) const override
static constexpr uint32_t kNodeNameMaxLength
std::unique_ptr< std::FILE, decltype(&std::fclose)> FilePtr
boost::filesystem::path databasePath(std::string chain_id) const override
void parse_storage_segment(const rapidjson::Value &val)
bool load_ms(const rapidjson::Value &val, char const *name, std::vector< std::string > &target)
bool load_telemetry_uris(const rapidjson::Value &val, char const *name, std::vector< telemetry::TelemetryEndpoint > &target)
bool load_bool(const rapidjson::Value &val, char const *name, bool &target)
void read_config_from_file(const std::string &filepath)
boost::filesystem::path chainPath(std::string chain_id) const override
void parse_blockchain_segment(const rapidjson::Value &val)
bool load_ma(const rapidjson::Value &val, char const *name, std::vector< libp2p::multi::Multiaddress > &target)
boost::asio::ip::tcp::endpoint getEndpointFrom(const std::string &host, uint16_t port) const
std::vector< telemetry::TelemetryEndpoint > telemetry_endpoints_
static outcome::result< Blob< size_ > > fromHex(std::string_view hex)
Definition: blob.hpp:186