Weber Fork Choice Rule
Notice: This document is a work-in-progress for researchers and implementers of the Weber protocol.
Table of contents
Introduction
The fork choice rule is the mechanism by which nodes in the Weber network reach consensus about the canonical chain. Weber builds upon the latest message driven Greedy Heaviest Observed SubTree (LMD-GHOST) algorithm but introduces significant enhancements:
- Reputation-weighted voting: Attestations from validators with higher reputation scores carry more weight
- Optimistic fork choice acceleration: A mechanism to quickly converge on a head without waiting for full attestation collection
- Multi-factor scoring: Block scoring based on multiple factors beyond simple vote counting
This document specifies the enhanced fork choice algorithm, including the procedures for block scoring, vote accounting, and chain selection.
Preliminaries
Type definitions
| # Validator index
ValidatorIndex = uint64
# Epoch number
Epoch = uint64
# Slot number
Slot = uint64
# A root/hash of a beacon block
Root = bytes32
# Validator reputation score (0-1000)
ReputationScore = uint16
# Boolean quick finality bit
QuickFinalityBit = bool
# Checkpoint for justification/finalization
class Checkpoint:
epoch: Epoch
root: Root
# Store object to track fork choice state
class Store:
# Time
time: uint64
genesis_time: uint64
# Blocks
blocks: Dict[Root, BeaconBlock]
block_states: Dict[Root, BeaconState]
# Checkpoints
justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
# Best justified checkpoint seen
best_justified_checkpoint: Checkpoint
# Weber-specific additions
validator_reputations: Dict[ValidatorIndex, ReputationScore]
validator_latest_messages: Dict[ValidatorIndex, LatestMessage]
optimistic_checkpoints: List[Checkpoint]
block_scores: Dict[Root, float]
quick_finality_votes: Dict[Root, Set[ValidatorIndex]]
# Cache for vote weights
block_vote_weights: Dict[Root, float]
# Online status tracking
validator_online_status: Dict[ValidatorIndex, bool]
# Last attestation time for each validator
validator_last_attestation: Dict[ValidatorIndex, uint64]
# Chain health metrics
chain_health: ChainHealthMetrics
|
| class LatestMessage:
epoch: Epoch
root: Root
|
| class ChainHealthMetrics:
participation_rate: float
reorg_count: uint64
attestation_inclusion_time: float
last_finalized_epoch: Epoch
finality_distance: uint64
|
Algorithm constants
| # Weber-specific constants for fork choice
REPUTATION_WEIGHT_FACTOR = 0.5 # How much reputation affects vote weight (0-1)
OPTIMISTIC_FACTOR = 0.2 # Weight given to optimistic votes
QUICK_FINALITY_THRESHOLD = 3 # Number of quick finality votes needed
MIN_REPUTATION_FOR_QUICK_FINALITY = 800 # Minimum reputation score to submit quick finality votes
QUICK_FINALITY_WEIGHT = 2.0 # Weight multiplier for quick finality votes
MAX_OPTIMISTIC_BOOST = 0.3 # Maximum boost from optimistic voting
VOTE_WEIGHT_SMOOTHING = 0.1 # Smoothing factor for weight calculations
OPTIMISTIC_CHECK_INTERVAL = 32 # Number of slots between optimistic decision checks
BASE_REPUTATION_SCORE = 500 # Starting reputation for new validators
MAX_REPUTATION_SCORE = 1000 # Maximum possible reputation score
MIN_REPUTATION_SCORE = 0 # Minimum possible reputation score
ONLINE_THRESHOLD_SLOTS = 32 # Number of slots before marking a validator offline
|
Fork choice state
Fork choice information is maintained in a Store object. The Weber fork choice extends the standard Store with additional fields to track validator reputations, optimistic checkpoints, and quick finality votes.
| def get_forkchoice_store(state: BeaconState, genesis_time: uint64) -> Store:
"""
Initialize a fork choice store from the given genesis state and time.
"""
current_epoch = get_current_epoch(state)
justified_checkpoint = Checkpoint(
epoch=current_epoch,
root=state.current_justified_checkpoint.root
)
finalized_checkpoint = Checkpoint(
epoch=current_epoch,
root=state.finalized_checkpoint.root
)
# Standard store initialization
store = Store(
time=uint64(genesis_time),
genesis_time=uint64(genesis_time),
justified_checkpoint=justified_checkpoint,
finalized_checkpoint=finalized_checkpoint,
best_justified_checkpoint=justified_checkpoint,
blocks={},
block_states={},
)
# Add genesis block
genesis_block_root = state.latest_block_header.hash_tree_root()
store.blocks[genesis_block_root] = BeaconBlock()
store.block_states[genesis_block_root] = state.copy()
# Weber-specific initializations
store.validator_reputations = {
i: BASE_REPUTATION_SCORE for i in range(len(state.validators))
}
store.validator_latest_messages = {}
store.optimistic_checkpoints = []
store.block_scores = {}
store.quick_finality_votes = {}
store.block_vote_weights = {}
store.validator_online_status = {
i: True for i in range(len(state.validators))
}
store.validator_last_attestation = {
i: uint64(genesis_time) for i in range(len(state.validators))
}
store.chain_health = ChainHealthMetrics(
participation_rate=1.0,
reorg_count=0,
attestation_inclusion_time=0.0,
last_finalized_epoch=finalized_checkpoint.epoch,
finality_distance=0
)
return store
|
Standard LMD-GHOST fork choice rule
The LMD-GHOST (Latest Message Driven Greedy Heaviest Observed SubTree) algorithm is the foundation of Weber's fork choice rule. This section specifies the standard algorithm before Weber's enhancements.
| def get_ancestor(store: Store, root: Root, slot: Slot) -> Root:
"""
Get the ancestor of the block with the given `root` at the given `slot`.
"""
block = store.blocks[root]
if block.slot > slot:
return get_ancestor(store, block.parent_root, slot)
elif block.slot == slot:
return root
else:
# root is older than queried slot, thus a skip slot. Return most recent root for slot.
return root
|
| def get_latest_attesting_balance(store: Store, root: Root) -> Gwei:
"""
Return the combined effective balance of all validators that have an attestation
for the block with the given `root`.
"""
state = store.block_states[root]
active_indices = get_active_validator_indices(state, get_current_epoch(state))
total_balance = Gwei(0)
for validator_index in active_indices:
if validator_index in store.validator_latest_messages:
latest_message = store.validator_latest_messages[validator_index]
voting_root = latest_message.root
if get_ancestor(store, voting_root, store.blocks[root].slot) == root:
total_balance += state.validators[validator_index].effective_balance
return total_balance
|
| def get_head_standard(store: Store) -> Root:
"""
Return the head block root according to the standard LMD-GHOST algorithm.
"""
# Start from justified root
head = store.justified_checkpoint.root
# Climb up to current slot
justified_slot = store.blocks[head].slot
current_slot = (store.time - store.genesis_time) // SECONDS_PER_SLOT
while justified_slot < current_slot:
children = [
root for root in store.blocks
if store.blocks[root].parent_root == head
and store.blocks[root].slot > justified_slot
]
if len(children) == 0:
# No children, return current head
return head
# Find heaviest child
head = max(children, key=lambda root: get_latest_attesting_balance(store, root))
justified_slot = store.blocks[head].slot
return head
|
Weber reputation-weighted fork choice
Overview
Weber enhances the standard LMD-GHOST algorithm by applying reputation weights to validator votes. This helps mitigate various attack vectors and creates stronger incentives for validators to follow the protocol rules.
Attestation weighting
In Weber, the weight of an attestation depends on the validator's reputation score:
| def get_validator_weight(store: Store, validator_index: ValidatorIndex) -> float:
"""
Calculate the voting weight of a validator based on reputation.
"""
if validator_index not in store.validator_reputations:
return 1.0 # Default weight
reputation = store.validator_reputations[validator_index]
# Apply non-linear weighting function to reputation
# This makes high reputation more valuable while ensuring some baseline weight
base_weight = 1.0
reputation_weight = (reputation / MAX_REPUTATION_SCORE) ** 2 # Squared for steeper curve
# Final weight is a combination of base weight and reputation
final_weight = base_weight * (1.0 - REPUTATION_WEIGHT_FACTOR) + reputation_weight * REPUTATION_WEIGHT_FACTOR
# Ensure weight is always positive but capped
return max(0.1, min(final_weight, 2.0))
|
| def update_validator_reputation(store: Store, validator_index: ValidatorIndex, delta: int) -> None:
"""
Update the reputation score of a validator with the given delta.
Ensures the score stays within bounds.
"""
if validator_index not in store.validator_reputations:
store.validator_reputations[validator_index] = BASE_REPUTATION_SCORE
current_score = store.validator_reputations[validator_index]
new_score = current_score + delta
# Ensure the score stays within bounds
clamped_score = max(MIN_REPUTATION_SCORE, min(new_score, MAX_REPUTATION_SCORE))
store.validator_reputations[validator_index] = clamped_score
|
| def update_validator_recent_activity(store: Store, validator_index: ValidatorIndex) -> None:
"""
Update the record of a validator's most recent activity.
Used to track online status.
"""
store.validator_last_attestation[validator_index] = store.time
# Mark validator as online if they were offline
if not store.validator_online_status.get(validator_index, True):
store.validator_online_status[validator_index] = True
|
| def update_validator_online_status(store: Store) -> None:
"""
Update the online status of all validators based on recent activity.
"""
current_time = store.time
for validator_index in store.validator_last_attestation:
last_attestation_time = store.validator_last_attestation[validator_index]
offline_threshold = ONLINE_THRESHOLD_SLOTS * SECONDS_PER_SLOT
if current_time - last_attestation_time > offline_threshold:
store.validator_online_status[validator_index] = False
|
Vote accounting
Weber tracks votes with reputation-based weighting:
| def process_attestation_with_weight(store: Store, attestation: Attestation) -> None:
"""
Process an attestation and update the store with weighted votes.
"""
target = attestation.data.target
beacon_block_root = attestation.data.beacon_block_root
# Get attesting indices
attesting_indices = get_attesting_indices(attestation)
for validator_index in attesting_indices:
# Skip inactive validators
if not is_active_validator(store, validator_index):
continue
# Update latest message
store.validator_latest_messages[validator_index] = LatestMessage(
epoch=target.epoch,
root=beacon_block_root
)
# Update recent activity timestamp
update_validator_recent_activity(store, validator_index)
# Reward validators for timely attestation
slot_delay = get_attestation_delay(store, attestation)
if slot_delay <= TARGET_ATTESTATION_DELAY:
update_validator_reputation(store, validator_index, REPUTATION_REWARD_TIMELY_ATTESTATION)
# Invalidate affected block scores
invalidate_block_scores_for_attestation(store, attestation)
# Update chain health metrics
update_chain_health_metrics(store)
|
Enhanced fork choice algorithm
Weber's primary fork choice algorithm extends LMD-GHOST with reputation weighting:
| def get_latest_attesting_weighted_balance(store: Store, root: Root) -> float:
"""
Return the reputation-weighted balance of all validators attesting to the block with the given root.
"""
state = store.block_states[root]
active_indices = get_active_validator_indices(state, get_current_epoch(state))
total_weighted_balance = 0.0
for validator_index in active_indices:
if validator_index in store.validator_latest_messages:
latest_message = store.validator_latest_messages[validator_index]
voting_root = latest_message.root
if get_ancestor(store, voting_root, store.blocks[root].slot) == root:
# Apply validator-specific weight
weight = get_validator_weight(store, validator_index)
effective_balance = state.validators[validator_index].effective_balance
total_weighted_balance += effective_balance * weight
return total_weighted_balance
|
| def compute_block_score(store: Store, root: Root) -> float:
"""
Compute a comprehensive score for a block, considering:
1. Weighted attestation balance
2. Quick finality votes
3. Block proposer reputation
"""
# Use cached score if available and recent
if root in store.block_scores and is_score_recent(store, root):
return store.block_scores[root]
# Get attestation weight
attestation_weight = get_latest_attesting_weighted_balance(store, root)
# Apply quick finality boost if applicable
quick_finality_boost = 0.0
if root in store.quick_finality_votes:
num_votes = len(store.quick_finality_votes[root])
if num_votes >= QUICK_FINALITY_THRESHOLD:
quick_finality_boost = attestation_weight * QUICK_FINALITY_WEIGHT
# Apply proposer reputation bonus
proposer_index = store.blocks[root].proposer_index
proposer_reputation = store.validator_reputations.get(proposer_index, BASE_REPUTATION_SCORE)
proposer_bonus = (proposer_reputation / MAX_REPUTATION_SCORE) * 0.1 * attestation_weight
# Calculate final score
final_score = attestation_weight + quick_finality_boost + proposer_bonus
# Cache the result
store.block_scores[root] = final_score
return final_score
|
| def get_weber_head(store: Store) -> Root:
"""
Return the head block root according to Weber's enhanced fork choice algorithm.
"""
# Start from justified root
head = store.best_justified_checkpoint.root
# Climb up to current slot
justified_slot = store.blocks[head].slot
current_slot = (store.time - store.genesis_time) // SECONDS_PER_SLOT
while justified_slot < current_slot:
children = [
root for root in store.blocks
if store.blocks[root].parent_root == head
and store.blocks[root].slot > justified_slot
]
if len(children) == 0:
# No children, return current head
return head
# Find heaviest child using Weber's block scoring
head = max(children, key=lambda root: compute_block_score(store, root))
justified_slot = store.blocks[head].slot
return head
|
Weber Optimistic Fork Choice
Weber introduces an optimistic fork choice mechanism to accelerate consensus by quickly converging on a likely head even before full attestation collection.
Optimistic GHOST
| def should_use_optimistic_head(store: Store) -> bool:
"""
Determine if the optimistic head selection should be used.
Depends on network health and validator participation.
"""
# Check if we have enough validators online
min_online_validators = len(store.validator_online_status) * 0.67
online_count = sum(1 for status in store.validator_online_status.values() if status)
if online_count < min_online_validators:
return False
# Check if chain is healthy
if store.chain_health.participation_rate < 0.8:
return False
if store.chain_health.finality_distance > 4:
return False
return True
|
| def get_optimistic_head(store: Store) -> Root:
"""
Select a head based on optimistic metrics for faster convergence.
"""
# Start with the standard justified root
head = store.best_justified_checkpoint.root
# Find the path with highest proposer reputation
justified_slot = store.blocks[head].slot
current_slot = (store.time - store.genesis_time) // SECONDS_PER_SLOT
while justified_slot < current_slot:
children = [
root for root in store.blocks
if store.blocks[root].parent_root == head
and store.blocks[root].slot > justified_slot
]
if len(children) == 0:
return head
# Score children by a combination of:
# 1. Proposer reputation
# 2. Initial attestations (with accelerated counting)
# 3. Whether they extend the previously optimistic chain
scored_children = []
for child in children:
proposer_index = store.blocks[child].proposer_index
proposer_score = store.validator_reputations.get(proposer_index, BASE_REPUTATION_SCORE)
# Initial attestation score with optimistic weighting
attestation_score = compute_initial_attestation_score(store, child)
# Boost score if this extends a previous optimistic selection
previous_boost = 1.0
for checkpoint in store.optimistic_checkpoints:
if get_ancestor(store, child, store.blocks[checkpoint.root].slot) == checkpoint.root:
previous_boost = 1.0 + MAX_OPTIMISTIC_BOOST
break
# Calculate final score for this child
child_score = (proposer_score / MAX_REPUTATION_SCORE) * attestation_score * previous_boost
scored_children.append((child, child_score))
# Choose highest scoring child
if not scored_children:
return head
head = max(scored_children, key=lambda item: item[1])[0]
justified_slot = store.blocks[head].slot
# Record this optimistic selection
current_epoch = compute_epoch_at_slot(current_slot)
store.optimistic_checkpoints.append(Checkpoint(epoch=current_epoch, root=head))
# Keep only recent checkpoints
if len(store.optimistic_checkpoints) > 10:
store.optimistic_checkpoints.pop(0)
return head
|
Fork choice with quick finality
Quick finality allows validators with high reputation to accelerate block finalization:
| def on_quick_finality_vote(store: Store, vote: QuickFinalityVote) -> None:
"""
Process a vote for quick finality from a highly reputable validator.
"""
validator_index = vote.validator_index
block_root = vote.block_root
# Check if validator has high enough reputation
if validator_index not in store.validator_reputations or store.validator_reputations[validator_index] < MIN_REPUTATION_FOR_QUICK_FINALITY:
return
# Register vote
if block_root not in store.quick_finality_votes:
store.quick_finality_votes[block_root] = set()
store.quick_finality_votes[block_root].add(validator_index)
# If threshold reached, update justified checkpoint if better than current
vote_count = len(store.quick_finality_votes[block_root])
if vote_count >= QUICK_FINALITY_THRESHOLD:
# Get block and state
block = store.blocks[block_root]
state = store.block_states[block_root]
# Create checkpoint
quick_checkpoint = Checkpoint(
epoch=compute_epoch_at_slot(block.slot),
root=block_root
)
# If better than current justified, update best justified
if quick_checkpoint.epoch > store.best_justified_checkpoint.epoch:
store.best_justified_checkpoint = quick_checkpoint
|
Recovery from optimistic forks
Weber's optimistic fork choice includes safety mechanisms to recover from incorrect optimistic decisions:
| def is_network_healthy(store: Store) -> bool:
"""
Determine if the network is healthy enough for optimistic operations.
"""
# Check participation rate
if store.chain_health.participation_rate < 0.8:
return False
# Check finality lag
current_epoch = compute_epoch_at_slot((store.time - store.genesis_time) // SECONDS_PER_SLOT)
if current_epoch - store.chain_health.last_finalized_epoch > 4:
return False
# Check attestation inclusion time
if store.chain_health.attestation_inclusion_time > 2 * SECONDS_PER_SLOT:
return False
return True
|
| def is_checkpoint_justified_by_vote_count(store: Store, checkpoint: Checkpoint) -> bool:
"""
Check if a checkpoint has enough votes to be considered justified by traditional means.
"""
# Get state at checkpoint
checkpoint_state = store.block_states[checkpoint.root]
# Count votes for this checkpoint
epoch = checkpoint.epoch
total_balance = get_total_active_balance(checkpoint_state)
voted_balance = Gwei(0)
for validator_index, latest_message in store.validator_latest_messages.items():
if latest_message.epoch >= epoch:
attestation_root = latest_message.root
if get_ancestor(store, attestation_root, store.blocks[checkpoint.root].slot) == checkpoint.root:
if validator_index < len(checkpoint_state.validators):
voted_balance += checkpoint_state.validators[validator_index].effective_balance
# Check if balance is sufficient for justification
return voted_balance * 3 >= total_balance * 2 # 2/3 threshold
|
| def check_and_revert_optimistic_decisions(store: Store) -> None:
"""
Periodically check if optimistic decisions should be reverted.
"""
# If the network seems unhealthy, fall back to standard fork choice
if not is_network_healthy(store):
# Clear optimistic votes
store.quick_finality_votes = {}
# Reset to standard justified checkpoint if it differs
if store.best_justified_checkpoint != store.justified_checkpoint:
proposed_justified = store.best_justified_checkpoint
# Only keep it if it's properly justified by attestations
if is_checkpoint_justified_by_vote_count(store, proposed_justified):
# Keep the optimistic checkpoint
pass
else:
# Revert to the standard justified checkpoint
store.best_justified_checkpoint = store.justified_checkpoint
# Recalculate head using standard algorithm
get_weber_head(store)
|
Fork choice API
Actions
| def on_tick(store: Store, time: uint64) -> None:
"""
Called when the current time increases.
"""
# Standard tick processing
previous_time = store.time
store.time = time
# Check for any new attestation/block slots
previous_slot = previous_time // SECONDS_PER_SLOT
current_slot = time // SECONDS_PER_SLOT
if current_slot > previous_slot:
# Process any skipped slots
for slot in range(previous_slot + 1, current_slot + 1):
# Update state for skipped slots
pass
# Weber addition: Periodically check optimistic decisions
if time % OPTIMISTIC_CHECK_INTERVAL == 0:
check_and_revert_optimistic_decisions(store)
# Update validator online status
update_validator_online_status(store)
|
| def on_attestation(store: Store, attestation: Attestation) -> None:
"""
Called when an attestation is received.
"""
# Validate attestation
validate_attestation(store, attestation)
# Standard attestation processing
target = attestation.data.target
beacon_block_root = attestation.data.beacon_block_root
# Weber addition: Process with reputation weighting
process_attestation_with_weight(store, attestation)
# Update validator reputations
for validator_index in get_attesting_indices(attestation):
if validator_index in store.validator_reputations:
# Update recent attestation time
update_validator_recent_activity(store, validator_index)
# Invalidate cached scores along this path
invalidate_block_scores_for_attestation(store, attestation)
|
| def on_block(store: Store, block: BeaconBlock) -> None:
"""
Called when a block is received.
"""
# Validate the block
validate_block(store, block)
# Get block root
block_root = hash_tree_root(block)
# Store the block
store.blocks[block_root] = block
# Store the post-state
state = state_transition(store.block_states[block.parent_root], block)
store.block_states[block_root] = state
# Update justified checkpoint if needed
if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
# Update standard justified checkpoint
store.justified_checkpoint = state.current_justified_checkpoint
# Update finalized checkpoint if needed
if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch:
store.finalized_checkpoint = state.finalized_checkpoint
# When finalizing a new epoch, we can cleanup old data
cleanup_fork_choice_store(store)
# Weber addition: Process quick finality votes in block
for vote in block.body.quick_finality_votes:
on_quick_finality_vote(store, vote)
# Update block scores
calculate_and_cache_block_score(store, block_root)
# Update proposer reputation
update_proposer_reputation(store, block.proposer_index)
|
| def update_proposer_reputation(store: Store, proposer_index: ValidatorIndex) -> None:
"""
Update the reputation of a block proposer.
"""
# Reward proposers for timely blocks
update_validator_reputation(store, proposer_index, REPUTATION_REWARD_BLOCK_PROPOSAL)
# Note: In a full implementation, this would check the quality of the block
# (e.g., including all available attestations, transactions, etc.)
|
Accessors
| def get_head(store: Store) -> Root:
"""
Get the current chain head according to Weber fork choice.
This is the main entry point for fork choice.
"""
# Check if we should use optimistic head selection
if should_use_optimistic_head(store):
return get_optimistic_head(store)
else:
return get_weber_head(store)
|
| def get_justified_checkpoint(store: Store) -> Checkpoint:
"""
Get the justified checkpoint.
"""
return store.justified_checkpoint
|
| def get_finalized_checkpoint(store: Store) -> Checkpoint:
"""
Get the finalized checkpoint.
"""
return store.finalized_checkpoint
|
Implementation strategies
Efficient fork choice updates
| def invalidate_block_scores_for_attestation(store: Store, attestation: Attestation) -> None:
"""
Efficiently invalidate cached scores that would be affected by this attestation.
"""
# Get the attestation data root
root = attestation.data.beacon_block_root
# Invalidate this block and all descendants
invalidate_block_and_descendants(store, root)
|
| def invalidate_block_and_descendants(store: Store, root: Root) -> None:
"""
Invalidate the score for a block and all its descendants.
"""
if root in store.block_scores:
del store.block_scores[root]
# Recursively invalidate children
for child in get_children(store, root):
invalidate_block_and_descendants(store, child)
|
| def get_children(store: Store, root: Root) -> List[Root]:
"""
Get all child blocks of the block with the given root.
"""
return [
block_root for block_root in store.blocks
if store.blocks[block_root].parent_root == root
]
|
Fork choice caching
| def is_score_recent(store: Store, root: Root) -> bool:
"""
Determine if a cached score is still valid.
"""
# Check if we've received new attestations for this root since scoring
# This is a simplified version - a real implementation would track attestation times
return root in store.block_scores
|
| def calculate_and_cache_block_score(store: Store, root: Root) -> float:
"""
Calculate and cache the score for a block.
"""
score = compute_block_score(store, root)
store.block_scores[root] = score
return score
|
| def update_score_timestamp(store: Store, root: Root) -> None:
"""
Record when a block's score was last calculated.
"""
# In a real implementation, this would store the current time
# For this specification, we'll assume it's handled implicitly
pass
|
Attestation processing optimizations
| def batch_process_attestations(store: Store, attestations: List[Attestation]) -> None:
"""
Process multiple attestations efficiently in a batch.
"""
# Group attestations by block root
attestations_by_root = {}
for attestation in attestations:
root = attestation.data.beacon_block_root
if root not in attestations_by_root:
attestations_by_root[root] = []
attestations_by_root[root].append(attestation)
# Process each group efficiently
for root, atts in attestations_by_root.items():
batch_update_votes_for_root(store, root, atts)
# Invalidate affected cached scores once per group
for root in attestations_by_root:
invalidate_block_and_descendants(store, root)
|
| def batch_update_votes_for_root(store: Store, root: Root, attestations: List[Attestation]) -> None:
"""
Update votes for a specific root in bulk.
"""
# Count all validators voting for this root
validators_for_root = set()
for attestation in attestations:
validators_for_root.update(get_attesting_indices(attestation))
# Update latest messages in bulk
target_epoch = attestations[0].data.target.epoch # All attestations in batch have same target
for validator_index in validators_for_root:
store.validator_latest_messages[validator_index] = LatestMessage(
epoch=target_epoch,
root=root
)
# Update recent activity
update_validator_recent_activity(store, validator_index)
|
| def compute_initial_attestation_score(store: Store, root: Root) -> float:
"""
Compute an initial attestation score for a block before full attestations are collected.
This uses early attestations to predict the likely final weight.
"""
# Get block and state
block = store.blocks[root]
state = store.block_states[root]
# Check how many validators have already attested
attesting_validators = set()
for validator_index, latest_message in store.validator_latest_messages.items():
if get_ancestor(store, latest_message.root, block.slot) == root:
attesting_validators.add(validator_index)
# Get total active balance
active_indices = get_active_validator_indices(state, get_current_epoch(state))
total_balance = sum(state.validators[i].effective_balance for i in active_indices)
# Get attesting balance
attesting_balance = sum(state.validators[i].effective_balance for i in attesting_validators)
# Calculate time since block creation
time_since_block = store.time - (store.genesis_time + block.slot * SECONDS_PER_SLOT)
# Early attestations are weighted more heavily to project final score
time_factor = max(1.0, SECONDS_PER_SLOT / max(1, time_since_block))
projected_balance = min(total_balance, attesting_balance * time_factor)
return projected_balance
|
Security considerations
Defense against attacks
The Weber fork choice incorporates several protections against common attack vectors:
-
Reputation Weighted Votes: By weighting votes based on validator reputation, Weber mitigates the impact of misbehaving validators. Attackers would need to maintain high reputation scores while attacking, which creates contradicting incentives.
-
Quick Finality Thresholds: The high reputation requirement for quick finality votes prevents new or misbehaving validators from influencing optimistic consensus.
-
Fallback Mechanisms: The system automatically falls back to standard fork choice rules if network health metrics indicate potential attacks or issues.
-
Economic Penalties: Through the reputation system, attackers face both direct penalties and reduced influence in the consensus process.
Incentive compatibility
The reputation-weighted fork choice creates aligned incentives for validators:
- Validators are incentivized to produce attestations promptly to maintain high reputation.
- Validators with high reputation gain more influence in the consensus process.
- The quick finality mechanism rewards validators for consistent, reliable performance.
This incentive structure rewards good behavior without significantly penalizing occasional honest mistakes or network issues.
Validator balancing
The Weber fork choice algorithm includes mechanisms to prevent centralization of influence:
- The reputation weighting is bounded to prevent any single validator from gaining excessive influence.
- Periodic reputation decay ensures that validators must continuously perform well.
- The validator rotation mechanism prevents long-term domination by specific validators.
Weber's fork choice implementation maintains performance statistics to monitor health and efficiency:
- Convergence Time: How quickly nodes agree on a head block
- Reorg Frequency: How often the canonical chain changes
- Vote Participation: Percentage of active validators participating in consensus
- Optimistic Hit Rate: How often optimistic decisions match final consensus
- Score Calculation Efficiency: CPU time spent on score calculations
These metrics help implement