Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions DEPRECATED.md
Original file line number Diff line number Diff line change
@@ -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:

Expand Down
1 change: 1 addition & 0 deletions src/blockchain/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#

add_library(blockchain
fork_choice.cpp
impl/block_storage_error.cpp
impl/block_storage_impl.cpp
impl/block_storage_initializer.cpp
Expand Down
309 changes: 309 additions & 0 deletions src/blockchain/fork_choice.cpp
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;
}

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;
}

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) {
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;
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));
}

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
Loading
Loading