diff --git a/src/app/configuration.hpp b/src/app/configuration.hpp index 922b7979..26bf1e9c 100644 --- a/src/app/configuration.hpp +++ b/src/app/configuration.hpp @@ -13,13 +13,12 @@ #include #include #include -#include #include "app/validator_keys_manifest.hpp" #include "crypto/xmss/xmss_provider.hpp" namespace lean::app { - class Configuration : Singleton { + class Configuration { public: using Endpoint = boost::asio::ip::tcp::endpoint; diff --git a/src/app/configurator.cpp b/src/app/configurator.cpp index fef51eb1..785d47e3 100644 --- a/src/app/configurator.cpp +++ b/src/app/configurator.cpp @@ -121,7 +121,6 @@ namespace lean::app { config_->database_.directory = "db"; config_->database_.cache_size = 512 << 20; // 512MiB - config_->metrics_.endpoint = {boost::asio::ip::address_v4::any(), 9615}; config_->metrics_.enabled = std::nullopt; namespace po = boost::program_options; @@ -196,6 +195,8 @@ namespace lean::app { ("version,v", "show version") ("config,c", po::value(), "config-file path") ("log,l", po::value>(), "Sets a custom logging filter") + ("api-host", po::value(), "Set address for OpenMetrics over HTTP.") + ("api-port", po::value(), "Set port for OpenMetrics over HTTP.") ; // clang-format on @@ -250,6 +251,9 @@ namespace lean::app { logger_cli_args_ = vm["log"].as>(); } + BOOST_OUTCOME_TRY( + parseEndpoint(config_->api_endpoint_, vm, "api-host", "api-port")); + return false; } @@ -294,6 +298,10 @@ namespace lean::app { return load_default(); } + const boost::asio::ip::tcp::endpoint &Configurator::apiEndpoint() const { + return config_->apiEndpoint(); + } + outcome::result> Configurator::calculateConfig( qtils::SharedRef logger) { logger_ = std::move(logger); @@ -779,9 +787,6 @@ namespace lean::app { config_->metrics_.enabled = false; } - BOOST_OUTCOME_TRY(parseEndpoint( - config_->api_endpoint_, cli_values_map_, "api-host", "api-port")); - if (not config_->node_key_.has_value()) { config_->node_key_ = randomKeyPair(); SL_INFO(logger_, "Generating random node key"); diff --git a/src/app/configurator.hpp b/src/app/configurator.hpp index 4d58ba50..590ccf4c 100644 --- a/src/app/configurator.hpp +++ b/src/app/configurator.hpp @@ -5,6 +5,7 @@ #pragma once +#include #include #include #include @@ -53,6 +54,7 @@ namespace lean::app { std::vector getLoggingCliArgs() { return logger_cli_args_; } + const boost::asio::ip::tcp::endpoint &apiEndpoint() const; outcome::result> calculateConfig( qtils::SharedRef logger); diff --git a/src/app/impl/http_server.cpp b/src/app/impl/http_server.cpp index d8d37d93..e0cce1b3 100644 --- a/src/app/impl/http_server.cpp +++ b/src/app/impl/http_server.cpp @@ -14,6 +14,7 @@ #include "app/chain_spec.hpp" #include "app/configuration.hpp" #include "app/state_manager.hpp" +#include "blockchain/block_tree.hpp" #include "blockchain/fork_choice_mutex.hpp" #include "metrics/handler.hpp" #include "serde/json.hpp" @@ -23,8 +24,6 @@ #include "utils/http.hpp" namespace lean::app { - constexpr auto *kContentTypeJson = "application/json; charset=utf-8"; - struct Enabled { bool enabled; @@ -37,11 +36,13 @@ namespace lean::app { qtils::SharedRef app_config, qtils::SharedRef metrics_handler, qtils::SharedRef chain_spec, + qtils::SharedRef block_tree, qtils::SharedRef fork_choice_store) : log_{logsys->getLogger("HttpServer", "http")}, app_config_{std::move(app_config)}, metrics_handler_{std::move(metrics_handler)}, chain_spec_{std::move(chain_spec)}, + block_tree_{std::move(block_tree)}, fork_choice_store_{std::move(fork_choice_store)} { state_manager->takeControl(*this); } @@ -93,11 +94,8 @@ namespace lean::app { std::string_view{request.method_string()}, url); if (url == "/lean/v0/health") { - response.set(boost::beast::http::field::content_type, - kContentTypeJson); - response.body() = - R"({"status":"healthy","service":"lean-rpc-api"})"; - return response; + return http::respondJson( + R"({"status":"healthy","service":"lean-rpc-api"})"); } if (url == "/lean/v0/states/finalized") { auto finalized = self->fork_choice_store_->getLatestFinalized(); @@ -115,34 +113,27 @@ namespace lean::app { } if (url == "/lean/v0/checkpoints/justified") { auto justified = self->fork_choice_store_->getLatestJustified(); - response.set(boost::beast::http::field::content_type, - kContentTypeJson); - response.body() = std::format(R"({{"root":"0x{}","slot":{}}})", - justified.root.toHex(), - justified.slot); - return response; + return http::respondJson( + std::format(R"({{"root":"0x{}","slot":{}}})", + justified.root.toHex(), + justified.slot)); } if (url == "/lean/v0/fork_choice") { if (auto result_res = self->fork_choice_store_->apiForkChoice()) { auto &result = result_res.value(); - response.set(boost::beast::http::field::content_type, - kContentTypeJson); - response.body() = json::encode(json::NameCase::SNAKE, result); - } else { - response.result( - boost::beast::http::status::internal_server_error); + return http::respondJson( + json::encode(json::NameCase::SNAKE, result)); } + response.result( + boost::beast::http::status::internal_server_error); return response; } if (url == "/lean/v0/admin/aggregator") { if (request.method() == boost::beast::http::verb::get) { auto is_aggregator = self->chain_spec_->isAggregator(); - response.set(boost::beast::http::field::content_type, - kContentTypeJson); - response.body() = - std::format(R"({{"is_aggregator":{}}})", is_aggregator); - return response; + return http::respondJson( + std::format(R"({{"is_aggregator":{}}})", is_aggregator)); } if (request.method() == boost::beast::http::verb::post) { Enabled body; @@ -155,14 +146,30 @@ namespace lean::app { } auto enabled = body.enabled; auto previous = self->chain_spec_->setIsAggregator(enabled); - response.set(boost::beast::http::field::content_type, - kContentTypeJson); - response.body() = + return http::respondJson( std::format(R"({{"is_aggregator":{},"previous":{}}})", enabled, - previous); + previous)); + } + } + if (url == "/lean/v0/blocks/finalized") { + auto finalized = self->fork_choice_store_->getLatestFinalized(); + auto block_result = + self->block_tree_->tryGetSignedBlock(finalized.root); + if (not block_result.has_value()) { + response.result( + boost::beast::http::status::internal_server_error); + return response; + } + auto &block = block_result.value(); + if (not block.has_value()) { + response.result(boost::beast::http::status::not_found); return response; } + response.set(boost::beast::http::field::content_type, + "application/octet-stream"); + response.body() = qtils::byte2str(encode(*block).value()); + return response; } response.result(boost::beast::http::status::not_found); return response; diff --git a/src/app/impl/http_server.hpp b/src/app/impl/http_server.hpp index b320fe61..eafb71b7 100644 --- a/src/app/impl/http_server.hpp +++ b/src/app/impl/http_server.hpp @@ -24,6 +24,10 @@ namespace lean::app { class ChainSpec; } // namespace lean::app +namespace lean::blockchain { + class BlockTree; +} // namespace lean::blockchain + namespace lean::metrics { class Handler; } // namespace lean::metrics @@ -39,6 +43,7 @@ namespace lean::app { qtils::SharedRef app_config, qtils::SharedRef metrics_handler, qtils::SharedRef chain_spec, + qtils::SharedRef block_tree, qtils::SharedRef fork_choice_store); ~HttpServer(); @@ -50,6 +55,7 @@ namespace lean::app { qtils::SharedRef app_config_; qtils::SharedRef metrics_handler_; qtils::SharedRef chain_spec_; + qtils::SharedRef block_tree_; qtils::SharedRef fork_choice_store_; std::shared_ptr io_context_; std::optional io_thread_; diff --git a/src/blockchain/block_production_metrics.def b/src/blockchain/block_production_metrics.def index 2e30b225..fd9541cc 100644 --- a/src/blockchain/block_production_metrics.def +++ b/src/blockchain/block_production_metrics.def @@ -25,3 +25,29 @@ METRIC_COUNTER(lean_block_building_success_total, METRIC_COUNTER(lean_block_building_failures_total, "lean_block_building_failures_total", "Failed block builds (exception in build_block)"); + +// https://github.com/leanEthereum/leanSpec/pull/753 +// phase = "select_payloads" | "compact" | "stf_simulate" +METRIC_HISTOGRAM_LABELS( + lean_block_proposal_attestation_build_phase_seconds, + "lean_block_proposal_attestation_build_phase_seconds", + "Phase-level time in block-proposal attestation selection: select_payloads (greedy child-payload pick), compact (recursive merge of proofs per AttestationData), stf_simulate (candidate block STF).", + (0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2, 4, 8), + ({"phase"})); +METRIC_COUNTER( + lean_block_proposal_attestation_builds_total, + "lean_block_proposal_attestation_builds_total", + "Completed block-proposal attestation selection runs (one per proposal attempt)."); +METRIC_COUNTER( + lean_block_proposal_child_payloads_consumed_total, + "lean_block_proposal_child_payloads_consumed_total", + "Child aggregated payloads selected during greedy proof picking (before recursive compaction)."); +METRIC_HISTOGRAM(lean_block_proposal_attestation_data_selected, + "lean_block_proposal_attestation_data_selected", + "Distinct AttestationData entries in the proposal block body.", + (0, 1, 2, 4, 8, 16, 32)); +METRIC_HISTOGRAM( + lean_block_proposal_aggregates_selected, + "lean_block_proposal_aggregates_selected", + "Aggregated signature proofs in the proposal result after compaction.", + (0, 1, 2, 4, 8, 16, 32, 64, 128)); diff --git a/src/blockchain/block_tree.hpp b/src/blockchain/block_tree.hpp index c03bc305..533c093c 100644 --- a/src/blockchain/block_tree.hpp +++ b/src/blockchain/block_tree.hpp @@ -169,7 +169,7 @@ namespace lean::blockchain { * "/leanconsensus/req/blocks_by_root/1/ssz_snappy" protocol. */ virtual outcome::result> tryGetSignedBlock( - const BlockHash block_hash) const = 0; + const BlockHash &block_hash) const = 0; }; } // namespace lean::blockchain diff --git a/src/blockchain/fork_choice.cpp b/src/blockchain/fork_choice.cpp index 528920ff..3a4b12e9 100644 --- a/src/blockchain/fork_choice.cpp +++ b/src/blockchain/fork_choice.cpp @@ -414,6 +414,13 @@ namespace lean { BOOST_OUTCOME_TRY(auto aggregated, getProposalAttestations(slot, proposer_index, head_root)); + metrics_->lean_block_proposal_attestation_builds_total()->inc(); + + aggregated = + aggregateDuplicate(*head_state, aggregated.first, aggregated.second); + + metrics_->lean_block_proposal_aggregates_selected()->observe( + aggregated.second.size()); // Create the final block with all collected attestations Block block{ @@ -485,20 +492,30 @@ namespace lean { AggregatedAttestations aggregated_attestations; AttestationSignatures aggregated_proofs; auto expected_source = head_state->latest_justified; + size_t processed_att_data = 0; for (auto &data : sorted_data) { + auto time_select = + metrics_ + ->lean_block_proposal_attestation_build_phase_seconds( + {{"phase", "select_payloads"}}) + ->timerManual(); + if (processed_att_data >= MAX_ATTESTATIONS_DATA) { + break; + } if (data.source != expected_source) { continue; } auto attestations_it = attestations_by_data_.find(sszHash(data)); if (attestations_it == attestations_by_data_.end()) { - break; + continue; } auto &attestations = attestations_it->second; // TODO(zeam): producer may aggregate if (attestations.proofs.empty()) { - break; + continue; } + ++processed_att_data; for (auto &proof : attestations.proofs) { aggregated_attestations.push_back({ .aggregation_bits = proof.participants, @@ -507,6 +524,11 @@ namespace lean { aggregated_proofs.push_back(proof); } + time_select(); + auto time_stf = metrics_ + ->lean_block_proposal_attestation_build_phase_seconds( + {{"phase", "stf_simulate"}}) + ->timer(); auto post_state = *head_state; BOOST_OUTCOME_TRY(stf_.processSlots(post_state, slot)); BOOST_OUTCOME_TRY(stf_.processBlock( @@ -518,11 +540,56 @@ namespace lean { .state_root = {}, .body = {.attestations = aggregated_attestations}, })); + time_stf.stop(); expected_source = post_state.latest_justified; } + metrics_->lean_block_proposal_attestation_data_selected()->observe( + processed_att_data); + metrics_->lean_block_proposal_child_payloads_consumed_total()->inc( + aggregated_attestations.size()); return std::make_pair(aggregated_attestations, aggregated_proofs); } + std::pair + ForkChoiceStore::aggregateDuplicate( + const State &state, + const AggregatedAttestations &attestations, + const AttestationSignatures &signatures) { + auto metric_time = + metrics_ + ->lean_block_proposal_attestation_build_phase_seconds( + {{"phase", "compact"}}) + ->timer(); + using Group = + std::pair>; + std::unordered_map groups; + for (auto &&[attestation, proof] : + std::views::zip(attestations, signatures)) { + auto key = sszHash(attestation.data); + auto it = groups.find(key); + if (it == groups.end()) { + it = groups.emplace(key, Group{attestation.data, {}}).first; + } + it->second.second.emplace_back(proof); + } + AggregatedAttestations new_attestations; + AttestationSignatures new_signatures; + for (auto &[data, proofs] : groups | std::views::values) { + auto proof = proofs.at(0); + if (proofs.size() != 1) { + proof = aggregateSignatures( + state, data, std::map{}, proofs) + .proof; + } + new_attestations.push_back({ + .aggregation_bits = proof.participants, + .data = data, + }); + new_signatures.push_back(proof); + } + return std::make_pair(new_attestations, new_signatures); + } + outcome::result ForkChoiceStore::validateAttestation( const Attestation &attestation) { auto has_block = [&](const BlockHash &hash) { @@ -831,6 +898,10 @@ namespace lean { auto &parent_state = *parent_state_res.value(); const auto &validators = parent_state.validators; + if (block.proposer_index >= validators.size()) { + return false; + } + for (auto &&[aggregated_attestation, aggregated_signature] : std::views::zip(aggregated_attestations, attestation_signatures)) { if (not validateAggregatedSignature(parent_state, @@ -1406,48 +1477,60 @@ namespace lean { continue; } auto &state = *state_res.value(); - std::vector> child_public_keys; - std::vector child_proofs; + auto aggregated = aggregateSignatures(state, + attestations.data, + attestations.signatures, + attestations.proofs); + aggregated_attestations.emplace_back(aggregated); + attestations.signatures.clear(); + attestations.proofs.clear(); + attestations.proofs.emplace_back(aggregated.proof); + } + return aggregated_attestations; + } + + SignedAggregatedAttestation ForkChoiceStore::aggregateSignatures( + const State &state, + const AttestationData &data, + const auto &signatures_in, + const auto &proofs_in) { + std::vector> child_public_keys; + std::vector child_proofs; + std::vector public_keys; + std::vector signatures; + AggregationBits participants; + for (auto &[validator_id, signature] : signatures_in) { + public_keys.emplace_back( + state.validators.data().at(validator_id).attestation_pubkey); + signatures.emplace_back(signature); + participants.add(validator_id); + } + for (auto &proof : proofs_in) { std::vector public_keys; - std::vector signatures; - AggregationBits participants; - for (auto &[validator_id, signature] : attestations.signatures) { + for (auto &&validator_id : proof.participants.iter()) { public_keys.emplace_back( state.validators.data().at(validator_id).attestation_pubkey); - signatures.emplace_back(signature); participants.add(validator_id); } - for (auto &proof : attestations.proofs) { - std::vector public_keys; - for (auto &&validator_id : proof.participants.iter()) { - public_keys.emplace_back( - state.validators.data().at(validator_id).attestation_pubkey); - participants.add(validator_id); - } - child_public_keys.emplace_back(std::move(public_keys)); - child_proofs.emplace_back(proof.proof_data); - } - auto payload = attestationPayload(attestations.data); - auto aggregated_signature = - xmss_provider_->aggregateSignatures(child_public_keys, - child_proofs, - public_keys, - signatures, - attestations.data.slot, - payload); - AggregatedSignatureProof proof{ - .participants = participants, - .proof_data = aggregated_signature, - }; - aggregated_attestations.emplace_back(SignedAggregatedAttestation{ - .data = attestations.data, - .proof = proof, - }); - attestations.signatures.clear(); - attestations.proofs.clear(); - attestations.proofs.emplace_back(proof); + child_public_keys.emplace_back(std::move(public_keys)); + child_proofs.emplace_back(proof.proof_data); } - return aggregated_attestations; + auto payload = attestationPayload(data); + auto aggregated_signature = + xmss_provider_->aggregateSignatures(child_public_keys, + child_proofs, + public_keys, + signatures, + data.slot, + payload); + return SignedAggregatedAttestation{ + .data = data, + .proof = + { + .participants = participants, + .proof_data = aggregated_signature, + }, + }; } void ForkChoiceStore::prune(Slot finalized_slot) { diff --git a/src/blockchain/fork_choice.hpp b/src/blockchain/fork_choice.hpp index f112f549..3673f192 100644 --- a/src/blockchain/fork_choice.hpp +++ b/src/blockchain/fork_choice.hpp @@ -306,6 +306,15 @@ namespace lean { getProposalAttestations(Slot slot, ValidatorIndex proposer_index, BlockHash parent_root); + /** + * Aggregate duplicate attestations referencing same data. + * Groups aggregated attestations by data and recursively aggregates their + * proofs. + */ + std::pair aggregateDuplicate( + const State &state, + const AggregatedAttestations &attestations, + const AttestationSignatures &signatures); /** * Produce an attestation for the given slot and validator. @@ -486,6 +495,10 @@ namespace lean { const AggregatedSignatureProof &signature) const; std::vector aggregateSignatures(); + SignedAggregatedAttestation aggregateSignatures(const State &state, + const AttestationData &data, + const auto &signatures_in, + const auto &proofs_in); void prune(Slot finalized_slot); void updateMetricGossipSignatures(); diff --git a/src/blockchain/impl/block_tree_impl.cpp b/src/blockchain/impl/block_tree_impl.cpp index 4e95a005..c062b152 100644 --- a/src/blockchain/impl/block_tree_impl.cpp +++ b/src/blockchain/impl/block_tree_impl.cpp @@ -762,7 +762,7 @@ namespace lean::blockchain { } outcome::result> BlockTreeImpl::tryGetSignedBlock( - const BlockHash block_hash) const { + const BlockHash &block_hash) const { return block_tree_data_.sharedAccess( [&](const BlockTreeData &p) -> outcome::result> { diff --git a/src/blockchain/impl/block_tree_impl.hpp b/src/blockchain/impl/block_tree_impl.hpp index 13030c08..c102d650 100644 --- a/src/blockchain/impl/block_tree_impl.hpp +++ b/src/blockchain/impl/block_tree_impl.hpp @@ -108,7 +108,7 @@ namespace lean::blockchain { Checkpoint getLatestJustified() const override; outcome::result> tryGetSignedBlock( - const BlockHash block_hash) const override; + const BlockHash &block_hash) const override; // BlockHeaderRepository methods diff --git a/tests/mock/clock/manual_clock.hpp b/src/clock/manual_clock.hpp similarity index 100% rename from tests/mock/clock/manual_clock.hpp rename to src/clock/manual_clock.hpp diff --git a/src/commands/test_driver.hpp b/src/commands/test_driver.hpp new file mode 100644 index 00000000..0caa9631 --- /dev/null +++ b/src/commands/test_driver.hpp @@ -0,0 +1,581 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include +#include + +#include "app/chain_spec.hpp" +#include "blockchain/block_storage.hpp" +#include "blockchain/fork_choice.hpp" +#include "blockchain/impl/anchor_block_impl.hpp" +#include "blockchain/impl/anchor_state_impl.hpp" +#include "blockchain/validator_registry.hpp" +#include "clock/manual_clock.hpp" +#include "crypto/xmss/xmss_provider_impl.hpp" +#include "log/logger.hpp" +#include "metrics/metrics_mock.hpp" +#include "serde/json.hpp" +#include "types/fork_choice_test_json.hpp" +#include "types/signed_block.hpp" +#include "types/state.hpp" +#include "utils/http.hpp" + +#define USING_(ns, name) using name = ns::name + +#define MOCK_UNUSED \ + override { \ + throw std::logic_error{ \ + fmt::format("unused mock function {}", __PRETTY_FUNCTION__)}; \ + } + +struct ValidatorRegistryMock : lean::ValidatorRegistry { + const ValidatorIndices ¤tValidatorIndices() const override { + return current_validator_indices_; + } + ValidatorIndices allValidatorsIndices() const MOCK_UNUSED; + std::optional nodeIdByIndex( + lean::ValidatorIndex index) const override { + return std::format("node_{}", index); + } + std::optional validatorIndicesForNodeId( + std::string_view node_id) const MOCK_UNUSED; + + ValidatorIndices current_validator_indices_{1}; +}; + +struct ChainSpecMock : lean::app::ChainSpec { + const lean::app::Bootnodes &getBootnodes() const MOCK_UNUSED; + bool isAggregator() const override { + return true; + } + bool setIsAggregator(bool is_aggregator) MOCK_UNUSED; +}; + +struct ConfigurationMock : lean::app::Configuration { + uint64_t cliSubnetCount() const override { + return 1; + } +}; + +struct ValidatorKeysManifestMock : lean::app::ValidatorKeysManifest { + std::optional getKeypair( + const lean::crypto::xmss::XmssPublicKey &public_key) const override { + return {}; + } + std::vector getAllXmssPubkeys() + const override { + return {}; + } +}; + +struct XmssProviderMock : lean::crypto::xmss::XmssProvider { + lean::crypto::xmss::XmssKeypair generateKeypair( + uint64_t activation_epoch, uint64_t num_active_epochs) MOCK_UNUSED; + lean::crypto::xmss::XmssSignature sign( + lean::crypto::xmss::XmssPrivateKey xmss_private_key, + uint32_t epoch, + const lean::crypto::xmss::XmssMessage &message) override { + return {}; + } + bool verify( + const lean::crypto::xmss::XmssPublicKey &xmss_public_key, + const lean::crypto::xmss::XmssMessage &message, + uint32_t epoch, + const lean::crypto::xmss::XmssSignature &xmss_signature) override { + return true; + } + lean::crypto::xmss::XmssAggregatedSignature aggregateSignatures( + std::span> + child_public_keys, + std::span child_proofs, + std::span public_keys, + std::span signatures, + uint32_t epoch, + const lean::crypto::xmss::XmssMessage &message) const override { + return {}; + } + bool verifyAggregatedSignatures( + std::span public_keys, + uint32_t epoch, + const lean::crypto::xmss::XmssMessage &message, + lean::crypto::xmss::XmssAggregatedSignatureIn aggregated_signature) + const override { + return true; + } +}; + +struct BlockTreeMock : lean::blockchain::BlockTree { + USING_(lean, BlockHash); + USING_(lean, BlockHeader); + USING_(lean, BlockIndex); + USING_(lean, BlockBody); + USING_(lean, SignedBlock); + USING_(lean, Checkpoint); + + // BlockHeaderRepository + outcome::result getSlotByHash( + const BlockHash &block_hash) const override { + return blocks_.at(block_hash).slot; + } + outcome::result getBlockHeader( + const BlockHash &block_hash) const override { + return blocks_.at(block_hash); + } + outcome::result> tryGetBlockHeader( + const BlockHash &block_hash) const override { + return blocks_.at(block_hash); + } + + // BlockTree + const BlockHash &getGenesisBlockHash() const MOCK_UNUSED; + bool has(const BlockHash &hash) const override { + return blocks_.contains(hash); + } + outcome::result getBlockBody(const BlockHash &) const MOCK_UNUSED; + outcome::result addBlockHeader(const BlockHeader &) MOCK_UNUSED; + outcome::result addBlockBody(const BlockHash &, + const BlockBody &) MOCK_UNUSED; + outcome::result addExistingBlock(const BlockHash &, + const BlockHeader &) MOCK_UNUSED; + outcome::result addBlock(SignedBlock block) override { + auto header = block.block.getHeader(); + header.updateHash(); + blocks_.emplace(header.hash(), header); + children_[header.parent_root].emplace_back(header.hash()); + return outcome::success(); + } + outcome::result removeLeaf(const BlockHash &) MOCK_UNUSED; + outcome::result finalize(const BlockHash &hash) override { + last_finalized_ = blocks_.at(hash).index(); + return outcome::success(); + } + outcome::result setJustified(const BlockHash &hash) override { + last_justified_ = blocks_.at(hash).index(); + return outcome::success(); + } + outcome::result> getBestChainFromBlock( + const BlockHash &, uint64_t) const MOCK_UNUSED; + outcome::result> getDescendingChainToBlock( + const BlockHash &, uint64_t) const MOCK_UNUSED; + bool isFinalized(const BlockIndex &) const MOCK_UNUSED; + BlockIndex bestBlock() const MOCK_UNUSED; + outcome::result getBestContaining(const BlockHash &) const + MOCK_UNUSED; + std::vector getLeaves() const MOCK_UNUSED; + outcome::result> getChildren( + const BlockHash &hash) const override { + std::vector children; + if (auto it = children_.find(hash); it != children_.end()) { + children = it->second; + } + return children; + } + BlockIndex lastFinalized() const override { + return last_finalized_; + } + Checkpoint getLatestJustified() const override { + return last_justified_; + } + outcome::result> tryGetSignedBlock( + const BlockHash &) const MOCK_UNUSED; + + lean::BlockIndex last_finalized_; + lean::BlockIndex last_justified_; + std::unordered_map blocks_; + std::unordered_map> children_; +}; + +struct BlockStorageMock : lean::blockchain::BlockStorage { + USING_(lean, BlockHash); + USING_(lean, BlockHeader); + USING_(lean, BlockBody); + USING_(lean, BlockData); + USING_(lean, BlockIndex); + USING_(lean, Slot); + USING_(lean, SignedBlock); + USING_(lean, State); + + // BlockStorage + outcome::result setBlockTreeLeaves(std::vector) MOCK_UNUSED; + outcome::result> getBlockTreeLeaves() const + MOCK_UNUSED; + outcome::result assignHashToSlot(const BlockIndex &) MOCK_UNUSED; + outcome::result deassignHashToSlot(const BlockIndex &) MOCK_UNUSED; + outcome::result> getBlockHash(Slot) const MOCK_UNUSED; + outcome::result seekLastSlot() const MOCK_UNUSED; + outcome::result hasBlockHeader(const BlockHash &) const MOCK_UNUSED; + outcome::result putBlockHeader(const BlockHeader &) MOCK_UNUSED; + outcome::result getBlockHeader(const BlockHash &) const + MOCK_UNUSED; + outcome::result> tryGetBlockHeader( + const BlockHash &) const MOCK_UNUSED; + outcome::result putBlockBody(const BlockHash &, + const BlockBody &) MOCK_UNUSED; + outcome::result> getBlockBody( + const BlockHash &) const MOCK_UNUSED; + outcome::result removeBlockBody(const BlockHash &) MOCK_UNUSED; + outcome::result putBlock(const BlockData &) MOCK_UNUSED; + outcome::result putState(const BlockHash &block_hash, + const State &state) override { + states_.emplace(block_hash, state); + return outcome::success(); + } + outcome::result> getState( + const BlockHash &block_hash) const override { + return states_.at(block_hash); + } + outcome::result removeState(const BlockHash &block_hash) MOCK_UNUSED; + outcome::result getBlock(const BlockHash &, + BlockParts) const MOCK_UNUSED; + outcome::result removeBlock(const BlockHash &) MOCK_UNUSED; + outcome::result getSignedBlock(const BlockHash &) const + MOCK_UNUSED; + + std::unordered_map states_; +}; + +struct VerifySignaturesRequest { + lean::State anchor_state; + lean::SignedBlock signed_block; + + JSON_FIELDS(anchor_state, signed_block); +}; + +struct VerifySignaturesResponse { + bool succeeded; + std::optional error; + + JSON_FIELDS(succeeded, error); +}; + +struct StateTransitionRequest { + lean::State pre; + std::vector blocks; + + JSON_FIELDS(pre, blocks); +}; + +struct StateTransitionPost { + lean::Slot slot; + lean::Slot latest_block_header_slot; + lean::BlockHash latest_block_header_state_root; + size_t historical_block_hashes_count; + + JSON_FIELDS(slot, + latest_block_header_slot, + latest_block_header_state_root, + historical_block_hashes_count); +}; + +struct StateTransitionResponse { + bool succeeded; + std::optional error; + std::optional post; + + JSON_FIELDS(succeeded, error, post); +}; + +struct ForkChoiceInit { + lean::State anchor_state; + lean::Block anchor_block; + + JSON_FIELDS(anchor_state, anchor_block); +}; + +struct DriverSnapshot { + lean::Slot head_slot; + lean::BlockHash head_root; + uint64_t time; + lean::Checkpoint justified_checkpoint; + lean::Checkpoint finalized_checkpoint; + lean::BlockHash safe_target; + + JSON_FIELDS(head_slot, + head_root, + time, + justified_checkpoint, + finalized_checkpoint, + safe_target); +}; + +struct StepResponse { + bool accepted; + std::optional error; + DriverSnapshot snapshot; + + JSON_FIELDS(accepted, error, snapshot); +}; + +template +lean::http::Response httpJson(const lean::http::Request &request, auto &&f) { + lean::http::Response response; + RequestJson request_json; + try { + lean::json::decode( + lean::json::NameCase::CAMEL, request_json, request.body()); + } catch (std::exception &e) { + response.result(boost::beast::http::status::bad_request); + response.body() = e.what(); + return response; + } + auto call = [&] { return f(request_json); }; + if constexpr (std::is_void_v) { + call(); + response.result(boost::beast::http::status::no_content); + return response; + } else { + auto response_json = call(); + return lean::http::respondJson( + lean::json::encode(lean::json::NameCase::CAMEL, response_json)); + } +} + +struct ForkChoiceDriver { + using BlockHash = lean::BlockHash; + + ForkChoiceDriver(std::shared_ptr logsys, + lean::State init_state) { + anchor_state_ = + std::make_shared(init_state); + anchor_block_ = + std::make_shared(*anchor_state_); + + auto block_tree = std::make_shared(); + block_tree->last_finalized_ = anchor_block_->index(); + block_tree->last_justified_ = block_tree->last_finalized_; + block_tree->blocks_.emplace(anchor_block_->hash(), *anchor_block_); + + auto block_storage = std::make_shared(); + block_storage->states_.emplace(anchor_block_->hash(), *anchor_state_); + + store_.emplace(anchor_state_, + anchor_block_, + std::make_shared(), + logsys, + std::make_shared(), + std::make_shared(), + std::make_shared(), + std::make_shared(), + std::make_shared(), + std::make_shared(), + block_tree, + block_storage); + store_->dontPropose(); + } + + lean::ValidatorRegistry::ValidatorIndices validator_indices_{0}; + std::shared_ptr anchor_state_; + std::shared_ptr anchor_block_; + std::optional store_; +}; + +inline int cmdTestDriver(std::shared_ptr logsys, + boost::asio::ip::tcp::endpoint api_endpoint) { + auto log = logsys->getLogger("TestDriver", "http"); + auto fork_choice = std::make_shared>(); + lean::http::ServerConfig config_api{ + .endpoint = api_endpoint, + .on_request = + [logsys, log, fork_choice](lean::http::Request request) { + lean::http::Response response; + std::string_view url{request.target()}; + SL_INFO( + log, "{} {}", std::string_view{request.method_string()}, url); + if (url == "/lean/v0/health") { + return lean::http::respondJson( + R"({"status":"healthy","service":"lean-rpc-api"})"); + } + if (url == "/lean/v0/test_driver/verify_signatures/run") { + return httpJson( + request, [&](VerifySignaturesRequest request) { + lean::ValidatorRegistry::ValidatorIndices validator_indices{ + 0}; + auto block_storage = std::make_shared(); + block_storage->states_.emplace( + request.signed_block.block.parent_root, + request.anchor_state); + lean::ForkChoiceStore store{ + {}, + logsys, + std::make_shared(), + {}, + {}, + {}, + {}, + {}, + 0, + std::make_shared(), + std::make_shared(), + std::make_shared< + lean::crypto::xmss::XmssProviderImpl>(), + std::make_shared(), + block_storage, + false, + 1, + }; + return VerifySignaturesResponse{ + .succeeded = + store.validateBlockSignatures(request.signed_block), + }; + }); + } + if (url == "/lean/v0/test_driver/state_transition/run") { + return httpJson( + request, [&](StateTransitionRequest request) { + lean::STF stf{ + logsys, + std::make_shared(), + std::make_shared(), + }; + auto stf_many = [&]() -> outcome::result { + auto state = request.pre; + for (auto &block : request.blocks) { + BOOST_OUTCOME_TRY( + state, stf.stateTransition(block, state, true)); + } + return state; + }; + auto state_result = stf_many(); + if (not state_result.has_value()) { + return StateTransitionResponse{ + .succeeded = false, + .error = state_result.error().message(), + }; + } + auto &state = state_result.value(); + return StateTransitionResponse{ + .succeeded = true, + .post = + StateTransitionPost{ + .slot = state.slot, + .latest_block_header_slot = + state.latest_block_header.slot, + .latest_block_header_state_root = + state.latest_block_header.state_root, + .historical_block_hashes_count = + state.historical_block_hashes.size(), + }, + }; + }); + } + if (url == "/lean/v0/test_driver/fork_choice/init") { + return httpJson( + request, [&](ForkChoiceInit request) { + fork_choice->emplace(logsys, request.anchor_state); + }); + } + if (url == "/lean/v0/test_driver/fork_choice/step") { + if (not fork_choice->has_value()) { + response.result(boost::beast::http::status::bad_request); + response.body() = + "\"/lean/v0/test_driver/fork_choice/init\" was not called"; + return response; + } + auto &store = fork_choice->value().store_.value(); + return httpJson( + request, [&](lean::ForkChoiceStep request) { + std::optional> result; + if (auto *tick_step = + std::get_if(&request.v)) { + std::chrono::milliseconds time; + if (tick_step->interval.has_value()) { + time = + (**fork_choice) + .anchor_state_->config.genesisTimeMs() + + *tick_step->interval * lean::INTERVAL_DURATION_MS; + } else if (tick_step->time.has_value()) { + time = std::chrono::seconds{*tick_step->time}; + } else { + throw std::runtime_error{ + "TickStep no interval or time"}; + } + result = [&]() -> outcome::result { + store.onTick(time); + return outcome::success(); + }(); + } else if (auto *block_step = + std::get_if(&request.v)) { + result = [&] { + auto &block = block_step->block; + lean::SignedBlock signed_block{ + .block = block, + .signature = {}, + }; + for (auto &attestation : block.body.attestations) { + signed_block.signature.attestation_signatures + .push_back({.participants = + attestation.aggregation_bits}); + } + auto block_time = + std::chrono::seconds{store.getConfig().genesis_time} + + block.slot * lean::SLOT_DURATION_MS; + store.onTick(block_time); + return store.onBlock(signed_block); + }(); + } else if (auto *attestation_step = + std::get_if( + &request.v)) { + result = [&] { + return store.onGossipAttestation( + attestation_step->attestation); + }(); + } else if (auto *aggregated_step = std::get_if< + lean::GossipAggregatedAttestationStep>( + &request.v)) { + result = [&] { + return store.onGossipAggregatedAttestation( + aggregated_step->attestation); + }(); + } + if (not result.value().has_value()) { + return StepResponse{ + .accepted = false, + .error = result->error().message(), + }; + } + auto head = store.getHead(); + return StepResponse{ + .accepted = true, + .snapshot = + DriverSnapshot{ + .head_slot = head.slot, + .head_root = head.root, + .time = store.time().interval, + .justified_checkpoint = + store.getLatestJustified(), + .finalized_checkpoint = + store.getLatestFinalized(), + .safe_target = store.getSafeTarget().root, + }, + }; + }); + } + response.result(boost::beast::http::status::not_found); + return response; + }, + .max_request_size = 16 << 20, + }; + boost::asio::io_context io_context; + if (auto res = lean::http::serve(log, io_context, config_api); + not res.has_value()) { + SL_WARN(log, + "listen api {}:{} error: {}", + api_endpoint.address().to_string(), + api_endpoint.port(), + res.error()); + return EXIT_FAILURE; + } + SL_INFO(log, + "listen api {}:{}", + api_endpoint.address().to_string(), + api_endpoint.port()); + io_context.run(); + return EXIT_SUCCESS; +} diff --git a/src/executable/lean_node.cpp b/src/executable/lean_node.cpp index 0b08b92e..18f9a5ad 100644 --- a/src/executable/lean_node.cpp +++ b/src/executable/lean_node.cpp @@ -20,6 +20,7 @@ #include "app/configurator.hpp" #include "commands/generate_genesis.hpp" #include "commands/key_generate_node_key.hpp" +#include "commands/test_driver.hpp" #include "injector/node_injector.hpp" #include "loaders/loader.hpp" #include "log/logger.hpp" @@ -93,12 +94,6 @@ int main(int argc, const char **argv, const char **env) { return EXIT_FAILURE; } - if (argc == 1) { - // Run without arguments - wrong_usage(); - return EXIT_FAILURE; - } - if (getArg(1) == "key" and getArg(2) == "generate-node-key") { cmdKeyGenerateNodeKey(); return EXIT_SUCCESS; @@ -157,6 +152,11 @@ int main(int argc, const char **argv, const char **env) { } } + if (auto *s = getenv("HIVE_LEAN_TEST_DRIVER"); + s != nullptr and std::string_view{s} == "1") { + return cmdTestDriver(logging_system, app_configurator->apiEndpoint()); + } + // Parse remaining args if (auto res = app_configurator->step2(); res.has_value()) { if (res.value()) { diff --git a/src/metrics/metrics.hpp b/src/metrics/metrics.hpp index 89383949..32b0a237 100644 --- a/src/metrics/metrics.hpp +++ b/src/metrics/metrics.hpp @@ -126,6 +126,8 @@ namespace lean::metrics { * @return HistogramTimer that records duration when it goes out of scope */ HistogramTimer timer(); + + auto timerManual(); }; /** @@ -276,4 +278,11 @@ namespace lean::metrics { return HistogramTimer(this); } + inline auto Histogram::timerManual() { + return [this, start_time{HistogramTimer::Clock::now()}] { + HistogramTimer::Duration elapsed = + HistogramTimer::Clock::now() - start_time; + observe(elapsed.count()); + }; + } } // namespace lean::metrics diff --git a/tests/mock/metrics_mock.hpp b/src/metrics/metrics_mock.hpp similarity index 100% rename from tests/mock/metrics_mock.hpp rename to src/metrics/metrics_mock.hpp diff --git a/src/serde/json.hpp b/src/serde/json.hpp index ca0b16b0..810a201e 100644 --- a/src/serde/json.hpp +++ b/src/serde/json.hpp @@ -89,6 +89,15 @@ namespace lean::json { } } + template + void encode(JsonOut json, const std::optional &v) { + if (v.has_value()) { + encode(json, *v); + } else { + json.v.SetNull(); + } + } + template void encode(JsonOut json, const qtils::ByteArr &v) { encode(json, fmt::format("{:0xx}", qtils::Hex{v})); diff --git a/tests/test_vectors/fork_choice_test_json.hpp b/src/types/fork_choice_test_json.hpp similarity index 100% rename from tests/test_vectors/fork_choice_test_json.hpp rename to src/types/fork_choice_test_json.hpp diff --git a/src/utils/http.cpp b/src/utils/http.cpp index 24a339ab..092a0e6f 100644 --- a/src/utils/http.cpp +++ b/src/utils/http.cpp @@ -84,4 +84,12 @@ namespace lean::http { }); return outcome::success(); } + + Response respondJson(std::string_view json) { + Response response; + response.set(boost::beast::http::field::content_type, + "application/json; charset=utf-8"); + response.body() = json; + return response; + } } // namespace lean::http diff --git a/src/utils/http.hpp b/src/utils/http.hpp index dd77d84d..0c8dddee 100644 --- a/src/utils/http.hpp +++ b/src/utils/http.hpp @@ -39,4 +39,6 @@ namespace lean::http { outcome::result serve(log::Logger log, boost::asio::io_context &io_context, ServerConfig config); + + Response respondJson(std::string_view json); } // namespace lean::http diff --git a/tests/mock/blockchain/block_tree_mock.hpp b/tests/mock/blockchain/block_tree_mock.hpp index af3b57ac..4c2ad899 100644 --- a/tests/mock/blockchain/block_tree_mock.hpp +++ b/tests/mock/blockchain/block_tree_mock.hpp @@ -104,7 +104,7 @@ namespace lean::blockchain { MOCK_METHOD(outcome::result>, tryGetSignedBlock, - (const BlockHash block_hash), + (const BlockHash &), (const, override)); }; diff --git a/tests/test_vectors/fork_choice_test.cpp b/tests/test_vectors/fork_choice_test.cpp index c22f384e..cd09b4b5 100644 --- a/tests/test_vectors/fork_choice_test.cpp +++ b/tests/test_vectors/fork_choice_test.cpp @@ -8,18 +8,18 @@ #include "blockchain/impl/anchor_block_impl.hpp" #include "blockchain/impl/anchor_state_impl.hpp" -#include "fork_choice_test_json.hpp" +#include "clock/manual_clock.hpp" +#include "metrics/metrics_mock.hpp" #include "mock/app/chain_spec_mock.hpp" #include "mock/app/configuration_mock.hpp" #include "mock/app/validator_keys_manifest_mock.hpp" #include "mock/blockchain/block_storage_mock.hpp" #include "mock/blockchain/block_tree_mock.hpp" #include "mock/blockchain/validator_registry_mock.hpp" -#include "mock/clock/manual_clock.hpp" #include "mock/crypto/xmss_provider_mock.hpp" -#include "mock/metrics_mock.hpp" #include "test_vectors.hpp" #include "testutil/prepare_loggers.hpp" +#include "types/fork_choice_test_json.hpp" using lean::BlockHash; using testing::_; diff --git a/tests/test_vectors/state_transition_test.cpp b/tests/test_vectors/state_transition_test.cpp index 55e4523d..8cbcf67b 100644 --- a/tests/test_vectors/state_transition_test.cpp +++ b/tests/test_vectors/state_transition_test.cpp @@ -5,8 +5,8 @@ */ #include "blockchain/state_transition_function.hpp" +#include "metrics/metrics_mock.hpp" #include "mock/blockchain/block_tree_mock.hpp" -#include "mock/metrics_mock.hpp" #include "state_transition_test_json.hpp" #include "test_vectors.hpp" #include "testutil/prepare_loggers.hpp" diff --git a/tests/test_vectors/verify_signatures_test.cpp b/tests/test_vectors/verify_signatures_test.cpp index e371fb47..69ced95b 100644 --- a/tests/test_vectors/verify_signatures_test.cpp +++ b/tests/test_vectors/verify_signatures_test.cpp @@ -6,11 +6,11 @@ #include "blockchain/fork_choice.hpp" #include "crypto/xmss/xmss_provider_impl.hpp" +#include "metrics/metrics_mock.hpp" #include "mock/app/validator_keys_manifest_mock.hpp" #include "mock/blockchain/block_storage_mock.hpp" #include "mock/blockchain/block_tree_mock.hpp" #include "mock/blockchain/validator_registry_mock.hpp" -#include "mock/metrics_mock.hpp" #include "test_vectors.hpp" #include "testutil/prepare_loggers.hpp" #include "verify_signatures_test_json.hpp" diff --git a/tests/unit/blockchain/fork_choice_test.cpp b/tests/unit/blockchain/fork_choice_test.cpp index 23f56e8a..42736755 100644 --- a/tests/unit/blockchain/fork_choice_test.cpp +++ b/tests/unit/blockchain/fork_choice_test.cpp @@ -13,12 +13,12 @@ #include "blockchain/block_tree.hpp" #include "blockchain/is_justifiable_slot.hpp" #include "blockchain/state_transition_function.hpp" +#include "metrics/metrics_mock.hpp" #include "mock/app/validator_keys_manifest_mock.hpp" #include "mock/blockchain/block_storage_mock.hpp" #include "mock/blockchain/block_tree_mock.hpp" #include "mock/blockchain/validator_registry_mock.hpp" #include "mock/crypto/xmss_provider_mock.hpp" -#include "mock/metrics_mock.hpp" #include "qtils/test/outcome.hpp" #include "testutil/prepare_loggers.hpp" #include "types/attestation.hpp" diff --git a/tests/unit/blockchain/state_transition_function_test.cpp b/tests/unit/blockchain/state_transition_function_test.cpp index 058de8e9..1f9bd104 100644 --- a/tests/unit/blockchain/state_transition_function_test.cpp +++ b/tests/unit/blockchain/state_transition_function_test.cpp @@ -10,8 +10,8 @@ #include "blockchain/impl/anchor_block_impl.hpp" #include "blockchain/impl/anchor_state_impl.hpp" +#include "metrics/metrics_mock.hpp" #include "mock/blockchain/block_tree_mock.hpp" -#include "mock/metrics_mock.hpp" #include "testutil/prepare_loggers.hpp" #include "types/config.hpp" #include "types/state.hpp"