Skip to content

Confirmation Rule #3339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 171 commits into
base: dev
Choose a base branch
from

Conversation

saltiniroberto
Copy link
Contributor

@saltiniroberto saltiniroberto commented Apr 27, 2023

Introduction

The objective of this PR is to introduce a Confirmation Rule for the Ethereum protocol.
A confirmation rule is an algorithm run by nodes, outputting whether a certain block is confirmed. When that is the case, the block is guaranteed to never be reorged, under certain assumptions, primarily about network synchrony and about the percentage of honest stake.

Detailed Explanation

For a detailed explanation of the algorithm, see this article.
The algorithm specified in this PR corresponds to Algorithm 5 in the paper.

TODO

Here is a non-exclusive list of TODOs

  • Review the spec change for correctness
    • Algorithm correctness
    • Type correctness (e.g. int vs float or uint64)
  • Add preconditions (e.g. parameters >= 0, etc..)
  • Review the naming used for the various functions
  • Improve the documentation in the spec
  • Add more tests
  • Check that the changes to the file setup.py are correct. The current changes allow linting the confirmation rule spec, but they may not be entirely correct.

Last things to do before merging

  • Revert the changes to linter.ini. These changes have been introduced just to speed up the development process by relaxing the requirement on the maximum line length.
  • Move fork_choice/confirmation_rule.md to specs/bellatrix and delete the fork_choice folder.
  • Make sure PR Confirmation rule prerequisite - fork choice filter change #3431 has already been merged
  • Double check any change to the fork choice tests

saltiniroberto and others added 30 commits April 12, 2023 12:41
Copy link

@Mart1i1n Mart1i1n left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Find type

@saltiniroberto saltiniroberto marked this pull request as ready for review May 2, 2024 13:28

return (
end_epoch > start_epoch + 1
or (end_epoch == start_epoch + 1 and start_slot % SLOTS_PER_EPOCH == 0))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If start_slot = 0 and end_slot = 31 (where end_epoch == start_epoch == 0), do we count it as "includes an entire epoch"? If not, should we describe the function as exclusive end_slot?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another way would be to change the doc to

Returns True if the range from start_slot to end_slot (inclusive of both) includes an entire epoch

In this way, we are not saying that if the range includes an entire epoch, then the function returns True.
Only that whenever the function returns True than the range includes an entire epoch.

The above works with both inclusive and exclusive.

Comment on lines 211 to 218
if is_full_validator_set_for_block_covered(store, block_root):
return is_one_confirmed(store, block_root)
else:
block = store.blocks[block_root]
return (
is_one_confirmed(store, block_root)
and is_lmd_confirmed(store, block.parent_root)
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it different from the definition 6 (lmd-safety condition) in the paper? The paper require all the ancestors of the block to be is_lmd_confirmed, regardless of whether full validator set is covered or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It is different.
This does not always yield exactly the same result as in the paper, but it does in most cases, and it is quicker to compute.

current_slot = get_current_slot(store)
block = store.blocks[block_root]
parent_block = store.blocks[block.parent_root]
support = int(get_weight(store, block_root))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the weight from the fork_choice data, which can be queried from the beacon API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so.


block_epoch = compute_epoch_at_slot(block.slot)

# If `block_epoch` is not either the current or previous epoch, then return `store.finalized_checkpoint.root`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the confirmation rule for the block between finalized checkpoint and justified checkpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any block descendant of the latest finalized checkpoint is treated in the same way

support = int(get_weight(store, block_root))
justified_state = store.checkpoint_states[store.justified_checkpoint]
maximum_support = int(
get_committee_weight_between_slots(justified_state, Slot(parent_block.slot + 1), Slot(current_slot - 1))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we assume that we run the protocol at the beginning of each slot, when the block for the current slot is not proposed? Otherwise, for the block proposed in the current slot (with its parent proposed in the previous slot), Slot(parent_block.slot + 1) > Slot(current_slot - 1).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The protocol is run at the beginning of each slot regardless of whether we propose or not a block in that slot. Also, we do not run the protocol on blocks for the current slots regardless.

Comment on lines 358 to 362
min(
ceil_div(total_active_balance * CONFIRMATION_BYZANTINE_THRESHOLD, 100),
CONFIRMATION_SLASHING_THRESHOLD,
ffg_support_for_checkpoint
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ceil_div(total_active_balance * CONFIRMATION_BYZANTINE_THRESHOLD, 100) always smaller than or equal to CONFIRMATION_SLASHING_THRESHOLD?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the paper, it is ceil_div((total_active_balance - remaining_ffg_weight) * CONFIRMATION_BYZANTINE_THRESHOLD, 100). I wonder whether it is a typo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ceil_div(total_active_balance * CONFIRMATION_BYZANTINE_THRESHOLD, 100) always smaller than or equal to CONFIRMATION_SLASHING_THRESHOLD

Not necessarily.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is ceil_div(total_active_balance * CONFIRMATION_BYZANTINE_THRESHOLD, 100) always smaller than or equal to CONFIRMATION_SLASHING_THRESHOLD?

Which paper are you referring to?

@Mediabs2022
Copy link

I have show a way to delay the epoch justification delay justification and a way to use the delay of epoch justification to make honest chain not viable for head voting delay attack. Here is the current judgement condition in spec:

if not correct_justified and is_previous_epoch_justified(store): # A
    correct_justified = (
        store.unrealized_justifications[block_root].epoch >= store.justified_checkpoint.epoch and # B
        voting_source.epoch + 2 >= current_epoch # C
    )

The problem is in line B. The honest chain can not justify previous epoch, so unrealized epoch < justified epoch. This makes such a judgement not work, as honest chain is not viable for head as designed.

There is a proposed change (#3431) into the spec that fixes the problem you're describing and that is essential to the confirmation rule correctness

Sorry if I wasn’t clear, but I think there might be a misunderstanding about the code. store.unrealized_justifications[block_root].epoch >= store.justified_checkpoint.epoch requires that the chain must contains enough attestations to justified the previous epoch, but in our attack, the honest chain can not contain enough attestations to justified the previous epoch.

According to the change I have mentioned above the canonical chain will have an entire epoch to include those attestations on chain. There is also related patch allowing for such inclusion: #3360

yes, you are right. sorry for misunderstand the code.

However, I think theoretically this attack can still occur under semi - synchronous conditions (for example, in the case of network partition). It just requires withholding for a longer time.

@Mart1i1n
Copy link

I have show a way to delay the epoch justification delay justification and a way to use the delay of epoch justification to make honest chain not viable for head voting delay attack. Here is the current judgement condition in spec:

if not correct_justified and is_previous_epoch_justified(store): # A
    correct_justified = (
        store.unrealized_justifications[block_root].epoch >= store.justified_checkpoint.epoch and # B
        voting_source.epoch + 2 >= current_epoch # C
    )

The problem is in line B. The honest chain can not justify previous epoch, so unrealized epoch < justified epoch. This makes such a judgement not work, as honest chain is not viable for head as designed.

There is a proposed change (#3431) into the spec that fixes the problem you're describing and that is essential to the confirmation rule correctness

Sorry if I wasn’t clear, but I think there might be a misunderstanding about the code. store.unrealized_justifications[block_root].epoch >= store.justified_checkpoint.epoch requires that the chain must contains enough attestations to justified the previous epoch, but in our attack, the honest chain can not contain enough attestations to justified the previous epoch.

According to the change I have mentioned above the canonical chain will have an entire epoch to include those attestations on chain. There is also related patch allowing for such inclusion: #3360

yes, you are right. sorry for misunderstand the code.

However, I think theoretically this attack can still occur under semi - synchronous conditions (for example, in the case of network partition). It just requires withholding for a longer time.

Yeah, even in synchronous conditions, this attack can still be conducted. We have shown such a way in the appendix of our paper eprint. Also, we have designed a solution to address all reorganizations including this attack in this paper.

@Jiggnubs

This comment was marked as spam.

# for an explanation of the formula used below.

# First, calculate the number of committees in the end epoch
num_slots_in_end_epoch = int(compute_slots_since_epoch_start(end_slot) + 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
num_slots_in_end_epoch = int(compute_slots_since_epoch_start(end_slot) + 1)
num_slots_in_end_epoch = int(compute_slots_since_epoch_start(end_slot))

I think it’s an off-by-one. Consider end_slot=63, SLOTS_PER_EPOCH=32, then num_slots_in_end_epoch = 32 which doesn’t seem correct

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have built an elegant and provable solution to solve all known attacks for Ethereum PoS, the EIP can be found in eip.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it’s an off-by-one. Consider end_slot=63, SLOTS_PER_EPOCH=32, then num_slots_in_end_epoch = 32 which doesn’t seem correct

This should be correct as, by the function spec, we also want to consider the weights associated with end_slot.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and compute_slots_since_epoch_start(end_slot) should already include the end_slot to my observation.

compute_slots_since_epoch_start(end_slot) evaluates to: end_slot - compute_start_slot_at_epoch(compute_epoch_at_slot(end_slot)) = end_slot - epoch_start_slot, what am I missing?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
general:enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.