-
Notifications
You must be signed in to change notification settings - Fork 3
Fork choice #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Fork choice #18
Changes from 3 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,309 @@ | ||
| /** | ||
| * Copyright Quadrivium LLC | ||
| * All Rights Reserved | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
|
|
||
| #include "blockchain/fork_choice.hpp" | ||
|
|
||
| #include "types/signed_block.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<Checkpoint> ForkChoiceStore::getLatestJustified() { | ||
| using Key = std::tuple<Slot, BlockHash>; | ||
| std::optional<Key> 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(); | ||
| return Checkpoint{.root = hash, .slot = slot}; | ||
| } | ||
|
|
||
| void ForkChoiceStore::updateHead() { | ||
| if (auto latest_justified = getLatestJustified()) { | ||
| latest_justified_ = latest_justified.value(); | ||
| } | ||
| head_ = | ||
| getForkChoiceHead(blocks_, latest_justified_, latest_known_votes_, 0); | ||
|
|
||
| latest_finalized_ = states_[head_].latest_finalized; | ||
turuslan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
turuslan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| void ForkChoiceStore::acceptNewVotes() { | ||
| for (auto &[voter, vote] : latest_new_votes_) { | ||
| latest_known_votes_[voter] = vote; | ||
| } | ||
| latest_new_votes_.clear(); | ||
| updateHead(); | ||
| } | ||
|
|
||
| void ForkChoiceStore::tickInterval(bool has_proposal) { | ||
| ++time_; | ||
| auto current_interval = time_ % INTERVALS_PER_SLOT; | ||
| if (current_interval == 0) { | ||
| if (has_proposal) { | ||
| acceptNewVotes(); | ||
| } | ||
| } else if (current_interval == 1) { | ||
| // validators will vote in this interval using safe target previously | ||
| // computed | ||
| } else if (current_interval == 2) { | ||
| updateSafeTarget(); | ||
| } else { | ||
| acceptNewVotes(); | ||
| } | ||
| } | ||
|
|
||
| void ForkChoiceStore::advanceTime(Interval time, bool has_proposal) { | ||
| // Calculate the number of intervals that have passed since genesis | ||
| auto tick_interval_time = | ||
| (time - config_.genesis_time) / SECONDS_PER_INTERVAL; | ||
|
|
||
| // Tick the store one interval at a time until the target time is reached | ||
| while (time_ < tick_interval_time) { | ||
| // Determine if a proposal should be signaled for the next interval | ||
| auto should_signal_proposal = | ||
| has_proposal and (time_ + 1) == tick_interval_time; | ||
|
|
||
| // Tick the interval and potentially signal a proposal | ||
| tickInterval(should_signal_proposal); | ||
| } | ||
| } | ||
|
|
||
| BlockHash ForkChoiceStore::getProposalHead(Slot slot) { | ||
| auto slot_time = config_.genesis_time + slot * SECONDS_PER_SLOT; | ||
| // this would be a no-op if the store is already ticked to the current | ||
| // time | ||
| advanceTime(slot_time, true); | ||
| // this would be a no-op or just a fast compute if store was already | ||
| // ticked to accept new votes for a registered validator with the node | ||
| acceptNewVotes(); | ||
| return head_; | ||
| } | ||
|
|
||
| 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<void> ForkChoiceStore::validateAttestation( | ||
| const SignedVote &signed_vote) { | ||
| 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 | ||
| auto current_slot = time_ / SECONDS_PER_INTERVAL; | ||
| if (vote.slot > current_slot + 1) { | ||
| return Error::INVALID_ATTESTATION; | ||
turuslan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| return outcome::success(); | ||
| } | ||
|
|
||
| outcome::result<void> 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.slot < vote.slot) { | ||
turuslan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| latest_known_votes_.insert_or_assign(validator_id, vote.target); | ||
| } | ||
|
|
||
| // 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.slot <= vote.target.slot) { | ||
| latest_new_votes_.erase(latest_new_vote); | ||
| } | ||
| } else { | ||
| // forkchoice should be correctly ticked to current time before importing | ||
| // gossiped attestations | ||
| auto time_slots = time_ / SECONDS_PER_INTERVAL; | ||
turuslan marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (vote.slot > time_slots) { | ||
| 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.slot < vote.target.slot) { | ||
| latest_new_votes_.insert_or_assign(validator_id, vote.target); | ||
| } | ||
| } | ||
|
|
||
| return outcome::success(); | ||
| } | ||
|
|
||
| outcome::result<void> 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 | ||
| OUTCOME_TRY(processAttestation(signed_vote, true)); | ||
turuslan marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| updateHead(); | ||
|
|
||
| return outcome::success(); | ||
| } | ||
|
|
||
| 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<BlockHash, uint64_t> 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.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<uint64_t, Slot, BlockHash>; | ||
| std::unordered_multimap<BlockHash, Checkpoint> 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 getForkchoiceStore(State anchor_state, Block anchor_block) { | ||
| BOOST_ASSERT(anchor_block.state_root == sszHash(anchor_state)); | ||
| auto anchor_root = anchor_block.hash(); | ||
| ForkChoiceStore store{ | ||
| .time_ = anchor_block.slot * INTERVALS_PER_SLOT, | ||
| .config_ = anchor_state.config, | ||
| .head_ = anchor_root, | ||
| .safe_target_ = anchor_root, | ||
| .latest_justified_ = anchor_state.latest_justified, | ||
| .latest_finalized_ = anchor_state.latest_finalized, | ||
| }; | ||
| store.blocks_.emplace(anchor_root, std::move(anchor_block)); | ||
| store.states_.emplace(anchor_root, std::move(anchor_state)); | ||
| return store; | ||
| } | ||
| } // namespace lean | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.