nimbus-eth2/tests/fork_choice/scenarios/votes.nim

683 lines
17 KiB
Nim

# beacon_chain
# Copyright (c) 2018-2021 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
# import ../interpreter # included to be able to use "suite"
func setup_votes(): tuple[fork_choice: ForkChoiceBackend, ops: seq[Operation]] =
var balances = @[Gwei(1), Gwei(1)]
let GenesisRoot = fakeHash(0)
# Initialize the fork choice context
# We start with epoch 0 fully finalized to avoid epoch 0 special cases.
result.fork_choice = ForkChoiceBackend.init(
justifiedCheckpoint = Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalizedCheckpoint = Checkpoint(root: GenesisRoot, epoch: Epoch(1))
)
# ----------------------------------
# Head should be genesis
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: GenesisRoot
)
# Add block 2
#
# 0
# /
# 2
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(2),
parent_root: GenesisRoot,
blk_justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
blk_finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1))
)
# Head should be 2
#
# 0
# /
# 2 <- head
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(2)
)
# Add block 1 as a fork
#
# 0
# / \
# 2 1
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(1),
parent_root: GenesisRoot,
blk_justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
blk_finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1))
)
# Head is still 2 due to tiebreaker as fakeHash(2) (0xD8...) > fakeHash(1) (0x7C...)
#
# 0
# / \
# head-> 2 1
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(2)
)
# Add a vote to block 1
#
# 0
# / \
# 2 1 <- +vote
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(0),
block_root: fakeHash(1),
target_epoch: Epoch(2)
)
# Head is now 1 as 1 has an extra vote
#
# 0
# / \
# 2 1 <- head
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(1)
)
# Add a vote to block 2
#
# 0
# / \
# +vote-> 2 1
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(1),
block_root: fakeHash(2),
target_epoch: Epoch(2)
)
# Head is back to 2 due to tiebreaker as fakeHash(2) (0xD8...) > fakeHash(1) (0x7C...)
#
# 0
# / \
# head-> 2 1
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(2)
)
# Add block 3 as on chain 1
#
# 0
# / \
# 2 1
# |
# 3
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(3),
parent_root: fakeHash(1),
blk_justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
blk_finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1))
)
# Head is still 2
#
# 0
# / \
# head-> 2 1
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(2)
)
# Move validator #0 vote from 1 to 3
#
# 0
# / \
# 2 1 <- -vote
# |
# 3 <- +vote
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(0),
block_root: fakeHash(3),
target_epoch: Epoch(3)
)
# Head is still 2
#
# 0
# / \
# head-> 2 1
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(2)
)
# Move validator #1 vote from 2 to 1 (this is an equivocation, but fork choice doesn't
# care)
#
# 0
# / \
# -vote-> 2 1 <- +vote
# |
# 3
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(1),
block_root: fakeHash(1),
target_epoch: Epoch(3)
)
# Head is now 3
#
# 0
# / \
# 2 1
# |
# 3 <- head
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(3)
)
# Add block 4 on chain 1-3
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(4),
parent_root: fakeHash(3),
blk_justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
blk_finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1))
)
# Head is now 4
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4 <- head
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(4)
)
# Add block 5, which has a justified epoch of 2.
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# /
# 5 <- justified epoch = 2
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(5),
parent_root: fakeHash(4),
blk_justified_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
blk_finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2))
)
# Ensure that 5 is filtered out and the head stays at 4.
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4 <- head
# /
# 5
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(4)
)
# Add block 6, which has a justified epoch of 0.
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# 5 6 <- justified epoch = 0
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(6),
parent_root: fakeHash(4),
blk_justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
blk_finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1))
)
# Move both votes to 5.
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# +2 vote-> 5 6
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(0),
block_root: fakeHash(5),
target_epoch: Epoch(4)
)
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(1),
block_root: fakeHash(5),
target_epoch: Epoch(4)
)
# Add blocks 7, 8 and 9. Adding these blocks helps test the `best_descendant`
# functionality.
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# 5 6
# |
# 7
# |
# 8
# /
# 9
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(7),
parent_root: fakeHash(5),
blk_justified_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
blk_finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1))
)
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(8),
parent_root: fakeHash(7),
blk_justified_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
blk_finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1))
)
# Finalizes 5
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(9),
parent_root: fakeHash(8),
blk_justified_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
blk_finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2))
)
# Ensure that 6 is the head, even though 5 has all the votes. This is testing to ensure
# that 5 is filtered out due to a differing justified epoch.
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# 5 6 <- head
# |
# 7
# |
# 8
# /
# 9
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
finalized_checkpoint: Checkpoint(root: GenesisRoot, epoch: Epoch(1)),
justified_state_balances: balances,
expected_head: fakeHash(6)
)
# Change fork-choice justified epoch to 1, and the start block to 5 and ensure that 9 is
# the head.
#
# << Change justified epoch to 1 >>
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# 5 6
# |
# 7
# |
# 8
# /
# head-> 9
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakehash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(9)
)
# Update votes to block 9
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# 5 6
# |
# 7
# |
# 8
# /
# 9 <- +2 votes
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(0),
block_root: fakeHash(9),
target_epoch: Epoch(5)
)
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(1),
block_root: fakeHash(9),
target_epoch: Epoch(5)
)
# Head should still be 9
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakehash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(9)
)
# Add block 10 (also finalizes 5)
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# 5 6
# |
# 7
# |
# 8
# / \
# 9 10
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(10),
parent_root: fakeHash(8),
blk_justified_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
blk_finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2))
)
# Head should still be 9
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakehash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(9)
)
# Introduce 2 new validators
balances = @[Gwei(1), Gwei(1), Gwei(1), Gwei(1)]
# Have them vote for block 10
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# 5 6
# |
# 7
# |
# 8
# / \
# 9 10 <- +2 votes
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(2),
block_root: fakeHash(10),
target_epoch: Epoch(5)
)
result.ops.add Operation(
kind: ProcessAttestation,
validator_index: ValidatorIndex(3),
block_root: fakeHash(10),
target_epoch: Epoch(5)
)
# Check that the head is now 10.
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# / \
# 5 6
# |
# 7
# |
# 8
# / \
# 9 10 <- head
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakehash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(10)
)
# Set the last 2 validators balances to 0
balances = @[Gwei(1), Gwei(1), Gwei(0), Gwei(0)]
# head should be 9 again
# .
# |
# 8
# / \
# head -> 9 10
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakehash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(9)
)
# Set the last 2 validators balances back to 1
balances = @[Gwei(1), Gwei(1), Gwei(1), Gwei(1)]
# head should be 10 again
# .
# |
# 8
# / \
# 9 10 <- head
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakehash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(10)
)
# Remove the validators
balances = @[Gwei(1), Gwei(1)]
# head should be 9 again
# .
# |
# 8
# / \
# head -> 9 10
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakehash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(9)
)
# Ensure that pruning does prune.
#
#
# 0
# / \
# 2 1
# |
# 3
# |
# 4
# -------pruned here ------
# 5 6
# |
# 7
# |
# 8
# / \
# 9 10
# Note: 5 and 6 become orphans
# - 5 is the new root
# - 6 is a discarded chain
result.ops.add Operation(
kind: Prune,
finalized_root: fakeHash(5),
expected_len: 6
)
# Prune shouldn't have changed the head
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(9)
)
# Add block 11
#
# 5 6
# |
# 7
# |
# 8
# / \
# 9 10
# |
# 11
result.ops.add Operation(
kind: ProcessBlock,
root: fakeHash(11),
parent_root: fakeHash(9),
blk_justified_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
blk_finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2))
)
# Head is now 11
result.ops.add Operation(
kind: FindHead,
justified_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
finalized_checkpoint: Checkpoint(root: fakeHash(5), epoch: Epoch(2)),
justified_state_balances: balances,
expected_head: fakeHash(11)
)
proc test_votes() =
test "fork_choice - testing with votes":
# for i in 0 ..< 12:
# echo " block (", i, ") hash: ", fakeHash(i)
# echo " ------------------------------------------------------"
var (ctx, ops) = setup_votes()
ctx.run(ops)
test_votes()