# 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 ```