diff --git a/DEPRECATED.md b/DEPRECATED.md index 0615c67..c7e0b5f 100644 --- a/DEPRECATED.md +++ b/DEPRECATED.md @@ -1,3 +1,10 @@ +[Spec](https://github.com/leanEthereum/leanSpec) uses names like +`get_vote_target`,\ +but they should be converted to `getVoteTarget`.\ +Yes, text search doesn't work. + +--- + Module system uses subscriptions and messages.\ For example: diff --git a/src/app/impl/timeline_impl.cpp b/src/app/impl/timeline_impl.cpp index 13d86c2..c640815 100644 --- a/src/app/impl/timeline_impl.cpp +++ b/src/app/impl/timeline_impl.cpp @@ -49,7 +49,10 @@ namespace lean::app { void TimelineImpl::start() { auto now = clock_->nowMsec(); - auto next_slot = (now - config_->genesis_time) / SLOT_DURATION_MS + 1; + auto next_slot = now > config_->genesis_time // somehow now could be less + // than genesis time + ? (now - config_->genesis_time) / SLOT_DURATION_MS + 1 + : 1; auto time_to_next_slot = config_->genesis_time + SLOT_DURATION_MS * next_slot - now; if (time_to_next_slot < SLOT_DURATION_MS / 2) { @@ -82,10 +85,52 @@ namespace lean::app { auto time_to_next_slot = config_->genesis_time + SLOT_DURATION_MS * next_slot - now; - SL_INFO(logger_, "Next slot is {} in {}ms", msg->slot, time_to_next_slot); + SL_INFO(logger_, "Next slot is {} in {}ms", next_slot, time_to_next_slot); + + const auto slot_start_abs = + config_->genesis_time + + SLOT_DURATION_MS * msg->slot; // in milliseconds + + auto abs_interval1 = slot_start_abs + SECONDS_PER_INTERVAL * 1000; + auto abs_interval2 = slot_start_abs + 2 * SECONDS_PER_INTERVAL * 1000; + auto abs_interval3 = slot_start_abs + 3 * SECONDS_PER_INTERVAL * 1000; + auto ms_to_abs = [&](uint64_t abs_time_ms) -> uint64_t { + return (abs_time_ms > now) ? (abs_time_ms - now) : 0ull; + }; + + // trigger interval 0 immediately + se_manager_->notify(EventTypes::SlotIntervalStarted, + std::make_shared( + 0, msg->slot, msg->epoch)); + + // schedule other intervals and next slot + auto time_to_interval_1 = ms_to_abs(abs_interval1); se_manager_->notifyDelayed( - std::chrono::milliseconds(time_to_next_slot), + std::chrono::milliseconds(time_to_interval_1), + EventTypes::SlotIntervalStarted, + std::make_shared( + 1, msg->slot, msg->epoch)); + + auto time_to_interval_2 = ms_to_abs(abs_interval2); + se_manager_->notifyDelayed( + std::chrono::milliseconds(time_to_interval_2), + EventTypes::SlotIntervalStarted, + std::make_shared( + 2, msg->slot, msg->epoch)); + + auto time_to_interval_3 = ms_to_abs(abs_interval3); + se_manager_->notifyDelayed( + std::chrono::milliseconds(time_to_interval_3), + EventTypes::SlotIntervalStarted, + std::make_shared( + 3, msg->slot, msg->epoch)); + + const auto next_slot_abs = + config_->genesis_time + SLOT_DURATION_MS * (msg->slot + 1); + auto time_to_next_slot_abs = ms_to_abs(next_slot_abs); + se_manager_->notifyDelayed( + std::chrono::milliseconds(time_to_next_slot_abs), EventTypes::SlotStarted, std::make_shared(msg->slot + 1, 0, false)); } diff --git a/src/blockchain/CMakeLists.txt b/src/blockchain/CMakeLists.txt index 51de4f2..cbf9ccb 100644 --- a/src/blockchain/CMakeLists.txt +++ b/src/blockchain/CMakeLists.txt @@ -5,11 +5,13 @@ # add_library(blockchain + fork_choice.cpp impl/block_storage_error.cpp impl/block_storage_impl.cpp impl/block_storage_initializer.cpp impl/block_tree_error.cpp impl/block_tree_impl.cpp + impl/fc_block_tree.cpp impl/block_tree_initializer.cpp impl/cached_tree.cpp impl/genesis_block_header_impl.cpp diff --git a/src/blockchain/fork_choice.cpp b/src/blockchain/fork_choice.cpp new file mode 100644 index 0000000..5ccea8c --- /dev/null +++ b/src/blockchain/fork_choice.cpp @@ -0,0 +1,517 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "blockchain/fork_choice.hpp" + +#include + +#include "types/signed_block.hpp" +#include "utils/__debug_env.hpp" + +namespace lean { + void ForkChoiceStore::updateSafeTarget() { + // 2/3rd majority min voting voting weight for target selection + auto min_target_score = ceilDiv(config_.num_validators * 2, 3); + + safe_target_ = getForkChoiceHead( + blocks_, latest_justified_, latest_new_votes_, min_target_score); + } + + std::optional ForkChoiceStore::getLatestJustified() { + using Key = std::tuple; + std::optional max; + for (auto &state : states_ | std::views::values) { + Key key{state.latest_justified.slot, state.latest_justified.root}; + if (not max.has_value() or key > max.value()) { + max = key; + } + } + if (not max.has_value()) { + return std::nullopt; + } + auto &[slot, hash] = max.value(); + if (slot == 0) { + for (auto &[hash, block] : blocks_) { + if (block.slot == 0) { + return Checkpoint{.root = hash, .slot = slot}; + } + } + } + return Checkpoint{.root = hash, .slot = slot}; + } + + void ForkChoiceStore::updateHead() { + if (auto latest_justified = getLatestJustified()) { + latest_justified_ = latest_justified.value(); + if (latest_justified_.slot == 0) { + for (auto &[hash, block] : blocks_) { + if (block.slot == 0) { + latest_justified_.root = hash; + } + } + } + } + head_ = + getForkChoiceHead(blocks_, latest_justified_, latest_known_votes_, 0); + + auto state_it = states_.find(head_); + if (state_it != states_.end()) { + latest_finalized_ = state_it->second.latest_finalized; + if (latest_finalized_.slot == 0) { + for (auto &[hash, block] : blocks_) { + if (block.slot == 0) { + latest_finalized_.root = hash; + } + } + } + } + } + + void ForkChoiceStore::acceptNewVotes() { + for (auto &[voter, vote] : latest_new_votes_) { + latest_known_votes_[voter] = vote; + } + latest_new_votes_.clear(); + updateHead(); + } + + Slot ForkChoiceStore::getCurrentSlot() { + Slot current_slot = time_ / INTERVALS_PER_SLOT; + return current_slot; + } + + + BlockHash ForkChoiceStore::getHead() { + return head_; + } + + const State &ForkChoiceStore::getState(const BlockHash &block_hash) const { + auto it = states_.find(block_hash); + if (it == states_.end()) { + throw std::out_of_range("No state for block hash"); + } + return it->second; + } + + bool ForkChoiceStore::hasBlock(const BlockHash &hash) const { + return blocks_.contains(hash); + } + + std::optional ForkChoiceStore::getBlockSlot( + const BlockHash &block_hash) const { + if (not blocks_.contains(block_hash)) { + return std::nullopt; + } + return blocks_.at(block_hash).slot; + } + + Slot ForkChoiceStore::getHeadSlot() const { + return blocks_.at(head_).slot; + } + + const Config &ForkChoiceStore::getConfig() const { + return config_; + } + + Checkpoint ForkChoiceStore::getLatestFinalized() const { + return latest_finalized_; + } + + + Checkpoint ForkChoiceStore::getVoteTarget() const { + // Start from head as target candidate + auto target_block_root = head_; + + // If there is no very recent safe target, then vote for the k'th ancestor + // of the head + for (auto i = 0; i < 3; ++i) { + if (blocks_.at(target_block_root).slot > blocks_.at(safe_target_).slot) { + target_block_root = blocks_.at(target_block_root).parent_root; + } + } + + // If the latest finalized slot is very far back, then only some slots are + // valid to justify, make sure the target is one of those + while (not isJustifiableSlot(latest_finalized_.slot, + blocks_.at(target_block_root).slot)) { + target_block_root = blocks_.at(target_block_root).parent_root; + } + + return Checkpoint{ + .root = target_block_root, + .slot = blocks_.at(target_block_root).slot, + }; + } + + outcome::result ForkChoiceStore::produceBlock( + Slot slot, ValidatorIndex validator_index) { + if (validator_index != slot % config_.num_validators) { + return Error::INVALID_PROPOSER; + } + const auto &head_root = getHead(); + const auto &head_state = getState(head_root); + + Block block{ + .slot = slot, + .proposer_index = validator_index, + .parent_root = head_root, + .state_root = {}, // to be filled after state transition + }; + for (auto &signed_vote : latest_known_votes_ | std::views::values) { + block.body.attestations.push_back(signed_vote); + } + BOOST_OUTCOME_TRY( + auto state, + stf_.stateTransition({.message = block}, head_state, false)); + block.state_root = sszHash(state); + block.setHash(); + + // Store block and state in forkchoice store + auto block_hash = block.hash(); + blocks_.emplace(block_hash, block); + states_.emplace(block_hash, std::move(state)); + + // update head (not in spec) + head_ = block_hash; + + return block; + } + + + outcome::result ForkChoiceStore::validateAttestation( + const SignedVote &signed_vote) { + SL_INFO(logger_, + "Validating attestation for target {}@{}, source {}@{}", + signed_vote.data.target.slot, + signed_vote.data.target.root, + signed_vote.data.source.slot, + signed_vote.data.source.root); + auto &vote = signed_vote.data; + + // Validate vote targets exist in store + if (not blocks_.contains(vote.source.root)) { + return Error::INVALID_ATTESTATION; + } + if (not blocks_.contains(vote.target.root)) { + return Error::INVALID_ATTESTATION; + } + + // Validate slot relationships + auto &source_block = blocks_.at(vote.source.root); + auto &target_block = blocks_.at(vote.target.root); + + if (source_block.slot > target_block.slot) { + return Error::INVALID_ATTESTATION; + } + if (vote.source.slot > vote.target.slot) { + return Error::INVALID_ATTESTATION; + } + + // Validate checkpoint slots match block slots + if (source_block.slot != vote.source.slot) { + return Error::INVALID_ATTESTATION; + } + if (target_block.slot != vote.target.slot) { + return Error::INVALID_ATTESTATION; + } + + // Validate attestation is not too far in the future + if (vote.slot > getCurrentSlot() + 1) { + return Error::INVALID_ATTESTATION; + } + + return outcome::success(); + } + + outcome::result ForkChoiceStore::processAttestation( + const SignedVote &signed_vote, bool is_from_block) { + // Validate attestation structure and constraints + BOOST_OUTCOME_TRY(validateAttestation(signed_vote)); + + auto &validator_id = signed_vote.data.validator_id; + auto &vote = signed_vote.data; + + if (is_from_block) { + // update latest known votes if this is latest + auto latest_known_vote = latest_known_votes_.find(validator_id); + if (latest_known_vote == latest_known_votes_.end() + or latest_known_vote->second.data.target.slot < vote.slot) { + latest_known_votes_.insert_or_assign(validator_id, signed_vote); + } + + // clear from new votes if this is latest + auto latest_new_vote = latest_new_votes_.find(validator_id); + if (latest_new_vote != latest_new_votes_.end() + and latest_new_vote->second.data.target.slot <= vote.target.slot) { + latest_new_votes_.erase(latest_new_vote); + } + } else { + // forkchoice should be correctly ticked to current time before importing + // gossiped attestations + if (vote.slot > getCurrentSlot() + 1) { + return Error::INVALID_ATTESTATION; + } + + // update latest new votes if this is the latest + auto latest_new_vote = latest_new_votes_.find(validator_id); + if (latest_new_vote == latest_new_votes_.end() + or latest_new_vote->second.data.target.slot < vote.target.slot) { + latest_new_votes_.insert_or_assign(validator_id, signed_vote); + } + } + + return outcome::success(); + } + + outcome::result ForkChoiceStore::onBlock(Block block) { + block.setHash(); + auto block_hash = block.hash(); + // If the block is already known, ignore it + if (blocks_.contains(block_hash)) { + return outcome::success(); + } + + auto &parent_state = states_.at(block.parent_root); + // at this point parent state should be available so node should sync parent + // chain if not available before adding block to forkchoice + + // Get post state from STF (State Transition Function) + auto state = + stf_.stateTransition({.message = block}, parent_state, true).value(); + blocks_.emplace(block_hash, block); + states_.emplace(block_hash, std::move(state)); + + // add block votes to the onchain known last votes + for (auto &signed_vote : block.body.attestations) { + // Add block votes to the onchain known last votes + BOOST_OUTCOME_TRY(processAttestation(signed_vote, true)); + } + + updateHead(); + + return outcome::success(); + } + + std::vector> + ForkChoiceStore::advanceTime(uint64_t now_sec) { + auto time_since_genesis = now_sec - config_.genesis_time / 1000; + + std::vector> result{}; + while (time_ < time_since_genesis) { + Slot current_slot = time_ / INTERVALS_PER_SLOT; + if (current_slot == 0) { + // Skip actions for slot zero, which is the genesis slot + time_ += 1; + continue; + } + if (time_ % INTERVALS_PER_SLOT == 0) { + // Slot start + SL_INFO(logger_, + "Slot {} started with time {}", + current_slot, + time_ * SECONDS_PER_INTERVAL); + auto producer_index = current_slot % config_.num_validators; + auto is_producer = validator_index_ == producer_index; + if (is_producer) { + acceptNewVotes(); + + auto res = produceBlock(current_slot, producer_index); + if (!res.has_value()) { + SL_ERROR(logger_, + "Failed to produce block for slot {}: {}", + current_slot, + res.error()); + continue; + } + const auto &new_block = res.value(); + + SignedBlock new_signed_block{.message = new_block, + .signature = qtils::ByteArr<32>{0}}; + + SL_INFO(logger_, + "Produced block for slot {} with parent {} state {}", + current_slot, + new_block.parent_root, + new_signed_block.message.state_root); + result.emplace_back(std::move(new_signed_block)); + } + } else if (time_ % INTERVALS_PER_SLOT == 1) { + // Interval one actions + auto head_root = getHead(); + auto head_slot = getBlockSlot(head_root); + BOOST_ASSERT_MSG(head_slot.has_value(), + "Head block must have a valid slot"); + Checkpoint head{.root = head_root, .slot = head_slot.value()}; + auto target = getVoteTarget(); + auto source = getLatestJustified(); + SL_INFO(logger_, + "For slot {}: head is {}@{}, target is {}@{}, source is {}@{}", + current_slot, + head.root, + head.slot, + target.root, + target.slot, + source->root, + source->slot); + SignedVote signed_vote{.data = + Vote{ + .validator_id = validator_index_, + .slot = current_slot, + .head = head, + .target = target, + .source = *source, + }, + // signature with zero bytes for now + .signature = qtils::ByteArr<32>{0}}; + + // Dispatching send signed vote only broadcasts to other peers. Current + // peer should process attestation directly + auto res = processAttestation(signed_vote, false); + BOOST_ASSERT_MSG(res.has_value(), "Produced vote should be valid"); + SL_INFO(logger_, + "Produced vote for target {}@{}", + signed_vote.data.target.slot, + signed_vote.data.target.root); + result.emplace_back(std::move(signed_vote)); + } else if (time_ % INTERVALS_PER_SLOT == 2) { + // Interval two actions + SL_INFO(logger_, + "Interval two of slot {} at time {}", + current_slot, + time_ * SECONDS_PER_INTERVAL); + updateSafeTarget(); + } else if (time_ % INTERVALS_PER_SLOT == 3) { + // Interval three actions + SL_INFO(logger_, + "Interval three of slot {} at time {}", + current_slot, + time_ * SECONDS_PER_INTERVAL); + acceptNewVotes(); + } + time_ += 1; + } + return result; + } + + + BlockHash getForkChoiceHead(const ForkChoiceStore::Blocks &blocks, + const Checkpoint &root, + const ForkChoiceStore::Votes &latest_votes, + uint64_t min_score) { + // If no votes, return the starting root immediately + if (latest_votes.empty()) { + return root.root; + } + + // For each block, count the number of votes for that block. A vote for + // any descendant of a block also counts as a vote for that block + std::unordered_map vote_weights; + auto get_weight = [&](const BlockHash &hash) { + auto it = vote_weights.find(hash); + return it != vote_weights.end() ? it->second : 0; + }; + + for (auto &vote : latest_votes | std::views::values) { + auto block_it = blocks.find(vote.data.target.root); + if (block_it != blocks.end()) { + while (block_it->second.slot > root.slot) { + ++vote_weights[block_it->first]; + block_it = blocks.find(block_it->second.parent_root); + BOOST_ASSERT(block_it != blocks.end()); + } + } + } + + // Identify the children of each block + using Key = std::tuple; + std::unordered_multimap children_map; + for (auto &[hash, block] : blocks) { + if (block.slot > root.slot and get_weight(hash) >= min_score) { + children_map.emplace(block.parent_root, Checkpoint::from(block)); + } + } + + // Start at the root (latest justified hash or genesis) and repeatedly + // choose the child with the most latest votes, tiebreaking by slot then + // hash + auto current = root.root; + while (true) { + auto [begin, end] = children_map.equal_range(current); + if (begin == end) { + return current; + } + Key max; + for (auto it = begin; it != end; ++it) { + Key key{ + get_weight(it->second.root), + it->second.slot, + it->second.root, + }; + if (it == begin or key > max) { + max = key; + } + } + current = std::get<2>(max); + } + } + + ForkChoiceStore::ForkChoiceStore( + const AnchorState &anchor_state, + const AnchorBlock &anchor_block, + qtils::SharedRef clock, + qtils::SharedRef logging_system) + : validator_index_(getPeerIndex()), + logger_( + logging_system->getLogger("ForkChoiceStore", "fork_choice_store")) { + BOOST_ASSERT(anchor_block.state_root == sszHash(anchor_state)); + anchor_block.setHash(); + auto anchor_root = anchor_block.hash(); + config_ = anchor_state.config; + auto now_sec = clock->nowSec(); + time_ = now_sec > config_.genesis_time + ? (now_sec - config_.genesis_time / 1000) / SECONDS_PER_INTERVAL + : 0; + head_ = anchor_root; + safe_target_ = anchor_root; + + // TODO: ensure latest justified and finalized are set correctly + latest_justified_ = Checkpoint::from(anchor_block); + latest_finalized_ = Checkpoint::from(anchor_block); + + blocks_.emplace(anchor_root, std::move(anchor_block)); + SL_INFO( + logger_, "Anchor block {} at slot {}", anchor_root, anchor_block.slot); + states_.emplace(anchor_root, std::move(anchor_state)); + } + + // Test constructor implementation + ForkChoiceStore::ForkChoiceStore( + uint64_t now_sec, + qtils::SharedRef logging_system, + Config config, + BlockHash head, + BlockHash safe_target, + Checkpoint latest_justified, + Checkpoint latest_finalized, + Blocks blocks, + std::unordered_map states, + Votes latest_known_votes, + Votes latest_new_votes, + ValidatorIndex validator_index) + : time_(now_sec / SECONDS_PER_INTERVAL), + logger_( + logging_system->getLogger("ForkChoiceStore", "fork_choice_store")), + config_(config), + head_(head), + safe_target_(safe_target), + latest_justified_(latest_justified), + latest_finalized_(latest_finalized), + blocks_(std::move(blocks)), + states_(std::move(states)), + latest_known_votes_(std::move(latest_known_votes)), + latest_new_votes_(std::move(latest_new_votes)), + validator_index_(validator_index) {} +} // namespace lean diff --git a/src/blockchain/fork_choice.hpp b/src/blockchain/fork_choice.hpp new file mode 100644 index 0000000..0ee51f5 --- /dev/null +++ b/src/blockchain/fork_choice.hpp @@ -0,0 +1,178 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include + +#include +#include + +#include "blockchain/is_justifiable_slot.hpp" +#include "blockchain/state_transition_function.hpp" +#include "clock/clock.hpp" +#include "types/block.hpp" +#include "types/state.hpp" +#include "types/validator_index.hpp" +#include "utils/ceil_div.hpp" + +namespace lean { + class ForkChoiceStore { + public: + using Blocks = std::unordered_map; + using Votes = std::unordered_map; + + enum class Error { + INVALID_ATTESTATION, + INVALID_PROPOSER, + }; + Q_ENUM_ERROR_CODE_FRIEND(Error) { + using E = decltype(e); + switch (e) { + case E::INVALID_ATTESTATION: + return "Invalid attestation"; + case E::INVALID_PROPOSER: + return "Invalid proposer"; + } + abort(); + } + + ForkChoiceStore(const AnchorState &anchor_state, + const AnchorBlock &anchor_block, + qtils::SharedRef clock, + qtils::SharedRef logging_system); + + BOOST_DI_INJECT_TRAITS(const AnchorState &, + const AnchorBlock &, + qtils::SharedRef, + qtils::SharedRef); + // Test constructor - only for use in tests + ForkChoiceStore(uint64_t now_sec, + qtils::SharedRef logging_system, + Config config = {}, + BlockHash head = {}, + BlockHash safe_target = {}, + Checkpoint latest_justified = {}, + Checkpoint latest_finalized = {}, + Blocks blocks = {}, + std::unordered_map states = {}, + Votes latest_known_votes = {}, + Votes latest_new_votes = {}, + ValidatorIndex validator_index = 0); + + // Compute the latest block that the validator is allowed to choose as the + // target + void updateSafeTarget(); + + std::optional getLatestJustified(); + + // Updates the store's latest justified checkpoint, head, and latest + // finalized state. + void updateHead(); + + // Process new votes that the staker has received. Vote processing is done + // at a particular time, because of safe target and view merge rules. + // Accepts the latest new votes, merges them into the known votes, and then + // updates the fork-choice head. + void acceptNewVotes(); + + Slot getCurrentSlot(); + + BlockHash getHead(); + const State &getState(const BlockHash &block_hash) const; + + bool hasBlock(const BlockHash &hash) const; + std::optional getBlockSlot(const BlockHash &block_hash) const; + Slot getHeadSlot() const; + const Config &getConfig() const; + Checkpoint getLatestFinalized() const; + + // Test helper methods + BlockHash getSafeTarget() const { + return safe_target_; + } + const Blocks &getBlocks() const { + return blocks_; + } + const Votes &getLatestNewVotes() const { + return latest_new_votes_; + } + const Votes &getLatestKnownVotes() const { + return latest_known_votes_; + } + Votes &getLatestNewVotesRef() { + return latest_new_votes_; + } + + /** + * Calculates the target checkpoint for a vote based on the head, safe + * target, and latest finalized state. + */ + Checkpoint getVoteTarget() const; + + /** + * Produce a new block for the given slot and validator. + * + * Algorithm Overview: + * 1. Validate proposer authorization for the target slot + * 2. Get the current chain head as the parent block + * 3. Iteratively build attestation set: + * - Create candidate block with current attestations + * - Apply state transition (slot advancement + block processing) + * - Find new valid attestations matching post-state requirements + * - Continue until no new attestations can be added + * 4. Finalize block with computed state root and store it + * + * Args: + * slot: Target slot number for block production + * validator_index: Index of validator authorized to propose this block + */ + outcome::result produceBlock(Slot slot, + ValidatorIndex validator_index); + + // Validate incoming attestation before processing. + // Performs basic validation checks on attestation structure and timing. + outcome::result validateAttestation(const SignedVote &signed_vote); + + // Validates and processes a new attestation (a signed vote), updating the + // store's latest votes. + outcome::result processAttestation(const SignedVote &signed_vote, + bool is_from_block); + + // Processes a new block, updates the store, and triggers a head update. + outcome::result onBlock(Block block); + + // Advance forkchoice store time to given timestamp. + // Ticks store forward interval by interval, performing appropriate + // actions for each interval type. + // Args: + // time: Target time in seconds since genesis. + // has_proposal: Whether node has proposal for current slot. + std::vector> advanceTime( + uint64_t now_sec); + + private: + STF stf_; + Interval time_; + Config config_; + BlockHash head_; + BlockHash safe_target_; + Checkpoint latest_justified_; + Checkpoint latest_finalized_; + Blocks blocks_; + std::unordered_map states_; + Votes latest_known_votes_; + Votes latest_new_votes_; + const ValidatorIndex validator_index_; + log::Logger logger_; + }; + + BlockHash getForkChoiceHead(const ForkChoiceStore::Blocks &blocks, + const Checkpoint &root, + const ForkChoiceStore::Votes &latest_votes, + uint64_t min_score); +} // namespace lean diff --git a/src/blockchain/impl/fc_block_tree.cpp b/src/blockchain/impl/fc_block_tree.cpp new file mode 100644 index 0000000..7dcdefd --- /dev/null +++ b/src/blockchain/impl/fc_block_tree.cpp @@ -0,0 +1,123 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "blockchain/impl/fc_block_tree.hpp" + +#include "blockchain/block_tree_error.hpp" +#include "types/signed_block.hpp" + +namespace lean::blockchain { + FCBlockTree::FCBlockTree(qtils::SharedRef fork_choice_store) + : fork_choice_store_(std::move(fork_choice_store)) {} + + const BlockHash &FCBlockTree::getGenesisBlockHash() const { + throw std::runtime_error("FCBlockTree::getGenesisBlockHash()"); + } + + bool FCBlockTree::has(const BlockHash &hash) const { + return fork_choice_store_->hasBlock(hash); + } + + outcome::result FCBlockTree::getBlockHeader( + const BlockHash &block_hash) const { + throw std::runtime_error("FCBlockTree::getBlockHeader()"); + } + + outcome::result> FCBlockTree::tryGetBlockHeader( + const BlockHash &block_hash) const { + throw std::runtime_error("FCBlockTree::tryGetBlockHeader()"); + } + + outcome::result FCBlockTree::getBlockBody( + const BlockHash &block_hash) const { + throw std::runtime_error("FCBlockTree::getBlockBody()"); + } + + outcome::result FCBlockTree::addBlockHeader(const BlockHeader &header) { + throw std::runtime_error("FCBlockTree::addBlockHeader()"); + } + + outcome::result FCBlockTree::addBlock(const Block &block) { + return fork_choice_store_->onBlock(block); + } + + outcome::result FCBlockTree::removeLeaf(const BlockHash &block_hash) { + throw std::runtime_error("FCBlockTree::removeLeaf()"); + } + + outcome::result FCBlockTree::addExistingBlock( + const BlockHash &block_hash, const BlockHeader &block_header) { + throw std::runtime_error("FCBlockTree::addExistingBlock()"); + } + + outcome::result FCBlockTree::addBlockBody(const BlockHash &block_hash, + const BlockBody &body) { + throw std::runtime_error("FCBlockTree::addBlockBody()"); + } + + outcome::result FCBlockTree::finalize( + const BlockHash &block_hash, const Justification &justification) { + throw std::runtime_error("FCBlockTree::finalize()"); + } + + outcome::result> FCBlockTree::getBestChainFromBlock( + const BlockHash &block, uint64_t maximum) const { + throw std::runtime_error("FCBlockTree::getBestChainFromBlock()"); + } + + outcome::result> + FCBlockTree::getDescendingChainToBlock(const BlockHash &block, + uint64_t maximum) const { + throw std::runtime_error("FCBlockTree::getDescendingChainToBlock()"); + } + + bool FCBlockTree::isFinalized(const BlockIndex &block) const { + throw std::runtime_error("FCBlockTree::isFinalized()"); + } + + BlockIndex FCBlockTree::bestBlock() const { + return BlockIndex{.slot = fork_choice_store_->getHeadSlot(), + .hash = fork_choice_store_->getHead()}; + } + + outcome::result FCBlockTree::getBestContaining( + const BlockHash &target_hash) const { + throw std::runtime_error("FCBlockTree::getBestContaining()"); + } + + std::vector FCBlockTree::getLeaves() const { + throw std::runtime_error("FCBlockTree::getLeaves()"); + } + // std::vector getLeavesInfo() const override; + + outcome::result> FCBlockTree::getChildren( + const BlockHash &block) const { + throw std::runtime_error("FCBlockTree::getChildren()"); + } + + BlockIndex FCBlockTree::lastFinalized() const { + auto finalized = fork_choice_store_->getLatestFinalized(); + return BlockIndex{.slot = finalized.slot, .hash = finalized.root}; + } + + outcome::result> FCBlockTree::tryGetSignedBlock( + const BlockHash block_hash) const { + throw std::runtime_error("FCBlockTree::tryGetSignedBlock()"); + } + + void FCBlockTree::import(std::vector blocks) {} + + // BlockHeaderRepository methods + + outcome::result FCBlockTree::getNumberByHash( + const BlockHash &block_hash) const { + auto opt = fork_choice_store_->getBlockSlot(block_hash); + if (not opt.has_value()) { + return BlockTreeError::HEADER_NOT_FOUND; + } + return opt.value(); + } +} // namespace lean::blockchain diff --git a/src/blockchain/impl/fc_block_tree.hpp b/src/blockchain/impl/fc_block_tree.hpp new file mode 100644 index 0000000..fdedc28 --- /dev/null +++ b/src/blockchain/impl/fc_block_tree.hpp @@ -0,0 +1,84 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#include "blockchain/block_tree.hpp" +#include "blockchain/fork_choice.hpp" + +namespace lean::blockchain { + + class FCBlockTree final : public BlockTree { + public: + FCBlockTree(qtils::SharedRef fork_choice_store); + + ~FCBlockTree() override = default; + + const BlockHash &getGenesisBlockHash() const override; + + bool has(const BlockHash &hash) const override; + + outcome::result getBlockHeader( + const BlockHash &block_hash) const override; + + outcome::result> tryGetBlockHeader( + const BlockHash &block_hash) const override; + + outcome::result getBlockBody( + const BlockHash &block_hash) const override; + + outcome::result addBlockHeader(const BlockHeader &header) override; + + outcome::result addBlock(const Block &block) override; + + outcome::result removeLeaf(const BlockHash &block_hash) override; + + outcome::result addExistingBlock( + const BlockHash &block_hash, const BlockHeader &block_header) override; + + outcome::result addBlockBody(const BlockHash &block_hash, + const BlockBody &body) override; + + outcome::result finalize(const BlockHash &block_hash, + const Justification &justification) override; + + outcome::result> getBestChainFromBlock( + const BlockHash &block, uint64_t maximum) const override; + + outcome::result> getDescendingChainToBlock( + const BlockHash &block, uint64_t maximum) const override; + + bool isFinalized(const BlockIndex &block) const override; + + BlockIndex bestBlock() const override; + + outcome::result getBestContaining( + const BlockHash &target_hash) const override; + + std::vector getLeaves() const override; + // std::vector getLeavesInfo() const override; + + outcome::result> getChildren( + const BlockHash &block) const override; + + BlockIndex lastFinalized() const override; + + outcome::result> tryGetSignedBlock( + const BlockHash block_hash) const override; + void import(std::vector blocks) override; + + // BlockHeaderRepository methods + + outcome::result getNumberByHash( + const BlockHash &block_hash) const override; + + private: + std::shared_ptr fork_choice_store_; + }; + +} // namespace lean::blockchain diff --git a/src/blockchain/state_transition_function.cpp b/src/blockchain/state_transition_function.cpp index d585601..1571182 100644 --- a/src/blockchain/state_transition_function.cpp +++ b/src/blockchain/state_transition_function.cpp @@ -7,6 +7,7 @@ #include "blockchain/state_transition_function.hpp" #include +#include #include "blockchain/is_justifiable_slot.hpp" #include "types/signed_block.hpp" @@ -69,17 +70,36 @@ namespace lean { } } - State STF::generateGenesisState(const Config &config) const { + AnchorState STF::generateGenesisState(const Config &config) { BlockHeader header; + header.slot = 0; + header.proposer_index = 0; + header.parent_root = kZeroHash; + header.state_root = kZeroHash; header.body_root = sszHash(BlockBody{}); - return State{ - .config = config, - .latest_block_header = header, - }; + + AnchorState result; + result.config = config; + result.slot = 0; + result.latest_block_header = header; + result.latest_justified = Checkpoint{.root = kZeroHash, .slot = 0}; + result.latest_finalized = Checkpoint{.root = kZeroHash, .slot = 0}; + // result.historical_block_hashes; + // result.justified_slots; + // result.justifications_roots; + // result.justifications_validators; + + return result; } - Block STF::genesisBlock(const State &state) const { - return Block{.state_root = sszHash(state)}; + AnchorBlock STF::genesisBlock(const State &state) { + AnchorBlock result; + result.slot = state.slot; + result.proposer_index = 0; + result.parent_root = kZeroHash; + result.state_root = sszHash(state); + result.body = BlockBody{}; + return result; } outcome::result STF::stateTransition(const SignedBlock &signed_block, diff --git a/src/blockchain/state_transition_function.hpp b/src/blockchain/state_transition_function.hpp index cf04bb5..64b9597 100644 --- a/src/blockchain/state_transition_function.hpp +++ b/src/blockchain/state_transition_function.hpp @@ -9,7 +9,10 @@ #include #include +#include "app/impl/chain_spec_impl.hpp" +#include "types/block.hpp" #include "types/slot.hpp" +#include "types/state.hpp" namespace lean { struct Block; @@ -51,8 +54,8 @@ namespace lean { abort(); } - State generateGenesisState(const Config &config) const; - Block genesisBlock(const State &state) const; + static AnchorState generateGenesisState(const Config &config); + static AnchorBlock genesisBlock(const State &state); /** * Apply block to parent state. @@ -62,10 +65,11 @@ namespace lean { const State &parent_state, bool check_state_root) const; - private: outcome::result processSlots(State &state, Slot slot) const; - void processSlot(State &state) const; outcome::result processBlock(State &state, const Block &block) const; + + private: + void processSlot(State &state) const; outcome::result processBlockHeader(State &state, const Block &block) const; outcome::result processOperations(State &state, diff --git a/src/executable/lean_node.cpp b/src/executable/lean_node.cpp index 5a8371a..03c34e8 100644 --- a/src/executable/lean_node.cpp +++ b/src/executable/lean_node.cpp @@ -21,6 +21,7 @@ #include "log/logger.hpp" #include "modules/module_loader.hpp" #include "se/subscription.hpp" +#include "types/config.hpp" using std::string_view_literals::operator""sv; @@ -38,8 +39,9 @@ namespace { using lean::log::LoggingSystem; int run_node(std::shared_ptr logsys, - std::shared_ptr appcfg) { - auto injector = std::make_unique(logsys, appcfg); + std::shared_ptr appcfg, + std::shared_ptr genesis_cfg) { + auto injector = std::make_unique(logsys, appcfg, genesis_cfg); // Load modules std::deque> loaders; @@ -182,7 +184,7 @@ int main(int argc, const char **argv, const char **env) { } // Setup config - auto configuration = ({ + auto app_configuration = ({ auto logger = logging_system->getLogger("Configurator", "lean"); auto config_res = app_configurator->calculateConfig(logger); @@ -197,6 +199,18 @@ int main(int argc, const char **argv, const char **env) { config_res.value(); }); + // set genesis config. Genesis time should be next multiple of 12 seconds + // since epoch (in ms) + constexpr uint64_t GENESIS_INTERVAL_MS = 12'000; // 12 seconds in milliseconds + uint64_t genesis_time = + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + genesis_time += GENESIS_INTERVAL_MS - (genesis_time % GENESIS_INTERVAL_MS); + + lean::Config genesis_config{.num_validators = 4, + .genesis_time = genesis_time}; + int exit_code; { @@ -204,7 +218,9 @@ int main(int argc, const char **argv, const char **env) { if (name.substr(0, 1) == "-") { // The first argument isn't subcommand, run as node - exit_code = run_node(logging_system, configuration); + exit_code = run_node(logging_system, + app_configuration, + std::make_shared(genesis_config)); } // else if (false and name == "subcommand-1"s) { diff --git a/src/injector/node_injector.cpp b/src/injector/node_injector.cpp index 50b43fd..0aa2888 100644 --- a/src/injector/node_injector.cpp +++ b/src/injector/node_injector.cpp @@ -29,6 +29,7 @@ #include "app/impl/watchdog.hpp" #include "blockchain/impl/block_storage_impl.hpp" #include "blockchain/impl/block_tree_impl.hpp" +#include "blockchain/impl/fc_block_tree.hpp" #include "blockchain/impl/genesis_block_header_impl.hpp" #include "clock/impl/clock_impl.hpp" #include "crypto/hasher/hasher_impl.hpp" @@ -56,15 +57,22 @@ namespace { std::move(c))[boost::di::override]; } + // Overload for shared_ptr to bind the underlying type + template + auto useConfig(std::shared_ptr c) { + return boost::di::bind().to(std::move(c))[boost::di::override]; + } + using injector::bind_by_lambda; template auto makeApplicationInjector(std::shared_ptr logsys, - std::shared_ptr config, + std::shared_ptr app_config, + std::shared_ptr genesis_config, Ts &&...args) { // clang-format off return di::make_injector( - di::bind.to(config), + di::bind.to(app_config), di::bind.to(logsys), di::bind.to(), di::bind.to(), @@ -81,6 +89,7 @@ namespace { .openmetricsHttpEndpoint() }; }), + di::bind.to(genesis_config), di::bind.to(), //di::bind.to(), di::bind.to(), @@ -89,7 +98,7 @@ namespace { di::bind.to(), di::bind.to(), di::bind.to(), - di::bind.to(), + di::bind.to(), // user-defined overrides... std::forward(args)...); @@ -98,10 +107,18 @@ namespace { template auto makeNodeInjector(std::shared_ptr logsys, - std::shared_ptr config, + std::shared_ptr app_config, + std::shared_ptr genesis_config, Ts &&...args) { + AnchorState genesis_state = STF::generateGenesisState(*genesis_config); + AnchorBlock genesis_block = STF::genesisBlock(genesis_state); + return di::make_injector( - makeApplicationInjector(std::move(logsys), std::move(config)), + makeApplicationInjector(std::move(logsys), + std::move(app_config), + std::move(genesis_config), + useConfig(genesis_state), + useConfig(genesis_block)), // user-defined overrides... std::forward(args)...); @@ -113,7 +130,8 @@ namespace lean::injector { public: using Injector = decltype(makeNodeInjector(std::shared_ptr(), - std::shared_ptr())); + std::shared_ptr(), + std::shared_ptr())); explicit NodeInjectorImpl(Injector injector) : injector_{std::move(injector)} {} @@ -122,9 +140,12 @@ namespace lean::injector { }; NodeInjector::NodeInjector(std::shared_ptr logsys, - std::shared_ptr config) + std::shared_ptr app_config, + std::shared_ptr genesis_config) : pimpl_{std::make_unique( - makeNodeInjector(std::move(logsys), std::move(config)))} {} + makeNodeInjector(std::move(logsys), + std::move(app_config), + std::move(genesis_config)))} {} std::shared_ptr NodeInjector::injectApplication() { return pimpl_->injector_ diff --git a/src/injector/node_injector.hpp b/src/injector/node_injector.hpp index e9dbb32..015c920 100644 --- a/src/injector/node_injector.hpp +++ b/src/injector/node_injector.hpp @@ -10,6 +10,12 @@ #include "se/subscription.hpp" +namespace lean { + class ForkChoiceStore; +} +namespace lean { + struct Config; +} namespace lean::log { class LoggingSystem; } // namespace lean::log @@ -36,7 +42,8 @@ namespace lean::injector { class NodeInjector final { public: explicit NodeInjector(std::shared_ptr logging_system, - std::shared_ptr configuration); + std::shared_ptr app_config, + std::shared_ptr genesis_config); std::shared_ptr injectApplication(); std::unique_ptr register_loader( diff --git a/src/loaders/impl/networking_loader.hpp b/src/loaders/impl/networking_loader.hpp index 8f70501..5586a55 100644 --- a/src/loaders/impl/networking_loader.hpp +++ b/src/loaders/impl/networking_loader.hpp @@ -29,6 +29,7 @@ namespace lean::loaders { public modules::NetworkingLoader { log::Logger logger_; qtils::SharedRef block_tree_; + qtils::SharedRef fork_choice_store_; std::shared_ptr> on_init_complete_; @@ -46,10 +47,12 @@ namespace lean::loaders { public: NetworkingLoader(std::shared_ptr logsys, std::shared_ptr se_manager, - qtils::SharedRef block_tree) + qtils::SharedRef block_tree, + qtils::SharedRef fork_choice_store) : Loader(std::move(logsys), std::move(se_manager)), logger_(logsys_->getLogger("Networking", "networking_module")), - block_tree_{std::move(block_tree)} {} + block_tree_{std::move(block_tree)}, + fork_choice_store_{std::move(fork_choice_store)} {} NetworkingLoader(const NetworkingLoader &) = delete; NetworkingLoader &operator=(const NetworkingLoader &) = delete; @@ -63,14 +66,16 @@ namespace lean::loaders { ->getFunctionFromLibrary, modules::NetworkingLoader &, std::shared_ptr, - qtils::SharedRef>( + qtils::SharedRef, + qtils::SharedRef>( "query_module_instance"); if (not module_accessor) { return; } - auto module_internal = (*module_accessor)(*this, logsys_, block_tree_); + auto module_internal = + (*module_accessor)(*this, logsys_, block_tree_, fork_choice_store_); on_init_complete_ = se::SubscriberCreator::template create< EventTypes::NetworkingIsLoaded>( diff --git a/src/loaders/impl/production_loader.hpp b/src/loaders/impl/production_loader.hpp index 5880381..6389c30 100644 --- a/src/loaders/impl/production_loader.hpp +++ b/src/loaders/impl/production_loader.hpp @@ -13,6 +13,9 @@ #include "modules/production/production.hpp" #include "se/subscription.hpp" +namespace lean::messages { + struct SlotIntervalStarted; +} // namespace lean::messages namespace lean::loaders { class ProductionLoader final @@ -21,6 +24,8 @@ namespace lean::loaders { public modules::ProductionLoader { qtils::SharedRef block_tree_; qtils::SharedRef hasher_; + qtils::SharedRef fork_choice_store_; + qtils::SharedRef clock_; std::shared_ptr> on_init_complete_; @@ -30,6 +35,10 @@ namespace lean::loaders { BaseSubscriber>> on_slot_started_; + std::shared_ptr< + BaseSubscriber>> + on_slot_interval_started_; std::shared_ptr< BaseSubscriber>> on_leave_update_; @@ -41,10 +50,14 @@ namespace lean::loaders { ProductionLoader(qtils::SharedRef logsys, qtils::SharedRef se_manager, qtils::SharedRef block_tree, - qtils::SharedRef hasher) + qtils::SharedRef hasher, + qtils::SharedRef fork_choice_store, + qtils::SharedRef clock) : Loader(std::move(logsys), std::move(se_manager)), block_tree_(std::move(block_tree)), - hasher_(std::move(hasher)) {} + hasher_(std::move(hasher)), + fork_choice_store_(std::move(fork_choice_store)), + clock_(clock) {} ProductionLoader(const ProductionLoader &) = delete; ProductionLoader &operator=(const ProductionLoader &) = delete; @@ -59,15 +72,17 @@ namespace lean::loaders { modules::ProductionLoader &, std::shared_ptr, std::shared_ptr, - std::shared_ptr>( + qtils::SharedRef, + std::shared_ptr, + std::shared_ptr>( "query_module_instance"); if (not module_accessor) { return; } - auto module_internal = - (*module_accessor)(*this, logsys_, block_tree_, hasher_); + auto module_internal = (*module_accessor)( + *this, logsys_, block_tree_, fork_choice_store_, hasher_, clock_); on_init_complete_ = se::SubscriberCreator::create< EventTypes::ProductionIsLoaded>( @@ -89,17 +104,17 @@ namespace lean::loaders { } }); - on_slot_started_ = - se::SubscriberCreator>:: - create( - *se_manager_, - SubscriptionEngineHandlers::kTest, - [module_internal](auto &, auto msg) { - if (auto m = module_internal.lock()) { - m->on_slot_started(std::move(msg)); - } - }); + on_slot_interval_started_ = se::SubscriberCreator< + qtils::Empty, + std::shared_ptr>:: + create( + *se_manager_, + SubscriptionEngineHandlers::kTest, + [module_internal](auto &, auto msg) { + if (auto m = module_internal.lock()) { + m->on_slot_interval_started(std::move(msg)); + } + }); on_leave_update_ = se::SubscriberCreator message) override { dispatchDerive(*se_manager_, message); } + + void dispatchSendSignedVote( + std::shared_ptr message) override { + dispatchDerive(*se_manager_, message); + } }; } // namespace lean::loaders diff --git a/src/modules/networking/CMakeLists.txt b/src/modules/networking/CMakeLists.txt index eac0ea5..4f2bc9c 100644 --- a/src/modules/networking/CMakeLists.txt +++ b/src/modules/networking/CMakeLists.txt @@ -20,5 +20,6 @@ add_lean_module(networking qtils::qtils Snappy::snappy soralog::soralog + blockchain sszpp ) diff --git a/src/modules/networking/module.cpp b/src/modules/networking/module.cpp index c1052b5..6381900 100644 --- a/src/modules/networking/module.cpp +++ b/src/modules/networking/module.cpp @@ -29,10 +29,14 @@ static std::shared_ptr module_instance; MODULE_C_API std::weak_ptr query_module_instance( lean::modules::NetworkingLoader &loader, std::shared_ptr logsys, - qtils::SharedRef block_tree) { + qtils::SharedRef block_tree, + qtils::SharedRef fork_choice_store) { if (!module_instance) { + BOOST_ASSERT(logsys); + BOOST_ASSERT(block_tree); + BOOST_ASSERT(fork_choice_store); module_instance = lean::modules::NetworkingImpl::create_shared( - loader, std::move(logsys), block_tree); + loader, std::move(logsys), block_tree, fork_choice_store); } return module_instance; } diff --git a/src/modules/networking/networking.cpp b/src/modules/networking/networking.cpp index b3bb831..19bf112 100644 --- a/src/modules/networking/networking.cpp +++ b/src/modules/networking/networking.cpp @@ -7,6 +7,7 @@ #include "modules/networking/networking.hpp" +#include #include #include #include @@ -19,6 +20,7 @@ #include #include "blockchain/block_tree.hpp" +#include "blockchain/impl/fc_block_tree.hpp" #include "modules/networking/block_request_protocol.hpp" #include "modules/networking/ssz_snappy.hpp" #include "modules/networking/status_protocol.hpp" @@ -62,11 +64,14 @@ namespace lean::modules { NetworkingImpl::NetworkingImpl( NetworkingLoader &loader, qtils::SharedRef logging_system, - qtils::SharedRef block_tree) + qtils::SharedRef block_tree, + qtils::SharedRef fork_choice_store) : loader_(loader), logger_(logging_system->getLogger("Networking", "networking_module")), - block_tree_{std::move(block_tree)} { + block_tree_{std::move(block_tree)}, + fork_choice_store_{std::move(fork_choice_store)} { libp2p::log::setLoggingSystem(logging_system->getSoralog()); + block_tree_ = std::make_shared(fork_choice_store_); } NetworkingImpl::~NetworkingImpl() { @@ -175,13 +180,18 @@ namespace lean::modules { self->receiveBlock(std::nullopt, std::move(block)); }); gossip_votes_topic_ = gossipSubscribe( - "vote", [weak_self{weak_from_this()}](SignedVote &&vote) { + "vote", [weak_self{weak_from_this()}](SignedVote &&signed_vote) { auto self = weak_self.lock(); if (not self) { return; } - self->loader_.dispatchSignedVoteReceived( - std::make_shared(std::move(vote))); + auto res = + self->fork_choice_store_->processAttestation(signed_vote, false); + BOOST_ASSERT_MSG(res.has_value(), "Gossiped vote should be valid"); + SL_INFO(self->logger_, + "Received vote for target {}@{}", + signed_vote.data.target.slot, + signed_vote.data.target.root); }); if (sample_peer.index != 0) { @@ -234,18 +244,19 @@ namespace lean::modules { std::shared_ptr NetworkingImpl::gossipSubscribe(std::string_view type, auto f) { auto topic = gossip_->subscribe(gossipTopic(type)); - libp2p::coroSpawn(*io_context_, - [topic, f{std::move(f)}]() -> libp2p::Coro { - while (auto raw_result = co_await topic->receive()) { - auto &raw = raw_result.value(); - if (auto uncompressed_res = snappyUncompress(raw)) { - auto &uncompressed = uncompressed_res.value(); - if (auto r = decode(uncompressed)) { - f(std::move(r.value())); - } - } - } - }); + libp2p::coroSpawn( + *io_context_, + [this, type, topic, f{std::move(f)}]() -> libp2p::Coro { + while (auto raw_result = co_await topic->receive()) { + auto &raw = raw_result.value(); + if (auto uncompressed_res = snappyUncompress(raw)) { + auto &uncompressed = uncompressed_res.value(); + if (auto r = decode(uncompressed)) { + f(std::move(r.value())); + } + } + } + }); return topic; } @@ -294,9 +305,12 @@ namespace lean::modules { // TODO(turuslan): detect finalized change void NetworkingImpl::receiveBlock(std::optional from_peer, - SignedBlock &&block) { - auto slot_hash = block.message.slotHash(); - SL_TRACE(logger_, "receiveBlock {}", slot_hash.slot); + SignedBlock &&signed_block) { + auto slot_hash = signed_block.message.slotHash(); + SL_DEBUG(logger_, + "receiveBlock slot {} hash {}", + slot_hash.slot, + slot_hash.hash); auto remove = [&](auto f) { std::vector queue{slot_hash.hash}; while (not queue.empty()) { @@ -309,7 +323,7 @@ namespace lean::modules { } } }; - auto parent_hash = block.message.parent_root; + auto parent_hash = signed_block.message.parent_root; if (block_cache_.contains(slot_hash.hash)) { SL_TRACE(logger_, "receiveBlock {} => ignore cached", slot_hash.slot); return; @@ -326,19 +340,22 @@ namespace lean::modules { return; } if (block_tree_->has(parent_hash)) { - std::vector blocks{std::move(block)}; + std::vector blocks{std::move(signed_block)}; remove([&](const BlockHash &block_hash) { blocks.emplace_back(block_cache_.extract(block_hash).mapped()); }); std::string __s; for (auto &block : blocks) { __s += std::format(" {}", block.message.slot); + auto res = fork_choice_store_->onBlock(block.message); + BOOST_ASSERT_MSG(res.has_value(), + "Fork choice store should accept imported block"); } - SL_TRACE(logger_, "receiveBlock {} => import{}", slot_hash.slot, __s); + SL_INFO(logger_, "receiveBlock {} => import{}", slot_hash.slot, __s); block_tree_->import(std::move(blocks)); return; } - block_cache_.emplace(slot_hash.hash, std::move(block)); + block_cache_.emplace(slot_hash.hash, std::move(signed_block)); block_children_.emplace(parent_hash, slot_hash.hash); if (block_cache_.contains(parent_hash)) { SL_TRACE(logger_, "receiveBlock {} => has parent", slot_hash.slot); diff --git a/src/modules/networking/networking.hpp b/src/modules/networking/networking.hpp index 45ea7bd..7d11425 100644 --- a/src/modules/networking/networking.hpp +++ b/src/modules/networking/networking.hpp @@ -8,6 +8,7 @@ #include +#include #include #include #include @@ -47,7 +48,8 @@ namespace lean::modules { class NetworkingImpl final : public Singleton, public Networking { NetworkingImpl(NetworkingLoader &loader, qtils::SharedRef logging_system, - qtils::SharedRef block_tree); + qtils::SharedRef block_tree, + qtils::SharedRef fork_choice_store); public: CREATE_SHARED_METHOD(NetworkingImpl); @@ -77,6 +79,7 @@ namespace lean::modules { NetworkingLoader &loader_; log::Logger logger_; qtils::SharedRef block_tree_; + qtils::SharedRef fork_choice_store_; std::shared_ptr injector_; std::shared_ptr io_context_; std::optional io_thread_; diff --git a/src/modules/networking/types.hpp b/src/modules/networking/types.hpp index 5e9b49a..fab2f8b 100644 --- a/src/modules/networking/types.hpp +++ b/src/modules/networking/types.hpp @@ -16,13 +16,13 @@ namespace lean { SSZ_CONT(finalized, head); }; - struct BlockRequest : ssz::ssz_container { + struct BlockRequest : ssz::ssz_variable_size_container { ssz::list blocks; SSZ_CONT(blocks); }; - struct BlockResponse : ssz::ssz_container { + struct BlockResponse : ssz::ssz_variable_size_container { ssz::list blocks; SSZ_CONT(blocks); diff --git a/src/modules/production/CMakeLists.txt b/src/modules/production/CMakeLists.txt index c92cfbb..3ac983c 100644 --- a/src/modules/production/CMakeLists.txt +++ b/src/modules/production/CMakeLists.txt @@ -14,5 +14,6 @@ add_lean_module(production LIBRARIES qtils::qtils soralog::soralog + blockchain sszpp ) \ No newline at end of file diff --git a/src/modules/production/interfaces.hpp b/src/modules/production/interfaces.hpp index 87b2a71..b37127c 100644 --- a/src/modules/production/interfaces.hpp +++ b/src/modules/production/interfaces.hpp @@ -12,6 +12,7 @@ namespace lean::messages { struct SlotStarted; + struct SlotIntervalStarted; struct Finalized; struct NewLeaf; } // namespace lean::messages @@ -28,6 +29,9 @@ namespace lean::modules { virtual void dispatchSendSignedBlock( std::shared_ptr message) = 0; + + virtual void dispatchSendSignedVote( + std::shared_ptr message) = 0; }; struct ProductionModule { @@ -35,8 +39,8 @@ namespace lean::modules { virtual void on_loaded_success() = 0; virtual void on_loading_is_finished() = 0; - virtual void on_slot_started( - std::shared_ptr) = 0; + virtual void on_slot_interval_started( + std::shared_ptr) = 0; virtual void on_leave_update(std::shared_ptr) = 0; diff --git a/src/modules/production/module.cpp b/src/modules/production/module.cpp index 1f8ad49..2223c56 100644 --- a/src/modules/production/module.cpp +++ b/src/modules/production/module.cpp @@ -26,10 +26,17 @@ MODULE_C_API std::weak_ptr query_module_instance(lean::modules::ProductionLoader &loader, std::shared_ptr logger, std::shared_ptr block_tree, - qtils::SharedRef hasher) { + std::shared_ptr fork_choice_store, + qtils::SharedRef hasher, + qtils::SharedRef clock) { if (!module_instance) { module_instance = lean::modules::ProductionModuleImpl::create_shared( - loader, std::move(logger), std::move(block_tree), std::move(hasher)); + loader, + std::move(logger), + std::move(block_tree), + std::move(fork_choice_store), + std::move(hasher), + std::move(clock)); } return module_instance; } diff --git a/src/modules/production/production.cpp b/src/modules/production/production.cpp index 732f42d..eb546d8 100644 --- a/src/modules/production/production.cpp +++ b/src/modules/production/production.cpp @@ -6,6 +6,8 @@ #include "modules/production/production.hpp" +#include + #include "blockchain/block_tree.hpp" #include "crypto/hasher.hpp" #include "modules/shared/networking_types.tmp.hpp" @@ -19,12 +21,16 @@ namespace lean::modules { ProductionLoader &loader, qtils::SharedRef logging_system, qtils::SharedRef block_tree, - qtils::SharedRef hasher) + std::shared_ptr fork_choice_store, + qtils::SharedRef hasher, + qtils::SharedRef clock) : loader_(loader), logsys_(std::move(logging_system)), logger_(logsys_->getLogger("ProductionModule", "production_module")), block_tree_(std::move(block_tree)), - hasher_(std::move(hasher)) {} + fork_choice_store_(std::move(fork_choice_store)), + hasher_(std::move(hasher)), + clock_(std::move(clock)) {} void ProductionModuleImpl::on_loaded_success() { SL_INFO(logger_, "Loaded success"); @@ -34,44 +40,28 @@ namespace lean::modules { SL_INFO(logger_, "Loading is finished"); } - void ProductionModuleImpl::on_slot_started( - std::shared_ptr msg) { - if (msg->epoch_change) { - SL_INFO(logger_, "Epoch changed to {}", msg->epoch); - } - - auto producer_index = msg->slot % getValidatorCount(); - auto is_producer = getPeerIndex() == producer_index; - - SL_INFO(logger_, - "Slot {} is started{}", - msg->slot, - is_producer ? " - I'm a producer" : ""); - - if (is_producer) { - auto parent_hash = block_tree_->bestBlock().hash; - // Produce block - Block block; - block.slot = msg->slot; - block.proposer_index = producer_index; - block.parent_root = parent_hash; - // block.state_root = ; - - // Add a block into the block tree - auto res = block_tree_->addBlock(block); - if (res.has_error()) { - SL_ERROR( - logger_, "Could not add block to the block tree: {}", res.error()); - return; - } - - // Notify subscribers - loader_.dispatch_block_produced(std::make_shared(block)); + void ProductionModuleImpl::on_slot_interval_started( + std::shared_ptr msg) { + // advance fork choice store to current time + auto res = fork_choice_store_->advanceTime(clock_->nowSec()); - // TODO(turuslan): signature - loader_.dispatchSendSignedBlock( - std::make_shared( - SignedBlock{.message = block})); + // dispatch all votes and blocks produced during advance time + for (auto &vote_or_block : res) { + qtils::visit_in_place( + vote_or_block, + [&](const SignedVote &v) { + loader_.dispatchSendSignedVote( + std::make_shared(v)); + }, + [&](const SignedBlock &v) { + loader_.dispatchSendSignedBlock( + std::make_shared(v)); + auto res = block_tree_->addBlock(v.message); + if (!res.has_value()) { + SL_ERROR( + logger_, "Failed to add produced block: {}", res.error()); + } + }); } } diff --git a/src/modules/production/production.hpp b/src/modules/production/production.hpp index 61d583c..a68a4eb 100644 --- a/src/modules/production/production.hpp +++ b/src/modules/production/production.hpp @@ -10,6 +10,8 @@ #include #include +#include "blockchain/fork_choice.hpp" + namespace lean::crypto { class Hasher; } @@ -23,7 +25,9 @@ namespace lean::modules { ProductionModuleImpl(lean::modules::ProductionLoader &loader, qtils::SharedRef logsys, qtils::SharedRef block_tree, - qtils::SharedRef hasher); + std::shared_ptr fork_choice_store, + qtils::SharedRef hasher, + qtils::SharedRef clock); public: CREATE_SHARED_METHOD(ProductionModuleImpl); @@ -31,7 +35,8 @@ namespace lean::modules { void on_loaded_success() override; void on_loading_is_finished() override; - void on_slot_started(std::shared_ptr) override; + void on_slot_interval_started( + std::shared_ptr) override; void on_leave_update(std::shared_ptr) override; void on_block_finalized( @@ -42,7 +47,9 @@ namespace lean::modules { qtils::SharedRef logsys_; lean::log::Logger logger_; qtils::SharedRef block_tree_; + qtils::SharedRef fork_choice_store_; qtils::SharedRef hasher_; + qtils::SharedRef clock_; }; diff --git a/src/modules/shared/prodution_types.tmp.hpp b/src/modules/shared/prodution_types.tmp.hpp index cee1800..a513646 100644 --- a/src/modules/shared/prodution_types.tmp.hpp +++ b/src/modules/shared/prodution_types.tmp.hpp @@ -17,6 +17,12 @@ namespace lean::messages { bool epoch_change; }; + struct SlotIntervalStarted { + Interval interval; + Slot slot; + Epoch epoch; + }; + struct NewLeaf { BlockHeader header; bool best = false; diff --git a/src/se/subscription_fwd.hpp b/src/se/subscription_fwd.hpp index 5b1ba46..71cb79b 100644 --- a/src/se/subscription_fwd.hpp +++ b/src/se/subscription_fwd.hpp @@ -84,6 +84,7 @@ namespace lean { /// New slot started SlotStarted, + SlotIntervalStarted, /// Used by `DeriveEventType::get` Derive, diff --git a/src/types/block.hpp b/src/types/block.hpp index b99deb2..e643cd1 100644 --- a/src/types/block.hpp +++ b/src/types/block.hpp @@ -10,7 +10,7 @@ #include "types/block_header.hpp" namespace lean { - struct Block : ssz::ssz_container { + struct Block : ssz::ssz_variable_size_container { uint64_t slot; uint64_t proposer_index; qtils::ByteArr<32> parent_root; @@ -29,11 +29,12 @@ namespace lean { return header; } - std::optional hash_cached; + mutable std::optional hash_cached; const BlockHash &hash() const { - return hash_cached.value(); + return hash_cached.has_value() ? hash_cached.value() + : (setHash(), hash_cached.value()); } - void setHash() { + void setHash() const { auto header = getHeader(); header.updateHash(); auto hash = header.hash(); @@ -45,4 +46,6 @@ namespace lean { return {slot, hash()}; } }; + + using AnchorBlock = qtils::Tagged; } // namespace lean diff --git a/src/types/block_body.hpp b/src/types/block_body.hpp index 5afb58f..e259f92 100644 --- a/src/types/block_body.hpp +++ b/src/types/block_body.hpp @@ -13,9 +13,10 @@ namespace lean { - struct BlockBody : ssz::ssz_container { + using Attestations = ssz::list; + struct BlockBody : ssz::ssz_variable_size_container { /// @note votes will be replaced by aggregated attestations. - ssz::list attestations; + Attestations attestations; SSZ_CONT(attestations); }; diff --git a/src/types/checkpoint.hpp b/src/types/checkpoint.hpp index f966074..90ffaf2 100644 --- a/src/types/checkpoint.hpp +++ b/src/types/checkpoint.hpp @@ -15,7 +15,11 @@ namespace lean { struct Checkpoint : ssz::ssz_container { qtils::ByteArr<32> root; - Slot slot; + Slot slot = 0; + + static Checkpoint from(const auto &v) { + return Checkpoint{.root = v.hash(), .slot = v.slot}; + } SSZ_CONT(root, slot); }; diff --git a/src/types/constants.hpp b/src/types/constants.hpp index 4a2f7bc..1808d5b 100644 --- a/src/types/constants.hpp +++ b/src/types/constants.hpp @@ -15,8 +15,11 @@ using qtils::literals::operator""_bytes; namespace lean { - static constexpr uint64_t SLOT_DURATION_MS = 4000; // 4 seconds - static constexpr uint64_t INTERVALS_PER_SLOT = 4; // 4 intervals by 1 second + static constexpr uint64_t INTERVALS_PER_SLOT = 4; // 4 intervals by 1 second + static constexpr uint64_t SECONDS_PER_INTERVAL = 1; + static constexpr uint64_t SECONDS_PER_SLOT = + SECONDS_PER_INTERVAL * INTERVALS_PER_SLOT; + static constexpr uint64_t SLOT_DURATION_MS = SECONDS_PER_SLOT * 1000; // State list lengths diff --git a/src/types/slot.hpp b/src/types/slot.hpp index c511811..50cf537 100644 --- a/src/types/slot.hpp +++ b/src/types/slot.hpp @@ -10,4 +10,5 @@ namespace lean { using Slot = uint64_t; + using Interval = uint64_t; } // namespace lean diff --git a/src/types/state.hpp b/src/types/state.hpp index c48ce72..1db37ec 100644 --- a/src/types/state.hpp +++ b/src/types/state.hpp @@ -12,7 +12,7 @@ #include "types/constants.hpp" namespace lean { - struct State : ssz::ssz_container { + struct State : ssz::ssz_variable_size_container { Config config; Slot slot; BlockHeader latest_block_header; @@ -39,4 +39,6 @@ namespace lean { justifications_roots, justifications_validators); }; + + using AnchorState = qtils::Tagged; } // namespace lean diff --git a/src/types/validator_index.hpp b/src/types/validator_index.hpp new file mode 100644 index 0000000..e1ae178 --- /dev/null +++ b/src/types/validator_index.hpp @@ -0,0 +1,13 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace lean { + using ValidatorIndex = uint64_t; +} // namespace lean diff --git a/src/types/vote.hpp b/src/types/vote.hpp index e18dd48..e70f4ba 100644 --- a/src/types/vote.hpp +++ b/src/types/vote.hpp @@ -11,8 +11,8 @@ namespace lean { struct Vote : public ssz::ssz_container { - uint64_t validator_id; - uint64_t slot; + uint64_t validator_id = 0; + uint64_t slot = 0; Checkpoint head; Checkpoint target; Checkpoint source; diff --git a/src/utils/ceil_div.hpp b/src/utils/ceil_div.hpp new file mode 100644 index 0000000..7f5e70d --- /dev/null +++ b/src/utils/ceil_div.hpp @@ -0,0 +1,15 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +namespace lean { + auto ceilDiv(const std::integral auto &l, const std::integral auto &r) { + return (l + r - 1) / r; + } +} // namespace lean diff --git a/tests/mock/clock/manual_clock.hpp b/tests/mock/clock/manual_clock.hpp new file mode 100644 index 0000000..29a2903 --- /dev/null +++ b/tests/mock/clock/manual_clock.hpp @@ -0,0 +1,56 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "clock/clock.hpp" + +#include + +namespace lean::clock { + + /** + * Mock implementation of SystemClock for testing purposes. + * Allows manual control of time progression. + */ + class ManualClock : public SystemClock { + public: + ManualClock() : current_time_msec_(0) {} + + explicit ManualClock(uint64_t initial_time_msec) + : current_time_msec_(initial_time_msec) {} + + TimePoint now() const override { + return TimePoint(std::chrono::milliseconds(current_time_msec_)); + } + + uint64_t nowSec() const override { + return current_time_msec_ / 1000; + } + + uint64_t nowMsec() const override { + return current_time_msec_; + } + + /** + * Advance the mock time by the specified number of milliseconds + */ + void advance(uint64_t milliseconds) { + current_time_msec_ += milliseconds; + } + + /** + * Set the mock time to a specific value in milliseconds + */ + void setTime(uint64_t milliseconds) { + current_time_msec_ = milliseconds; + } + + private: + uint64_t current_time_msec_; + }; + +} // namespace lean::clock \ No newline at end of file diff --git a/tests/unit/blockchain/CMakeLists.txt b/tests/unit/blockchain/CMakeLists.txt index 9d5bde8..4d04b31 100644 --- a/tests/unit/blockchain/CMakeLists.txt +++ b/tests/unit/blockchain/CMakeLists.txt @@ -13,6 +13,14 @@ target_link_libraries(block_storage_test storage ) +addtest(fork_choice_test + fork_choice_test.cpp + ) +target_link_libraries(fork_choice_test + blockchain + logger_for_tests + ) + addtest(state_transition_function_test state_transition_function_test.cpp ) diff --git a/tests/unit/blockchain/fork_choice_test.cpp b/tests/unit/blockchain/fork_choice_test.cpp new file mode 100644 index 0000000..e7f269e --- /dev/null +++ b/tests/unit/blockchain/fork_choice_test.cpp @@ -0,0 +1,645 @@ +/** + * Copyright Quadrivium LLC + * All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "blockchain/fork_choice.hpp" + +#include + +#include +#include +#include + +#include "qtils/test/outcome.hpp" +#include "tests/testutil/prepare_loggers.hpp" +#include "blockchain/is_justifiable_slot.hpp" +#include "types/signed_block.hpp" + +using lean::Block; +using lean::Checkpoint; +using lean::ForkChoiceStore; +using lean::getForkChoiceHead; +using lean::INTERVALS_PER_SLOT; +using lean::SignedVote; + +lean::BlockHash testHash(std::string_view s) { + lean::BlockHash hash; + EXPECT_LE(s.size(), hash.size()); + memcpy(hash.data(), s.data(), s.size()); + return hash; +} + +SignedVote makeVote(const Block &source, const Block &target) { + return SignedVote{ + .data = + { + .validator_id = 0, + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(source), + }, + .signature = {}, + }; +} + +std::optional getVote(const ForkChoiceStore::Votes &votes) { + auto it = votes.find(0); + if (it == votes.end()) { + return std::nullopt; + } + return it->second.data.target; +} + +lean::Config config{ + .num_validators = 100, + .genesis_time = 1000, +}; + +auto createTestStore(uint64_t time = 100, + lean::Config config_param = config, + lean::BlockHash head = {}, + lean::BlockHash safe_target = {}, + lean::Checkpoint latest_justified = {}, + lean::Checkpoint latest_finalized = {}, + ForkChoiceStore::Blocks blocks = {}, + std::unordered_map states = {}, + ForkChoiceStore::Votes latest_known_votes = {}, + ForkChoiceStore::Votes latest_new_votes = {}, + lean::ValidatorIndex validator_index = 0) { + return ForkChoiceStore( + time, + testutil::prepareLoggers(), + config_param, + head, + safe_target, + latest_justified, + latest_finalized, + blocks, + states, + latest_known_votes, + latest_new_votes, + validator_index); +} + +auto makeBlockMap(std::vector blocks) { + ForkChoiceStore::Blocks map; + for (auto block : blocks) { + block.setHash(); + map.emplace(block.hash(), block); + } + return map; +} + +std::vector makeBlocks(lean::Slot count) { + std::vector blocks; + auto parent_root = testHash("genesis-parent"); + for (lean::Slot slot = 0; slot < count; ++slot) { + lean::Block block{ + .slot = slot, + .parent_root = parent_root, + .state_root = testHash(std::format("state-{}", slot)), + }; + block.setHash(); + blocks.emplace_back(block); + parent_root = block.hash(); + } + return blocks; +} + +// Test basic vote target selection. +TEST(TestVoteTargetCalculation, test_get_vote_target_basic) { + auto blocks = makeBlocks(2); + auto &genesis = blocks.at(0); + auto &block_1 = blocks.at(1); + + // Recent finalization + auto finalized = Checkpoint::from(genesis); + + auto store = createTestStore(100, config, block_1.hash(), block_1.hash(), + finalized, finalized, makeBlockMap(blocks)); + + auto target = store.getVoteTarget(); + + // Should target the head block since finalization is recent + EXPECT_EQ(target.root, block_1.hash()); + EXPECT_EQ(target.slot, 1); +} + +// Test vote target selection with very old finalized checkpoint. +TEST(TestVoteTargetCalculation, test_vote_target_with_old_finalized) { + auto blocks = makeBlocks(10); + + // Very old finalized checkpoint (slot 0) + auto finalized = Checkpoint::from(blocks.at(0)); + + // Current head is at slot 9 + auto &head = blocks.at(9); + + auto store = createTestStore(100, config, head.hash(), head.hash(), + finalized, finalized, makeBlockMap(blocks)); + + auto target = store.getVoteTarget(); + + // Should return a valid checkpoint + EXPECT_TRUE(store.hasBlock(target.root)); +} + +// Test that vote target walks back from head when needed. +TEST(TestVoteTargetCalculation, test_vote_target_walks_back_from_head) { + auto blocks = makeBlocks(3); + auto &genesis = blocks.at(0); + auto &block_1 = blocks.at(1); + auto &block_2 = blocks.at(2); + + // Finalized at genesis + auto finalized = Checkpoint::from(genesis); + + auto store = createTestStore(100, config, block_2.hash(), block_1.hash(), + finalized, finalized, makeBlockMap(blocks)); + + auto target = store.getVoteTarget(); + + // Should walk back towards safe target + EXPECT_TRUE(store.hasBlock(target.root)); +} + +// Test that vote target respects justifiable slot constraints. +TEST(TestVoteTargetCalculation, test_vote_target_justifiable_slot_constraint) { + // Create a long chain to test slot justification + auto blocks = makeBlocks(21); + + // Finalized very early (slot 0) + auto finalized = Checkpoint::from(blocks.at(0)); + + // Head at slot 20 + auto &head = blocks.at(20); + + auto store = createTestStore(100, config, head.hash(), head.hash(), + finalized, finalized, makeBlockMap(blocks)); + + auto target = store.getVoteTarget(); + + // Should return a justifiable slot + EXPECT_TRUE(store.hasBlock(target.root)); + + // Check that the slot is justifiable after finalized slot + EXPECT_TRUE(lean::isJustifiableSlot(finalized.slot, target.slot)); +} + +// Test vote target when head and safe_target are the same. +TEST(TestVoteTargetCalculation, + test_vote_target_with_same_head_and_safe_target) { + auto blocks = makeBlocks(2); + auto &genesis = blocks.at(0); + auto &head = blocks.at(1); + + auto finalized = Checkpoint::from(genesis); + + auto store = createTestStore(500, config, head.hash(), head.hash(), + finalized, finalized, makeBlockMap(blocks)); + + auto target = store.getVoteTarget(); + + // Should target the head (which is also safe_target) + EXPECT_EQ(target.root, head.hash()); + EXPECT_EQ(target.slot, head.slot); +} + +// Test get_fork_choice_head with validator votes. +TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_with_votes) { + auto blocks = makeBlocks(3); + auto &root = blocks.at(0); + auto &target = blocks.at(2); + + ForkChoiceStore::Votes votes; + votes[0] = SignedVote{ + .data = { + .validator_id = 0, + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), + }, + .signature = {}, + }; + + auto head = getForkChoiceHead(makeBlockMap(blocks), + Checkpoint::from(root), + votes, + 0); + + EXPECT_EQ(head, target.hash()); +} + +// Test get_fork_choice_head with no votes returns the root. +TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_no_votes) { + auto blocks = makeBlocks(3); + auto &root = blocks.at(0); + + ForkChoiceStore::Votes empty_votes; + auto head = + getForkChoiceHead(makeBlockMap(blocks), Checkpoint::from(root), empty_votes, 0); + + EXPECT_EQ(head, root.hash()); +} + +// Test get_fork_choice_head respects minimum score. +TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_with_min_score) { + auto blocks = makeBlocks(3); + auto &root = blocks.at(0); + auto &target = blocks.at(2); + + ForkChoiceStore::Votes votes; + votes[0] = SignedVote{ + .data = { + .validator_id = 0, + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), + }, + .signature = {}, + }; + + auto head = getForkChoiceHead(makeBlockMap(blocks), + Checkpoint::from(root), + votes, + 2); + + EXPECT_EQ(head, root.hash()); +} + +// Test get_fork_choice_head with multiple votes. +TEST(TestForkChoiceHeadFunction, test_get_fork_choice_head_multiple_votes) { + auto blocks = makeBlocks(3); + auto &root = blocks.at(0); + auto &target = blocks.at(2); + + ForkChoiceStore::Votes votes; + for (int i = 0; i < 3; ++i) { + votes[i] = SignedVote{ + .data = { + .validator_id = static_cast(i), + .slot = target.slot, + .head = Checkpoint::from(target), + .target = Checkpoint::from(target), + .source = Checkpoint::from(root), + }, + .signature = {}, + }; + } + + auto head = getForkChoiceHead(makeBlockMap(blocks), + Checkpoint::from(root), + votes, + 0); + + EXPECT_EQ(head, target.hash()); +} + +// Test basic safe target update. +TEST(TestSafeTargetComputation, test_update_safe_target_basic) { + auto blocks = makeBlocks(1); + auto &genesis = blocks.at(0); + + auto finalized = Checkpoint::from(genesis); + + auto store = createTestStore(100, config, genesis.hash(), genesis.hash(), + finalized, finalized, makeBlockMap(blocks)); + + // Update safe target (this tests the method exists and runs) + store.updateSafeTarget(); + + // Safe target should be set + EXPECT_EQ(store.getSafeTarget(), genesis.hash()); +} + +// Test safe target computation with votes. +TEST(TestSafeTargetComputation, test_safe_target_with_votes) { + auto blocks = makeBlocks(2); + auto &genesis = blocks.at(0); + auto &block_1 = blocks.at(1); + + auto finalized = Checkpoint::from(genesis); + + ForkChoiceStore::Votes new_votes; + new_votes[0] = SignedVote{ + .data = { + .validator_id = 0, + .slot = block_1.slot, + .head = Checkpoint::from(block_1), + .target = Checkpoint::from(block_1), + .source = Checkpoint::from(genesis), + }, + .signature = {}, + }; + new_votes[1] = SignedVote{ + .data = { + .validator_id = 1, + .slot = block_1.slot, + .head = Checkpoint::from(block_1), + .target = Checkpoint::from(block_1), + .source = Checkpoint::from(genesis), + }, + .signature = {}, + }; + + auto store = createTestStore(100, config, block_1.hash(), genesis.hash(), + finalized, finalized, makeBlockMap(blocks), {}, + {}, new_votes); + + // Update safe target with votes + store.updateSafeTarget(); + + // Should have computed a safe target + EXPECT_TRUE(store.hasBlock(store.getSafeTarget())); +} + +// Test vote target with only one block. +TEST(TestEdgeCases, test_vote_target_single_block) { + auto blocks = makeBlocks(1); + auto &genesis = blocks.at(0); + + auto finalized = Checkpoint::from(genesis); + + auto store = createTestStore(100, config, genesis.hash(), genesis.hash(), + finalized, finalized, makeBlockMap(blocks)); + + auto target = store.getVoteTarget(); + + EXPECT_EQ(target.root, genesis.hash()); + EXPECT_EQ(target.slot, genesis.slot); +} + +// Test validation of a valid attestation. +TEST(TestAttestationValidation, test_validate_attestation_valid) { + auto blocks = makeBlocks(3); + auto &source = blocks.at(1); + auto &target = blocks.at(2); + + auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + + // Create valid signed vote + // Should validate without error + EXPECT_OUTCOME_SUCCESS( + sample_store.validateAttestation(makeVote(source, target))); +} + +// Test validation fails when source slot > target slot. +TEST(TestAttestationValidation, test_validate_attestation_slot_order_invalid) { + auto blocks = makeBlocks(3); + // Later than target + auto &source = blocks.at(2); + // Earlier than source + auto &target = blocks.at(1); + + auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + + // Create invalid signed vote (source > target slot) + EXPECT_OUTCOME_ERROR( + sample_store.validateAttestation(makeVote(source, target))); +} + +// Test validation fails when referenced blocks are missing. +TEST(TestAttestationValidation, test_validate_attestation_missing_blocks) { + auto sample_store = createTestStore(); + + // Create signed vote referencing missing blocks + EXPECT_OUTCOME_ERROR(sample_store.validateAttestation({})); +} + +// Test validation fails when checkpoint slots don't match block slots. +TEST(TestAttestationValidation, + test_validate_attestation_checkpoint_slot_mismatch) { + auto blocks = makeBlocks(3); + auto &source = blocks.at(1); + auto &target = blocks.at(2); + + auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + + // Create signed vote with mismatched checkpoint slot + auto vote = makeVote(source, target); + ++vote.data.source.slot; + EXPECT_OUTCOME_ERROR(sample_store.validateAttestation(vote)); +} + +// Test validation fails for attestations too far in the future. +TEST(TestAttestationValidation, test_validate_attestation_too_far_future) { + auto blocks = makeBlocks(10); + auto &source = blocks.at(1); + auto &target = blocks.at(9); + + // Use very low genesis time (0) so that target at slot 9 is far in future (slot 9 > current slot + 1) + lean::Config low_time_config{.num_validators = 100, .genesis_time = 0}; + auto sample_store = createTestStore(0, low_time_config, {}, {}, {}, {}, makeBlockMap(blocks)); + + // Create signed vote for future slot (target slot 9 when current is ~0) + EXPECT_OUTCOME_ERROR( + sample_store.validateAttestation(makeVote(source, target))); +} + +// Test processing attestation from network gossip. +TEST(TestAttestationProcessing, test_process_network_attestation) { + auto blocks = makeBlocks(3); + auto &source = blocks.at(1); + auto &target = blocks.at(2); + + auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + + // Create valid signed vote + // Process as network attestation + EXPECT_OUTCOME_SUCCESS( + sample_store.processAttestation(makeVote(source, target), false)); + + // Vote should be added to new votes + EXPECT_EQ(getVote(sample_store.getLatestNewVotes()), Checkpoint::from(target)); +} + +// Test processing attestation from a block. +TEST(TestAttestationProcessing, test_process_block_attestation) { + auto blocks = makeBlocks(3); + auto &source = blocks.at(1); + auto &target = blocks.at(2); + + auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + + // Create valid signed vote + // Process as block attestation + EXPECT_OUTCOME_SUCCESS( + sample_store.processAttestation(makeVote(source, target), true)); + + // Vote should be added to known votes + EXPECT_EQ(getVote(sample_store.getLatestKnownVotes()), + Checkpoint::from(target)); +} + +// Test that newer attestations supersede older ones. +TEST(TestAttestationProcessing, test_process_attestation_superseding) { + auto blocks = makeBlocks(3); + auto &target_1 = blocks.at(1); + auto &target_2 = blocks.at(2); + + auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + + // Process first (older) attestation + EXPECT_OUTCOME_SUCCESS( + sample_store.processAttestation(makeVote(target_1, target_1), false)); + + // Process second (newer) attestation + EXPECT_OUTCOME_SUCCESS( + sample_store.processAttestation(makeVote(target_1, target_2), false)); + + // Should have the newer vote + EXPECT_EQ(getVote(sample_store.getLatestNewVotes()), + Checkpoint::from(target_2)); +} + +// Test that block attestations remove corresponding new votes. +TEST(TestAttestationProcessing, + test_process_attestation_from_block_supersedes_new) { + auto blocks = makeBlocks(3); + auto &source = blocks.at(1); + auto &target = blocks.at(2); + + auto sample_store = createTestStore(100, config, {}, {}, {}, {}, makeBlockMap(blocks)); + + // First process as network vote + auto signed_vote = makeVote(source, target); + EXPECT_OUTCOME_SUCCESS(sample_store.processAttestation(signed_vote, false)); + + // Should be in new votes + ASSERT_TRUE(getVote(sample_store.getLatestNewVotes())); + + // Process same vote as block attestation + EXPECT_OUTCOME_SUCCESS(sample_store.processAttestation(signed_vote, true)); + + // Vote should move to known votes and be removed from new votes + ASSERT_FALSE(getVote(sample_store.getLatestNewVotes())); + EXPECT_EQ(getVote(sample_store.getLatestKnownVotes()), + Checkpoint::from(target)); +} + +// Test basic time advancement. +TEST(TestTimeAdvancement, test_advance_time_basic) { + // Create a simple store with minimal setup - use 0 time interval so advanceTime is a no-op + auto sample_store = createTestStore(0, config); + + // Target time equal to genesis time - should be a no-op + auto target_time = sample_store.getConfig().genesis_time / 1000; + + // This should not throw an exception and should return empty result + auto result = sample_store.advanceTime(target_time); + EXPECT_TRUE(result.empty()); +} + +// Test time advancement without proposal. +TEST(TestTimeAdvancement, test_advance_time_no_proposal) { + // Create a simple store with minimal setup + auto sample_store = createTestStore(0, config); + + // Target time equal to genesis time - should be a no-op + auto target_time = sample_store.getConfig().genesis_time / 1000; + + // This should not throw an exception and should return empty result + auto result = sample_store.advanceTime(target_time); + EXPECT_TRUE(result.empty()); +} + +// Test advance_time when already at target time. +TEST(TestTimeAdvancement, test_advance_time_already_current) { + // Create a simple store with time already set + auto sample_store = createTestStore(100, config); + + // Target time is in the past relative to current time - should be a no-op + auto current_target = sample_store.getConfig().genesis_time / 1000; + + // Try to advance to past time (should be no-op) + auto result = sample_store.advanceTime(current_target); + EXPECT_TRUE(result.empty()); +} + +// Test advance_time with small time increment. +TEST(TestTimeAdvancement, test_advance_time_small_increment) { + // Create a simple store + auto sample_store = createTestStore(0, config); + + // Target time equal to genesis time - should be a no-op + auto target_time = sample_store.getConfig().genesis_time / 1000; + + auto result = sample_store.advanceTime(target_time); + EXPECT_TRUE(result.empty()); +} + +// Test basic time advancement (replacing interval ticking). +TEST(TestTimeAdvancement, test_advance_time_step_by_step) { + // Create a simple store + auto sample_store = createTestStore(0, config); + + // Multiple calls to advance time with same target - should all be no-ops + for (int i = 1; i <= 5; ++i) { + auto target_time = sample_store.getConfig().genesis_time / 1000; + auto result = sample_store.advanceTime(target_time); + EXPECT_TRUE(result.empty()); + } +} + +// Test time advancement with multiple steps. +TEST(TestTimeAdvancement, test_advance_time_multiple_steps) { + // Create a simple store + auto sample_store = createTestStore(0, config); + + // Multiple calls to advance time - should all be no-ops + for (int i = 1; i <= 5; ++i) { + auto target_time = sample_store.getConfig().genesis_time / 1000; + auto result = sample_store.advanceTime(target_time); + EXPECT_TRUE(result.empty()); + } +} + +// Test time advancement with vote processing. +TEST(TestTimeAdvancement, test_advance_time_with_votes) { + // Create a simple store + auto sample_store = createTestStore(0, config); + + // Advance time - should be no-op + auto target_time = sample_store.getConfig().genesis_time / 1000; + auto result = sample_store.advanceTime(target_time); + EXPECT_TRUE(result.empty()); +} + +// Test getting current head. +TEST(TestHeadSelection, test_get_head_basic) { + auto blocks = makeBlocks(1); + auto &genesis = blocks.at(0); + auto sample_store = createTestStore(100, config, genesis.hash(), genesis.hash(), + {}, {}, makeBlockMap(blocks)); + + // Get current head + auto head = sample_store.getHead(); + + // Should return expected head + EXPECT_EQ(head, genesis.hash()); +} + +// Test that advance time functionality works. +TEST(TestHeadSelection, test_advance_time_functionality) { + // Create a simple store + auto sample_store = createTestStore(0, config); + + // Advance time - should be no-op + auto target_time = sample_store.getConfig().genesis_time / 1000; + auto result = sample_store.advanceTime(target_time); + EXPECT_TRUE(result.empty()); +} + +// Test basic block production capability. +TEST(TestHeadSelection, test_produce_block_basic) { + // Create a simple store + auto sample_store = createTestStore(0, config); + + // Try to produce a block - should throw due to missing state, which is expected + EXPECT_THROW(sample_store.produceBlock(1, 1), std::exception); +}