Phase 0 -- Fast Confirmation
Introduction
This document specifies a fast block confirmation rule (a.k.a. FCR) for the
Ethereum protocol.
The research paper for this rule can be found
here.
A shorter explainer is available
here.
This rule makes the following network synchrony assumption: starting from the
current slot, attestations created by honest validators in any slot are received
by the end of that slot. Consequently, this rule provides confirmations to users
who believe in the above assumption. If this assumption is broken, confirmed
blocks can be reorged without any adversarial behavior and without slashing.
Fast Confirmation Rule
Constants
| Name |
Value |
Description |
COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR |
uint64(5) |
Per mille value to add to the estimation of the committee weight across a range of slots not covering a full epoch in order to ensure the safety of the confirmation rule with high probability. See here for an explanation about the value chosen. |
Configuration
| Name |
Value |
Max. Value |
Description |
CONFIRMATION_BYZANTINE_THRESHOLD |
uint64(25) |
uint64(25) |
Assumed maximum percentage of Byzantine validators among the validator set |
Helpers
FastConfirmationStore
The FastConfirmationStore is responsible for tracking information required for
the fast confirmation rule. The fields being tracked are described below:
store: read-only instance of the fork choice Store, added for convenience.
confirmed_root: root of the most recent confirmed block.
previous_epoch_observed_justified_checkpoint: a justified checkpoint that
has been observed by all honest nodes at the beginning of the previous epoch
assuming synchrony.
current_epoch_observed_justified_checkpoint: a justified checkpoint that has
been observed by all honest nodes at the beginning of the current epoch
assuming synchrony.
previous_epoch_greatest_unrealized_checkpoint: a greatest unrealized
justified checkpoint at the start of the last slot of the previous epoch
according to a local view.
previous_slot_head: the head at the start of the previous slot.
current_slot_head: the head at the start of the current slot.
| @dataclass
class FastConfirmationStore(object):
store: Store
confirmed_root: Root
previous_epoch_observed_justified_checkpoint: Checkpoint
current_epoch_observed_justified_checkpoint: Checkpoint
previous_epoch_greatest_unrealized_checkpoint: Checkpoint
previous_slot_head: Root
current_slot_head: Root
|
get_fast_confirmation_store
Initialization of the fast confirmation store should happen at the same time as
initialization of the fork choice store and use the same trusted checkpoint.
Note: This function conservatively uses store.finalized_checkpoint for all
fast confirmation variables. Confirmed block will be advanced at any future
epoch boundary when the starting conditions are met, before that the finalized
block will be returned as the confirmed one.
| def get_fast_confirmation_store(store: Store) -> FastConfirmationStore:
return FastConfirmationStore(
store=store,
confirmed_root=store.finalized_checkpoint.root,
previous_epoch_observed_justified_checkpoint=store.finalized_checkpoint,
current_epoch_observed_justified_checkpoint=store.finalized_checkpoint,
previous_epoch_greatest_unrealized_checkpoint=store.finalized_checkpoint,
previous_slot_head=store.finalized_checkpoint.root,
current_slot_head=store.finalized_checkpoint.root,
)
|
Misc helper functions
get_block_slot
| def get_block_slot(store: Store, block_root: Root) -> Slot:
"""
Return a slot of the block.
"""
return store.blocks[block_root].slot
|
get_block_epoch
| def get_block_epoch(store: Store, block_root: Root) -> Epoch:
"""
Return an epoch of the block.
"""
return compute_epoch_at_slot(store.blocks[block_root].slot)
|
get_checkpoint_for_block
| def get_checkpoint_for_block(store: Store, block_root: Root, epoch: Epoch) -> Checkpoint:
"""
Return a checkpoint in the chain of the block at the ``epoch``.
"""
return Checkpoint(epoch=epoch, root=get_checkpoint_block(store, block_root, epoch))
|
get_current_target
| def get_current_target(store: Store) -> Checkpoint:
"""
Return current epoch target.
"""
head = get_head(store)
current_epoch = get_current_store_epoch(store)
return get_checkpoint_for_block(store, head, current_epoch)
|
is_start_slot_at_epoch
| def is_start_slot_at_epoch(slot: Slot) -> bool:
"""
Return ``True`` if ``slot`` is the start slot of an epoch.
"""
return compute_slots_since_epoch_start(slot) == 0
|
is_ancestor
| def is_ancestor(store: Store, block_root: Root, ancestor_root: Root) -> bool:
"""
Return ``True`` if ``ancestor_root`` is an ancestor of ``block_root``.
"""
return get_ancestor(store, block_root, store.blocks[ancestor_root].slot) == ancestor_root
|
get_ancestor_roots
| def get_ancestor_roots(store: Store, block_root: Root, terminal_root: Root) -> Sequence[Root]:
"""
Return a list of ancestors of ``block_root`` inclusive until ``terminal_root`` exclusive.
"""
root = block_root
ancestor_roots: list[Root] = []
while store.blocks[root].slot > store.blocks[terminal_root].slot:
ancestor_roots.insert(0, root)
root = store.blocks[root].parent_root
# Return when terminal_root is reached
if root == terminal_root:
return ancestor_roots
# Return empty list if terminal_root is not in the chain of block_root
return []
|
State helpers
This section encapsulates reads from different beacon states used by the Fast
Confirmation Rule.
Implementations MAY override the logic of each of these functions but the
semantics MUST be preserved.
get_slot_committee
Note: This function returns the committee for a specific slot. It MUST support
committees of epochs starting from current_epoch - 2.
| def get_slot_committee(store: Store, slot: Slot) -> Set[ValidatorIndex]:
"""
Return participants of all committees in ``slot``.
"""
head = get_head(store)
shuffling_source = store.block_states[head]
committees_count = get_committee_count_per_slot(shuffling_source, compute_epoch_at_slot(slot))
participants: Set[ValidatorIndex] = set()
for i in range(committees_count):
participants.update(get_beacon_committee(shuffling_source, slot, CommitteeIndex(i)))
return participants
|
get_pulled_up_head_state
| def get_pulled_up_head_state(store: Store) -> BeaconState:
"""
Return the state of the head pulled up to the current epoch if needed.
"""
head = get_head(store)
head_state = store.block_states[head]
if get_current_epoch(head_state) < get_current_store_epoch(store):
pulled_up_state = copy(head_state)
process_slots(pulled_up_state, compute_start_slot_at_epoch(get_current_store_epoch(store)))
return pulled_up_state
else:
return head_state
|
get_previous_balance_source
Note: Reconfirmation is the only place where previous balance source is used.
Implementations MAY switch to the current epoch balance source after they run
reconfirmation and before the logic to confirm new blocks is called. In this
case implementations will need to keep only a single balance source around.
| def get_previous_balance_source(fcr_store: FastConfirmationStore) -> BeaconState:
store = fcr_store.store
return store.checkpoint_states[fcr_store.previous_epoch_observed_justified_checkpoint]
|
get_current_balance_source
| def get_current_balance_source(fcr_store: FastConfirmationStore) -> BeaconState:
store = fcr_store.store
return store.checkpoint_states[fcr_store.current_epoch_observed_justified_checkpoint]
|
LMD-GHOST helpers
get_block_support_between_slots
Notes:
Validator votes from slots before start_slot and after end_slot might not be
distinguished from votes submitted by that same validator in
[start_slot, end_slot] interval. Due to committee shuffling near epoch
boundary the following cases are possible:
- Validator assigned to
start_slot - 1 and end_slot votes for block_root
in start_slot - 1 but does not vote in end_slot.
- Validator assigned to
start_slot and end_slot + 1 misses a vote in
start_slot, but votes for block_root in end_slot + 1.
In both cases the support would count a vote outside of the
[start_slot, end_slot] range. This inaccuracy is acceptable as it does not
affect safety.
Due to the algorithm logic, maximum distance between balance_source and
start_slot or end_slot is two epochs, which is less than
MAX_SEED_LOOKAHEAD. Therefore, participants of the committees from that span
of slots are consistent with the balance_source validator set.
| def get_block_support_between_slots(
store: Store,
balance_source: BeaconState,
block_root: Root,
start_slot: Slot,
end_slot: Slot,
) -> Gwei:
"""
Return support of the block by validators assigned to slots
between ``start_slot`` and ``end_slot`` (inclusive of both).
"""
participants: Set[ValidatorIndex] = set()
for slot in range(start_slot, end_slot + 1):
participants.update(get_slot_committee(store, Slot(slot)))
# Keep validators that were active at the balance_source epoch to be consistent
# with get_total_active_balance() computation, also filter out slashed validators
unslashed_and_active_indices = [
i
for i in participants
if (
not balance_source.validators[i].slashed
and is_active_validator(balance_source.validators[i], get_current_epoch(balance_source))
)
]
return Gwei(
sum(
balance_source.validators[i].effective_balance
for i in unslashed_and_active_indices
# Check that validator has voted in the support of the block
# and has not been slashed
if (
i in store.latest_messages
and store.latest_messages[i].root == block_root
and i not in store.equivocating_indices
)
)
)
|
is_full_validator_set_covered
| def is_full_validator_set_covered(start_slot: Slot, end_slot: Slot) -> bool:
"""
Return ``True`` if the range between ``start_slot`` and ``end_slot`` (inclusive of both) includes an entire epoch.
"""
start_full_epoch = compute_epoch_at_slot(start_slot + (SLOTS_PER_EPOCH - 1))
end_full_epoch = compute_epoch_at_slot(Slot(end_slot + 1))
return start_full_epoch < end_full_epoch
|
adjust_committee_weight_estimate_to_ensure_safety
Note: This function adjusts the estimate of the weight of a committee for a
sequence of slots spanning an epoch boundary that does not cover any full epoch
to ensure the safety of FCR with high probability. The sequence may be longer
than SLOTS_PER_EPOCH. See
https://gist.github.com/saltiniroberto/9ee53d29c33878d79417abb2b4468c20 for an
explanation of why this is required.
| def adjust_committee_weight_estimate_to_ensure_safety(estimate: Gwei) -> Gwei:
"""
Return adjusted ``estimate`` of the weight of a committee for a sequence of slots
spanning an epoch boundary that does not cover any full epoch.
"""
ceil = (estimate + 999) // 1000
return Gwei(ceil * (1000 + COMMITTEE_WEIGHT_ESTIMATION_ADJUSTMENT_FACTOR))
|
estimate_committee_weight_between_slots
| def estimate_committee_weight_between_slots(
total_active_balance: Gwei, start_slot: Slot, end_slot: Slot
) -> Gwei:
"""
Return estimate of the total weight of committees
between ``start_slot`` and ``end_slot`` (inclusive of both).
"""
# Sanity check
if start_slot > end_slot:
return Gwei(0)
# If an entire epoch is covered by the range, return the total active balance
if is_full_validator_set_covered(start_slot, end_slot):
return total_active_balance
start_epoch = compute_epoch_at_slot(start_slot)
end_epoch = compute_epoch_at_slot(end_slot)
committee_weight = total_active_balance // SLOTS_PER_EPOCH
if start_epoch == end_epoch:
return committee_weight * (end_slot - start_slot + 1)
else:
# First, calculate the number of committees in the end epoch
num_slots_in_end_epoch = compute_slots_since_epoch_start(end_slot) + 1
# Next, calculate the number of slots remaining in the end epoch
remaining_slots_in_end_epoch = SLOTS_PER_EPOCH - num_slots_in_end_epoch
# Then, calculate the number of slots in the start epoch
num_slots_in_start_epoch = SLOTS_PER_EPOCH - compute_slots_since_epoch_start(start_slot)
start_epoch_weight = committee_weight * num_slots_in_start_epoch
end_epoch_weight = committee_weight * num_slots_in_end_epoch
# A range that spans an epoch boundary, but does not span any full epoch
# needs pro-rata calculation, see https://gist.github.com/saltiniroberto/9ee53d29c33878d79417abb2b4468c20
# start_epoch_weight_pro_rated = start_epoch_weight * (1 - num_slots_in_end_epoch / SLOTS_PER_EPOCH)
start_epoch_weight_pro_rated = (
start_epoch_weight // SLOTS_PER_EPOCH * remaining_slots_in_end_epoch
)
return adjust_committee_weight_estimate_to_ensure_safety(
Gwei(start_epoch_weight_pro_rated + end_epoch_weight)
)
|
get_equivocation_score
Notes:
For simplicity, this function does not seek balance_source for slashed
validators as those validators are very likely already in
store.equivocating_indices.
Due to the algorithm logic, maximum distance between balance_source and
start_slot or end_slot is two epochs, which is less than
MAX_SEED_LOOKAHEAD. Therefore, participants of the committees from that span
of slots are consistent with the balance_source validator set.
| def get_equivocation_score(
store: Store,
balance_source: BeaconState,
start_slot: Slot,
end_slot: Slot,
) -> Gwei:
"""
Return total weight of equivocating participants of all committees
in the slots between ``start_slot`` and ``end_slot`` (inclusive of both).
"""
committee_indices: Set[ValidatorIndex] = set()
for slot in range(start_slot, end_slot + 1):
committee_indices.update(get_slot_committee(store, Slot(slot)))
# Keep equivocating validators that were active at the balance_source epoch to be consistent
# with get_total_active_balance() computation
active_equivocating_indices = [
i
for i in committee_indices.intersection(store.equivocating_indices)
if is_active_validator(balance_source.validators[i], get_current_epoch(balance_source))
]
return Gwei(
sum(balance_source.validators[i].effective_balance for i in active_equivocating_indices)
)
|
compute_adversarial_weight
Note: This function computes maximum possible weight that can be adversarial
in the committees of the span of slots assuming
CONFIRMATION_BYZANTINE_THRESHOLD and discounting already equivocated
validators.
| def compute_adversarial_weight(
store: Store,
balance_source: BeaconState,
start_slot: Slot,
end_slot: Slot,
) -> Gwei:
"""
Return maximum possible adversarial weight in the committees of the slots
between ``start_slot`` and ``end_slot`` (inclusive of both).
"""
total_active_balance = get_total_active_balance(balance_source)
maximum_weight = estimate_committee_weight_between_slots(
total_active_balance, start_slot, end_slot
)
max_adversarial_weight = maximum_weight // 100 * CONFIRMATION_BYZANTINE_THRESHOLD
# Discount total weight of equivocating validators
equivocation_score = get_equivocation_score(store, balance_source, start_slot, end_slot)
if max_adversarial_weight > equivocation_score:
return Gwei(max_adversarial_weight - equivocation_score)
else:
return Gwei(0)
|
get_adversarial_weight
| def get_adversarial_weight(store: Store, balance_source: BeaconState, block_root: Root) -> Gwei:
"""
Return maximum adversarial weight that can support the block.
"""
current_slot = get_current_slot(store)
block = store.blocks[block_root]
if get_block_epoch(store, block_root) > get_block_epoch(store, block.parent_root):
# Use the first epoch slot as the start slot when crossing epoch boundary
start_slot = compute_start_slot_at_epoch(get_block_epoch(store, block_root))
return compute_adversarial_weight(store, balance_source, start_slot, Slot(current_slot - 1))
else:
return compute_adversarial_weight(store, balance_source, block.slot, Slot(current_slot - 1))
|
compute_empty_slot_support_discount
Note: This function MAY compute parent's block support and adversarial weight
for empty slots belonging to current_epoch - 2. It can only happen in
reconfirmation, in all other cases the earliest possible epoch is
current_epoch - 1.
| def compute_empty_slot_support_discount(
store: Store, balance_source: BeaconState, block_root: Root
) -> Gwei:
"""
Return weight that can be discounted during the safety threshold computation
if there are empty slots preceding the block.
"""
block = store.blocks[block_root]
parent_block = store.blocks[block.parent_root]
# No empty slot
if parent_block.slot + 1 == block.slot:
return Gwei(0)
# Discount votes supporting the parent block if they are from the committees of empty slots
parent_support_in_empty_slots = get_block_support_between_slots(
store,
balance_source,
block.parent_root,
Slot(parent_block.slot + 1),
Slot(block.slot - 1),
)
# Adversarial weight is not discounted
adversarial_weight = compute_adversarial_weight(
store, balance_source, Slot(parent_block.slot + 1), Slot(block.slot - 1)
)
if parent_support_in_empty_slots > adversarial_weight:
return parent_support_in_empty_slots - adversarial_weight
else:
return Gwei(0)
|
get_support_discount
| def get_support_discount(store: Store, balance_source: BeaconState, block_root: Root) -> Gwei:
"""
Return weight that can be discounted during the safety threshold computation for the block.
"""
return compute_empty_slot_support_discount(store, balance_source, block_root)
|
compute_safety_threshold
| def compute_safety_threshold(store: Store, block_root: Root, balance_source: BeaconState) -> Gwei:
"""
Compute the LMD-GHOST safety threshold for ``block_root``.
"""
current_slot = get_current_slot(store)
block = store.blocks[block_root]
parent_block = store.blocks[block.parent_root]
total_active_balance = get_total_active_balance(balance_source)
proposer_score = compute_proposer_score(balance_source)
maximum_support = estimate_committee_weight_between_slots(
total_active_balance, Slot(parent_block.slot + 1), Slot(current_slot - 1)
)
support_discount = get_support_discount(store, balance_source, block_root)
adversarial_weight = get_adversarial_weight(store, balance_source, block_root)
# Return (maximum_support + proposer_score - support_discount) // 2 + adversarial_weight
# with an underflow guard
if support_discount < maximum_support + proposer_score + 2 * adversarial_weight:
return (maximum_support + proposer_score + 2 * adversarial_weight - support_discount) // 2
else:
return Gwei(0)
|
is_one_confirmed
Notes:
This function checks if a single block is LMD-GHOST safe by computing LMD-GHOST
safety indicator and comparing its value to the safety threshold.
At a high level the computation checks whether the actual score of the block
outweighs potential score of any block conflicting with it considering total
weight of the committees and maximal adversarial weight.
If this check passes the block is deemed LMD-GHOST safe, but it is not enough to
say that the block will remain canonical. To ensure the latter, each ancestor of
the block would also have to pass this check.
More details on this check can be found in the
paper.
This function MUST return False if block_root status is not VALID
according to the Optimistic sync specification.
| def is_one_confirmed(store: Store, balance_source: BeaconState, block_root: Root) -> bool:
"""
Return ``True`` if and only if the block is LMD-GHOST safe.
"""
support = get_attestation_score(store, block_root, balance_source)
safety_threshold = compute_safety_threshold(store, block_root, balance_source)
return support > safety_threshold
|
is_confirmed_chain_safe
Notes:
This function should be called at the start of each epoch to ensure that the
confirmed chain starting from
fcr_store.current_epoch_observed_justified_checkpoint.root remains LMD-GHOST
safe.
This check relaxes synchrony assumption by allowing GST to start from the
beginning of the previous slot. If such check was not run, GST start would have
to be assumed from the time of the first run of the algorithm which could have
been many epochs ago.
| def is_confirmed_chain_safe(fcr_store: FastConfirmationStore, confirmed_root: Root) -> bool:
"""
Return ``True`` if and only if all blocks of the confirmed chain
starting from current_epoch_observed_justified_checkpoint are LMD-GHOST safe.
"""
store = fcr_store.store
# Check if the confirmed_root is descendant of current_epoch_observed_justified_checkpoint
if not is_ancestor(
store, confirmed_root, fcr_store.current_epoch_observed_justified_checkpoint.root
):
return False
current_epoch = get_current_store_epoch(store)
if fcr_store.current_epoch_observed_justified_checkpoint.epoch + 1 >= current_epoch:
# Exclude the justified checkpoint block if it is from the previous epoch
# as then this block will always be canonical in this case.
start_root_exclusive = fcr_store.current_epoch_observed_justified_checkpoint.root
else:
# Limit reconfirmation to the first block of the previous epoch
# as if it is successful, reconfirmation of the ancestors is implied.
ancestor_at_previous_epoch_start = get_ancestor(
store, confirmed_root, compute_start_slot_at_epoch(Epoch(current_epoch - 1))
)
if get_block_epoch(store, ancestor_at_previous_epoch_start) + 1 == current_epoch:
# The parent of the first block of the previous epoch
start_root_exclusive = store.blocks[ancestor_at_previous_epoch_start].parent_root
else:
# The last block of the epoch before the previous one
start_root_exclusive = ancestor_at_previous_epoch_start
# Run is_one_confirmed for each block in the confirmed chain with the previous epoch balance source
chain_roots = get_ancestor_roots(store, confirmed_root, start_root_exclusive)
return all(
is_one_confirmed(store, get_previous_balance_source(fcr_store), root)
for root in chain_roots
)
|
FFG helpers
get_current_target_score
Note: This function uses LMD-GHOST votes to estimate the FFG support of the
current epoch target. Due to the way the computation happens, it MUST be used no
later than the end of the current epoch.
| def get_current_target_score(store: Store) -> Gwei:
"""
Return the estimate of FFG support of the current epoch target by using LMD-GHOST votes.
"""
target = get_current_target(store)
state = get_pulled_up_head_state(store)
unslashed_and_active_indices = [
i
for i in get_active_validator_indices(state, get_current_epoch(state))
if not state.validators[i].slashed
]
return Gwei(
sum(
state.validators[i].effective_balance
for i in unslashed_and_active_indices
if (
i in store.latest_messages
and i not in store.equivocating_indices
and target
== get_checkpoint_for_block(
store,
store.latest_messages[i].root,
get_latest_message_epoch(store.latest_messages[i]),
)
)
)
)
|
compute_honest_ffg_support_for_current_target
Note: This function computes honest FFG support of the current epoch target by
assuming CONFIRMATION_BYZANTINE_THRESHOLD and network synchrony, and taking
into account votes supporting the target that have been received thus far.
| def compute_honest_ffg_support_for_current_target(store: Store) -> Gwei:
"""
Compute honest FFG support of the current epoch target.
"""
current_slot = get_current_slot(store)
current_epoch = compute_epoch_at_slot(current_slot)
balance_source = get_pulled_up_head_state(store)
total_active_balance = get_total_active_balance(balance_source)
# Compute FFG support for the target
ffg_support_for_checkpoint = get_current_target_score(store)
# Compute the total FFG weight up to, but excluding, the current slot
ffg_weight_till_now = estimate_committee_weight_between_slots(
total_active_balance, compute_start_slot_at_epoch(current_epoch), Slot(current_slot - 1)
)
# Compute remaining honest FFG weight
remaining_ffg_weight = total_active_balance - ffg_weight_till_now
remaining_honest_ffg_weight = Gwei(
remaining_ffg_weight // 100 * (100 - CONFIRMATION_BYZANTINE_THRESHOLD)
)
# Compute potential adversarial weight
adversarial_weight = compute_adversarial_weight(
store, balance_source, compute_start_slot_at_epoch(current_epoch), Slot(current_slot - 1)
)
# Compute min honest FFG support
min_honest_ffg_support = ffg_support_for_checkpoint - min(
adversarial_weight, ffg_support_for_checkpoint
)
return Gwei(min_honest_ffg_support + remaining_honest_ffg_weight)
|
will_no_conflicting_checkpoint_be_justified
Note: This function assumes that all honest validators will be voting in
support of the current epoch target starting from the current moment in time.
| def will_no_conflicting_checkpoint_be_justified(store: Store) -> bool:
"""
Return ``True`` if and only if no checkpoint conflicting with the current target can ever be justified.
"""
# If the target is unrealized justified then no conflicting checkpoint can be justified
if get_current_target(store) == store.unrealized_justified_checkpoint:
return True
state = get_pulled_up_head_state(store)
total_active_balance = get_total_active_balance(state)
honest_ffg_support = compute_honest_ffg_support_for_current_target(store)
return 3 * honest_ffg_support > 1 * total_active_balance
|
will_current_target_be_justified
Note: This function assumes that all honest validators will be voting in
support of the current epoch target starting from the current moment in time.
| def will_current_target_be_justified(store: Store) -> bool:
"""
Return ``True`` if and only if the current target will eventually be justified.
"""
state = get_pulled_up_head_state(store)
total_active_balance = get_total_active_balance(state)
honest_ffg_support = compute_honest_ffg_support_for_current_target(store)
return 3 * honest_ffg_support >= 2 * total_active_balance
|
update_fast_confirmation_variables
Note: This function updates variables used by the fast confirmation rule.
| def update_fast_confirmation_variables(fcr_store: FastConfirmationStore) -> None:
# Update prev and curr slot head
store = fcr_store.store
fcr_store.previous_slot_head = fcr_store.current_slot_head
fcr_store.current_slot_head = get_head(store)
# Update greatest unrealized justified checkpoint at the last slot of an epoch
if is_start_slot_at_epoch(Slot(get_current_slot(store) + 1)):
fcr_store.previous_epoch_greatest_unrealized_checkpoint = (
store.unrealized_justified_checkpoint
)
# Update observed justified checkpoints at the start of an epoch
if is_start_slot_at_epoch(get_current_slot(store)):
fcr_store.previous_epoch_observed_justified_checkpoint = (
fcr_store.current_epoch_observed_justified_checkpoint
)
fcr_store.current_epoch_observed_justified_checkpoint = (
fcr_store.previous_epoch_greatest_unrealized_checkpoint
)
|
find_latest_confirmed_descendant
Notes:
This function examines canonical chain blocks starting from
latest_confirmed_root and returns the most recent block that satisfies FCR
conditions:
- Each block in its chain is LMD-GHOST safe, i.e. will be the winner of the
LMD-GHOST fork choice rule starting from the current moment in time.
- The block will not be filtered out during the current and the next epochs.
Assuming synchrony and CONFIRMATION_BYZANTINE_THRESHOLD value, the above
criteria ensures that the block returned by this function will remain canonical
in the view of all honest validators starting from the current moment in time.
This function works correctly only if the latest_confirmed_root belongs to the
canonical chain and is either from the previous or from the current epoch.
| def find_latest_confirmed_descendant(
fcr_store: FastConfirmationStore, latest_confirmed_root: Root
) -> Root:
"""
Return the most recent confirmed block in the suffix of the canonical chain
starting from ``latest_confirmed_root``.
"""
store = fcr_store.store
head = get_head(store)
current_epoch = get_current_store_epoch(store)
confirmed_root = latest_confirmed_root
if (
get_block_epoch(store, confirmed_root) + 1 == current_epoch
and get_voting_source(store, fcr_store.previous_slot_head).epoch + 2 >= current_epoch
and (
is_start_slot_at_epoch(get_current_slot(store))
or (
will_no_conflicting_checkpoint_be_justified(store)
and (
store.unrealized_justifications[fcr_store.previous_slot_head].epoch + 1
>= current_epoch
or store.unrealized_justifications[head].epoch + 1 >= current_epoch
)
)
)
):
# Get suffix of the canonical chain
canonical_roots = get_ancestor_roots(store, head, confirmed_root)
# Starting with the child of the latest_confirmed_root
# move towards the head in attempt to advance the confirmed block
# and stop when the first unconfirmed descendant is encountered
for block_root in canonical_roots:
block_epoch = get_block_epoch(store, block_root)
# If the current epoch is reached, exit the loop
# as this code is meant to confirm blocks from the previous epoch
if block_epoch == current_epoch:
break
# The algorithm can only rely on the previous head
# if it is a descendant of the block that is attempted to be confirmed
if not is_ancestor(store, fcr_store.previous_slot_head, block_root):
break
if not is_one_confirmed(store, get_current_balance_source(fcr_store), block_root):
break
confirmed_root = block_root
if (
is_start_slot_at_epoch(get_current_slot(store))
or store.unrealized_justifications[head].epoch + 1 >= current_epoch
):
# Get suffix of the canonical chain
canonical_roots = get_ancestor_roots(store, head, confirmed_root)
tentative_confirmed_root = confirmed_root
for block_root in canonical_roots:
block_epoch = get_block_epoch(store, block_root)
tentative_confirmed_epoch = get_block_epoch(store, tentative_confirmed_root)
# The following condition can only be true the first time
# the algorithm advances to a block from the current epoch
if block_epoch > tentative_confirmed_epoch:
# To confirm blocks from the current epoch ensure that
# current epoch target will be justified
if not will_current_target_be_justified(store):
break
if not is_one_confirmed(store, get_current_balance_source(fcr_store), block_root):
break
tentative_confirmed_root = block_root
# The tentative_confirmed_root can only be confirmed
# if it is for sure not going to be reorged out in either the current or next epoch.
if get_block_epoch(store, tentative_confirmed_root) == current_epoch or (
get_voting_source(store, tentative_confirmed_root).epoch + 2 >= current_epoch
and (
is_start_slot_at_epoch(get_current_slot(store))
or will_no_conflicting_checkpoint_be_justified(store)
)
):
confirmed_root = tentative_confirmed_root
return confirmed_root
|
get_latest_confirmed
Notes:
This function executes the FCR algorithm which takes the following sequence of
actions:
- Check if the
fcr_store.confirmed_root belongs to the canonical chain and is
not older than the previous epoch.
- Check if the confirmed chain starting from the
fcr_store.current_epoch_observed_justified_checkpoint can be re-confirmed
at the start of the current epoch which resets GST to the start of the
current epoch.
- If any of the above checks fail, set
fcr_store.confirmed_root to the
store.finalized_checkpoint.root. Either of the above conditions signify
that FCR assumptions (at least synchrony) are broken and the confirmed block
might not be safe.
- Restart the confirmation chain by setting
fcr_store.confirmed_root to
fcr_store.current_epoch_observed_justified_checkpoint.root if the restart
conditions are met. Under synchrony, such a checkpoint is for sure now the
greatest justified checkpoint in the view of any honest validator and,
therefore, any honest validator will keep voting for it for the entire epoch.
- Attempt to advance the
fcr_store.confirmed_root by calling
find_latest_confirmed_descendant.
| def get_latest_confirmed(fcr_store: FastConfirmationStore) -> Root:
"""
Return the most recent confirmed block by executing the FCR algorithm.
"""
store = fcr_store.store
confirmed_root = fcr_store.confirmed_root
current_epoch = get_current_store_epoch(store)
# Revert to finalized block if either of the following is true:
# 1) the latest confirmed block's epoch is older than the previous epoch,
# 2) the latest confirmed block does not belong to the canonical chain,
# 3) the confirmed chain starting from the current epoch observed justified checkpoint
# cannot be re-confirmed at the start of the current epoch.
head = get_head(store)
if (
get_block_epoch(store, confirmed_root) + 1 < current_epoch
or not is_ancestor(store, head, confirmed_root)
or (
is_start_slot_at_epoch(get_current_slot(store))
and not is_confirmed_chain_safe(fcr_store, confirmed_root)
)
):
confirmed_root = store.finalized_checkpoint.root
# Restart the confirmation chain if each of the following conditions are true:
# 1) it is the start of the current epoch,
# 2) epoch of fcr_store.current_epoch_observed_justified_checkpoint.root equals to the previous epoch,
# 3) fcr_store.current_epoch_observed_justified_checkpoint equals to unrealized justification of the head,
# 4) confirmed block is older than the block of fcr_store.current_epoch_observed_justified_checkpoint.
is_epoch_start = is_start_slot_at_epoch(get_current_slot(store))
observed_justified_block_slot = get_block_slot(
store, fcr_store.current_epoch_observed_justified_checkpoint.root
)
is_observed_justified_block_epoch_ok = (
compute_epoch_at_slot(observed_justified_block_slot) + 1 == current_epoch
)
is_head_unrealized_justified_ok = (
fcr_store.current_epoch_observed_justified_checkpoint
== store.unrealized_justifications[head]
)
is_confirmed_block_stale = get_block_slot(store, confirmed_root) < observed_justified_block_slot
if (
is_epoch_start
and is_observed_justified_block_epoch_ok
and is_head_unrealized_justified_ok
and is_confirmed_block_stale
):
confirmed_root = fcr_store.current_epoch_observed_justified_checkpoint.root
# Attempt to further advance the latest confirmed block
if get_block_epoch(store, confirmed_root) + 1 >= current_epoch:
return find_latest_confirmed_descendant(fcr_store, confirmed_root)
else:
return confirmed_root
|
Handlers
on_fast_confirmation
Notes:
This handler calls update_fast_confirmation_variables and then
get_latest_confirmed to update fcr_store.confirmed_root with the response of
that call.
Implementations MUST strictly follow the call sequence:
update_fast_confirmation_variables
get_latest_confirmed
Implementations MUST call update_fast_confirmation_variables in the first part
of a slot after attestations from past slots have been applied and before
get_attestation_due_ms(epoch) milliseconds has transpired since the start of
the slot.
Implementations MAY call update_fast_confirmation_variables after a valid
block from the expected block proposer for the assigned slot has been received
and processed if this happens before get_attestation_due_ms(epoch)
milliseconds has transpired since the start of the slot. Regardless of the
time of the call, update_fast_confirmation_variables MUST be called only once
per slot.
Implementations MAY call get_latest_confirmed at any point in time throughout
a slot.
| def on_fast_confirmation(fcr_store: FastConfirmationStore) -> None:
update_fast_confirmation_variables(fcr_store)
fcr_store.confirmed_root = get_latest_confirmed(fcr_store)
|