Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e2031f2
leanSpec 802
turuslan Jun 1, 2026
2ec60ce
leanSpec 781
turuslan Jun 1, 2026
035071f
fix hive wait_for_request
turuslan Jun 2, 2026
157e5be
blocks by range
turuslan Jun 4, 2026
d13d963
fix infinite loop
turuslan Jun 4, 2026
6522dac
check block count
turuslan Jun 4, 2026
9402fb3
response error
turuslan Jun 4, 2026
efdb20e
fix sync and range
turuslan Jun 5, 2026
9c42476
hive test
turuslan Jun 5, 2026
526ee95
multiple validators support
turuslan Jun 30, 2026
523b993
fix finality multivalidator
turuslan Jun 30, 2026
8229a30
lean-spec-tests a3bf5d7, leanSpec db21cc2
turuslan Jul 7, 2026
5edb8e8
fix gmock
turuslan Jul 7, 2026
edcdc91
prepare
turuslan Jul 7, 2026
d612dba
test_gossip_attestation_with_invalid_signature
turuslan Jul 7, 2026
939cae4
test_block_with_duplicate_aggregated_attestation_data_rejected
turuslan Jul 7, 2026
cdde45c
test_aggregated_attestation_head_slot_mismatch_rejected
turuslan Jul 7, 2026
fd8be96
test_aggregated_attestation_at_disparity_boundary_allowed
turuslan Jul 7, 2026
80c22d6
test_attestation_just_beyond_disparity_boundary_rejected
turuslan Jul 7, 2026
02d7ad9
test_attestation_one_full_slot_in_future_rejected
turuslan Jul 7, 2026
3d0f7d2
test_tick_interval_0_skips_acceptance_when_not_proposer
turuslan Jul 7, 2026
2ca1c95
update test driver
turuslan Jul 7, 2026
fd614e3
remove fixed
turuslan Jul 7, 2026
a2bbbe9
test driver check anchor
turuslan Jul 7, 2026
c23fbdd
print node version early
turuslan Jul 9, 2026
a950638
manual fixture
turuslan Jul 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
11 changes: 2 additions & 9 deletions src/blockchain/block_storage.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,6 @@ namespace lean::blockchain {

// -- body --

/**
* Saves provided body of block to block storage
* @returns result of saving
*/
virtual outcome::result<void> putBlockBody(const BlockHash &block_hash,
const BlockBody &block_body) = 0;

/**
* Tries to get block body
* @returns block body or error
Expand Down Expand Up @@ -226,8 +219,8 @@ namespace lean::blockchain {

// -- special

[[nodiscard]] virtual outcome::result<SignedBlock> getSignedBlock(
const BlockHash &block_hash) const = 0;
[[nodiscard]] virtual outcome::result<std::optional<SignedBlock>>
tryGetSignedBlock(const BlockHash &block_hash) const = 0;
};

} // namespace lean::blockchain
9 changes: 0 additions & 9 deletions src/blockchain/block_tree.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@ namespace lean::blockchain {
*/
virtual outcome::result<void> addBlockHeader(const BlockHeader &header) = 0;

/**
* Adds block body to the storage
* @param block_hash that corresponds to the block which body we are adding
* @param block_body that we are adding
* @return result with success if block body was inserted. Error otherwise
*/
virtual outcome::result<void> addBlockBody(const BlockHash &block_hash,
const BlockBody &block_body) = 0;

/**
* Add an existent block to the tree
* @param block_hash is hash of the added block in the tree
Expand Down
105 changes: 55 additions & 50 deletions src/blockchain/fork_choice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,15 @@
#include "utils/retain_if.hpp"

namespace lean {
inline ValidatorIndex getValidatorId(
const log::Logger &logger, const ValidatorRegistry &validator_registry) {
auto &indices = validator_registry.currentValidatorIndices();
if (indices.size() != 1) {
SL_FATAL(logger, "multiple validators on same node are not supported");
}
return *indices.begin();
inline std::unordered_set<SubnetIndex> getSubnets(
const log::Logger &logger,
const ValidatorRegistry &validator_registry,
SubnetIndex subnet_count) {
std::unordered_set<SubnetIndex> subnets;
for (auto &index : validator_registry.currentValidatorIndices()) {
subnets.emplace(validatorSubnet(index, subnet_count));
}
return subnets;
}

ForkChoiceStore::ForkChoiceStore(
Expand All @@ -66,9 +68,9 @@ namespace lean {
config_(anchor_state->config),
validator_registry_(std::move(validator_registry)),
validator_keys_manifest_(std::move(validator_keys_manifest)),
validator_id_{getValidatorId(logger_, *validator_registry_)},
is_aggregator_{[chain_spec] { return chain_spec->isAggregator(); }},
subnet_count_{app_config->cliSubnetCount()} {
subnet_count_{app_config->cliSubnetCount()},
subnets_{getSubnets(logger_, *validator_registry_, subnet_count_)} {
metrics_->stf_latest_justified_slot()->set(
block_tree_->getLatestJustified().slot);
metrics_->stf_latest_finalized_slot()->set(
Expand Down Expand Up @@ -159,14 +161,18 @@ namespace lean {
latest_new_attestations_(std::move(latest_new_attestations)),
validator_registry_(std::move(validator_registry)),
validator_keys_manifest_(std::move(validator_keys_manifest)),
validator_id_{getValidatorId(logger_, *validator_registry_)},
is_aggregator_{[is_aggregator] { return is_aggregator; }},
subnet_count_{subnet_count} {}
subnet_count_{subnet_count},
subnets_{getSubnets(logger_, *validator_registry_, subnet_count_)} {}

void ForkChoiceStore::dontPropose() {
dont_propose_ = true;
}

void ForkChoiceStore::ignoreBlockSignature() {
ignore_block_signature_ = true;
}

inline crypto::xmss::XmssMessage attestationPayload(
const AttestationData &attestation_data) {
return sszHash(attestation_data);
Expand Down Expand Up @@ -640,31 +646,32 @@ namespace lean {
// Validate checkpoint slots match block slots
if (auto res = getBlockSlot(data.source.root);
not res.has_value() or res.value() != data.source.slot) {
SL_TRACE(logger_,
"Invalid attestation: inconsistent source slot",
data.target,
data.source);
SL_TRACE(logger_, "Invalid attestation: inconsistent source slot");
return Error::INVALID_ATTESTATION;
}
if (auto res = getBlockSlot(data.target.root);
not res.has_value() or res.value() != data.target.slot) {
SL_TRACE(logger_,
"Invalid attestation: inconsistent target slot",
data.target,
data.source);
SL_TRACE(logger_, "Invalid attestation: inconsistent target slot");
return Error::INVALID_ATTESTATION;
}
if (auto res = getBlockSlot(data.head.root);
not res.has_value() or res.value() != data.head.slot) {
SL_TRACE(logger_, "Invalid attestation: inconsistent head slot");
return Error::INVALID_ATTESTATION;
}

// Time Check

// Validate attestation is not too far in the future
// We allow a small margin for clock disparity (1 slot), but no further.
if (data.slot > getCurrentSlot() + 1) {
Interval max_admissible{.interval =
time_.interval + GOSSIP_DISPARITY_INTERVALS};
if (data.slot > max_admissible.slot()) {
SL_TRACE(logger_,
"Invalid attestation: too big clock disparity",
data.target,
data.source);
return Error::INVALID_ATTESTATION;
return Error::ATTESTATION_TOO_FAR_IN_FUTURE;
}

return outcome::success();
Expand Down Expand Up @@ -692,8 +699,8 @@ namespace lean {
return Error::INVALID_ATTESTATION;
}
if (is_aggregator_()
and validatorSubnet(signed_attestation.validator_id, subnet_count_)
== validatorSubnet(validator_id_, subnet_count_)) {
and subnets_.contains(
validatorSubnet(signed_attestation.validator_id, subnet_count_))) {
addSignatureToAggregate(signed_attestation.data,
signed_attestation.validator_id,
signed_attestation.signature);
Expand Down Expand Up @@ -825,15 +832,6 @@ namespace lean {
// - They must wait for interval tick acceptance before
// contributing to fork choice weights.

// Convert Store time to slots to check for "future" attestations.
auto time_slot = getCurrentSlot();

// Reject the attestation if:
// - its slot is strictly greater than our current slot.
if (attestation_slot > time_slot) {
return Error::INVALID_ATTESTATION;
}

// Fetch the previously stored "new" attestation for this validator.
auto latest_new_attestation = latest_new_attestations_.find(validator_id);

Expand All @@ -852,6 +850,10 @@ namespace lean {

bool ForkChoiceStore::validateBlockSignatures(
const SignedBlock &signed_block) const {
if (ignore_block_signature_) {
return true;
}

// Unpack the signed block components
const auto &block = signed_block.block;
const auto &signatures = signed_block.signature;
Expand Down Expand Up @@ -977,6 +979,15 @@ namespace lean {

OUTCOME_TRY(parent_state, getState(block.parent_root));

// Reject a block body that repeats the same vote data.
std::unordered_set<Hash> attestation_data_set;
for (auto &attestation : block.body.attestations) {
attestation_data_set.emplace(sszHash(attestation.data));
}
if (block.body.attestations.size() != attestation_data_set.size()) {
return Error::DUPLICATE_ATTESTATION_DATA;
}

// at this point parent state should be available so node should sync
// parent-chain if not available before adding block to forkchoice

Expand Down Expand Up @@ -1140,14 +1151,6 @@ namespace lean {
} else if (time_.phase() == 1) {
SL_TRACE(logger_, "Interval 1 of slot {}", current_slot);

// Ensure the head is updated before voting
auto ana_res = acceptNewAttestations();
if (ana_res.has_error()) {
SL_WARN(logger_,
"Failed to accept new attestations: {}",
ana_res.error());
}

Checkpoint head = head_;
auto target =
getAttestationTarget(getLatestJustified(), head_, std::nullopt);
Expand All @@ -1166,7 +1169,6 @@ namespace lean {

auto metric_time =
metrics_->lean_attestations_production_time_seconds()->timer();
std::optional<Attestation> attestation;
for (auto validator_index :
validator_registry_->currentValidatorIndices()) {
if (dont_propose_) {
Expand All @@ -1179,20 +1181,18 @@ namespace lean {
if (not keypair.has_value()) {
continue;
}
if (not attestation.has_value()) {
attestation = produceAttestation(current_slot,
validator_index,
getLatestJustified(),
head_,
std::nullopt);
}
auto attestation = produceAttestation(current_slot,
validator_index,
getLatestJustified(),
head_,
std::nullopt);
// sign attestation
auto payload = attestationPayload(attestation->data);
auto payload = attestationPayload(attestation.data);
crypto::xmss::XmssSignature signature =
xmss_provider_->sign(keypair->private_key, current_slot, payload);
metrics_->lean_pq_sig_attestation_signatures_total()->inc();
auto signed_attestation =
SignedAttestation::from(*attestation, signature);
SignedAttestation::from(attestation, signature);

// Dispatching send signed vote-only broadcasts to other peers.
// Current peer should process attestation directly
Expand All @@ -1207,6 +1207,11 @@ namespace lean {
SL_DEBUG(logger_,
"Produced vote for target={}",
signed_attestation.data.target);
if (is_aggregator_) {
addSignatureToAggregate(signed_attestation.data,
signed_attestation.validator_id,
signed_attestation.signature);
}
result.emplace_back(signed_attestation);
}

Expand Down
17 changes: 13 additions & 4 deletions src/blockchain/fork_choice.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <memory>
#include <optional>
#include <unordered_map>
#include <unordered_set>

#include <boost/di.hpp>
#include <qtils/shared_ref.hpp>
Expand Down Expand Up @@ -88,6 +89,8 @@ namespace lean {
SIGNATURE_COUNT_MISMATCH,
TOO_MANY_ATTESTATIONS,
NO_KEYPAIR,
DUPLICATE_ATTESTATION_DATA,
ATTESTATION_TOO_FAR_IN_FUTURE,
};
Q_ENUM_ERROR_CODE_FRIEND(Error) {
using E = decltype(e);
Expand All @@ -110,6 +113,10 @@ namespace lean {
return "Too many attestations in block";
case E::NO_KEYPAIR:
return "No keypair";
case E::DUPLICATE_ATTESTATION_DATA:
return "DUPLICATE_ATTESTATION_DATA";
case E::ATTESTATION_TOO_FAR_IN_FUTURE:
return "ATTESTATION_TOO_FAR_IN_FUTURE";
}
abort();
}
Expand Down Expand Up @@ -161,6 +168,7 @@ namespace lean {
uint64_t subnet_count);

void dontPropose();
void ignoreBlockSignature();

// Compute the latest block that the validator is allowed to choose as the
// target
Expand Down Expand Up @@ -576,13 +584,14 @@ namespace lean {
std::unordered_map<Hash, AttestationsByData> attestations_by_data_;
qtils::SharedRef<ValidatorRegistry> validator_registry_;
qtils::SharedRef<app::ValidatorKeysManifest> validator_keys_manifest_;
/**
* Index of the validator running this store instance.
*/
ValidatorIndex validator_id_;
std::function<bool()> is_aggregator_;
uint64_t subnet_count_;
/**
* Validator subnets this node belongs to.
*/
std::unordered_set<SubnetIndex> subnets_;
bool dont_propose_ = false;
bool ignore_block_signature_ = false;
std::unordered_map<BlockHash, Slot> anchor_block_slots_;
};

Expand Down
5 changes: 5 additions & 0 deletions src/blockchain/fork_choice_mutex.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ namespace lean {
qtils::SharedRef<ForkChoiceStore> fork_choice)
: fork_choice_{std::move(fork_choice)} {}

Checkpoint ForkChoiceStoreMutex::getHead() const {
std::shared_lock lock{mutex_};
return fork_choice_->getHead();
}

Checkpoint ForkChoiceStoreMutex::getLatestFinalized() const {
std::shared_lock lock{mutex_};
return fork_choice_->getLatestFinalized();
Expand Down
1 change: 1 addition & 0 deletions src/blockchain/fork_choice_mutex.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ namespace lean {
public:
ForkChoiceStoreMutex(qtils::SharedRef<ForkChoiceStore> fork_choice);

Checkpoint getHead() const;
Checkpoint getLatestFinalized() const;
Checkpoint getLatestJustified() const;
outcome::result<std::shared_ptr<const State>> getState(
Expand Down
2 changes: 1 addition & 1 deletion src/blockchain/impl/anchor_block_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace lean {

namespace lean::blockchain {

class AnchorBlockImpl final : public AnchorBlock, Singleton<AnchorBlock> {
class AnchorBlockImpl final : public AnchorBlock {
public:
AnchorBlockImpl(const AnchorState &state);
};
Expand Down
3 changes: 2 additions & 1 deletion src/blockchain/impl/anchor_state_impl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "app/configuration.hpp"
#include "app/state_manager.hpp"
#include "blockchain/genesis_config.hpp"
#include "blockchain/impl/anchor_block_impl.hpp"
#include "modules/networking/ssl_context.hpp"
#include "modules/networking/state_sync_client.hpp"
#include "modules/production/read_config_yaml.hpp"
Expand Down Expand Up @@ -52,7 +53,7 @@ namespace lean::blockchain {
SL_INFO(logger,
"State successfully acquired from URL: {}",
state_sync_url);
static_cast<State &>(*this) = state_res.value();
AnchorState::operator=(state_res.value());
return;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/blockchain/impl/anchor_state_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ namespace lean::app {

namespace lean::blockchain {

class AnchorStateImpl final : public AnchorState, Singleton<AnchorState> {
class AnchorStateImpl final : public AnchorState {
public:
explicit AnchorStateImpl(State state) {
State::operator=(std::move(state));
Expand Down
Loading
Loading