-
-
Published
Linked with GitHub
# FC finality/justificaion atomicity problem
Paul pointed out that `store` can at times update finality non-atomically with justification. This is because of the (complex) logic in [Code-0](#Code-0)
In such a case, this can short-circuit `get_filtered_block_tree` because there are no leaf states that know about the finalized/justified combo in the state. That is `correct_justified and correct_finalized` in [Code-A](#Code-A) never returns true for any leaf block, thus `get_filtered_block_tree` will return an empty `blocks` which causes `get_head` to simply return whatever is `store.justified_checkpoint.root` (see [Code-B](#Code-B)).
### More on the problem
If we look again at [Code-0](#Code-0), we see that when newly finalized there are two times we might want to atomically update the justification:
1. When the newly justified is strictly better (greater than) what is in the store. Thus the `state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch` condition.
2. When `store.justified_checkpoint` is *higher* than what is in state (thus bypassing (1)), but it is on a chain that doesn't know about the finalized root.
It turns out that (2) is misguided. It can lead to the `store` having a combination of finalized and justified checkpoints that *does not exist* in any leaf state. Thus `get-filtered_block_tree` will return an empty dict, allowing `get_head` to return non-sensical values.
### The solution
You could imagine putting an even stricter clause for (2) -- That is, only keep the previous higher justified if there exists a leaf in the block-tree that has a state that has both the newly finalized and the previous higher justified (from the `store`) in that state (as `state.finalized_epoch` and `state.current_justified_epoch`).
Although such a condition would be *correct*, it would never be hit because if such a leaf block with such a state existed, then `store.finalized_epoch` would have already been updated with the previous higher justified we are concerned about when such a leaf block was imported. This is not the case because we are only now updating `store.finalized` on this other branch, thus such a leaf block *does not exist*.
Due to the above, the entire (1) and (2) are misguided conditionals and can be entirely removed. Instead `on_block` "Update finalized checkpoint" section can be written as follows, always atomically updated finalized and justified:
```python
# Update finalized checkpoint
if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch:
store.finalized_checkpoint = state.finalized_checkpoint
store.justified_checkpoint = state.current_justified_checkpoint
```
### Code-0
Code snippet from phase0 [`on_block`](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#on_block)
*The main idea is that there are two cases we want to update justified when we update finalized*:
1. When the newly justified is strictly better (greater than) what is in the store. Thus the `state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch` condition.
2. When `store.justified_checkpoint` is *higher* than what is in state, but it is on a chain that doesn't know about the finalized root.
It turns out that (2) is misguided. It can lead to the `store` having a combination of finalized and justified checkpoints that do not exist in any leaf state. Thus `get-filtered_block_tree` will return an empty dict.
```python
# Update finalized checkpoint
if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch:
store.finalized_checkpoint = state.finalized_checkpoint
# Potentially update justified if different from store
if store.justified_checkpoint != state.current_justified_checkpoint:
# Update justified if new justified is later than store justified
if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
store.justified_checkpoint = state.current_justified_checkpoint
return
# Update justified if store justified is not in chain with finalized checkpoint
finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
ancestor_at_finalized_slot = get_ancestor(store, store.justified_checkpoint.root, finalized_slot)
if ancestor_at_finalized_slot != store.finalized_checkpoint.root:
store.justified_checkpoint = state.current_justified_checkpoint
```
### Code-A
Code snippet from phase0 [`filter_block_tree`](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#filter_block_tree)
*If `correct_justified` and `correct_finalized` are never seen as a combo in any leaf state, then `filter_block_tree` will return an empty tree.*
```python
# If leaf block, check finalized/justified checkpoints as matching latest.
head_state = store.block_states[block_root]
correct_justified = (
store.justified_checkpoint.epoch == GENESIS_EPOCH
or head_state.current_justified_checkpoint == store.justified_checkpoint
)
correct_finalized = (
store.finalized_checkpoint.epoch == GENESIS_EPOCH
or head_state.finalized_checkpoint == store.finalized_checkpoint
)
# If expected finalized/justified, add to viable block-tree and signal viability to parent.
if correct_justified and correct_finalized:
blocks[block_root] = block
return True
```
### Code-B
Code snippet from phase0 [`get_head`](https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/fork-choice.md#get_head)
*If get_filtered_block_tree returns and empty `dict` then the first iteration of `while` loop will have empty children which will return `head` which is the value it was initialized at -- `store.justified_checkpoint.root`*
```python
blocks = get_filtered_block_tree(store)
# Execute the LMD-GHOST fork choice
head = store.justified_checkpoint.root
while True:
children = [
root for root in blocks.keys()
if blocks[root].parent_root == head
]
if len(children) == 0:
return head
```