From 757f5a31ddad30381f53ca07bddab0a3156b3aec Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 17 Feb 2020 12:02:24 -0700 Subject: [PATCH 1/2] add proposer index and add/modify tests --- specs/phase0/beacon-chain.md | 23 +++++++---- specs/phase0/validator.md | 9 ++-- specs/phase1/beacon-chain.md | 1 + .../pyspec/eth2spec/test/helpers/block.py | 12 ++++++ .../test/helpers/proposer_slashings.py | 2 +- .../test_process_block_header.py | 12 ++++++ .../test_process_proposer_slashing.py | 41 +++++++++++++------ .../eth2spec/test/sanity/test_blocks.py | 2 +- 8 files changed, 78 insertions(+), 24 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index acf098663..fc0a5f53b 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -377,6 +377,7 @@ class DepositData(Container): ```python class BeaconBlockHeader(Container): slot: Slot + proposer_index: ValidatorIndex parent_root: Root state_root: Root body_root: Root @@ -396,7 +397,6 @@ class SigningRoot(Container): ```python class ProposerSlashing(Container): - proposer_index: ValidatorIndex signed_header_1: SignedBeaconBlockHeader signed_header_2: SignedBeaconBlockHeader ``` @@ -456,6 +456,7 @@ class BeaconBlockBody(Container): ```python class BeaconBlock(Container): slot: Slot + proposer_index: ValidatorIndex parent_root: Root state_root: Root body: BeaconBlockBody @@ -1163,7 +1164,7 @@ def state_transition(state: BeaconState, signed_block: SignedBeaconBlock, valida ```python def verify_block_signature(state: BeaconState, signed_block: SignedBeaconBlock) -> bool: - proposer = state.validators[get_beacon_proposer_index(state)] + proposer = state.validators[signed_block.message.proposer_index] signing_root = compute_signing_root(signed_block.message, get_domain(state, DOMAIN_BEACON_PROPOSER)) return bls.Verify(proposer.pubkey, signing_root, signed_block.signature) ``` @@ -1434,18 +1435,21 @@ def process_block(state: BeaconState, block: BeaconBlock) -> None: def process_block_header(state: BeaconState, block: BeaconBlock) -> None: # Verify that the slots match assert block.slot == state.slot + # Verify that proposer index is the correct index + assert block.proposer_index == get_beacon_proposer_index(state) # Verify that the parent matches assert block.parent_root == hash_tree_root(state.latest_block_header) # Cache current block as the new latest block state.latest_block_header = BeaconBlockHeader( slot=block.slot, + proposer_index=block.proposer_index, parent_root=block.parent_root, state_root=Bytes32(), # Overwritten in the next process_slot call body_root=hash_tree_root(block.body), ) # Verify proposer is not slashed - proposer = state.validators[get_beacon_proposer_index(state)] + proposer = state.validators[block.proposer_index] assert not proposer.slashed ``` @@ -1494,12 +1498,17 @@ def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: ```python def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: + header_1 = proposer_slashing.signed_header_1.message + header_2 = proposer_slashing.signed_header_2.message + # Verify header slots match - assert proposer_slashing.signed_header_1.message.slot == proposer_slashing.signed_header_2.message.slot + assert header_1.slot == header_2.slot + # Verify header proposer indices match + assert header_1.proposer_index == header_2.proposer_index # Verify the headers are different - assert proposer_slashing.signed_header_1 != proposer_slashing.signed_header_2 + assert header_1 != header_2 # Verify the proposer is slashable - proposer = state.validators[proposer_slashing.proposer_index] + proposer = state.validators[header_1.proposer_index] assert is_slashable_validator(proposer, get_current_epoch(state)) # Verify signatures for signed_header in (proposer_slashing.signed_header_1, proposer_slashing.signed_header_2): @@ -1507,7 +1516,7 @@ def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSla signing_root = compute_signing_root(signed_header.message, domain) assert bls.Verify(proposer.pubkey, signing_root, signed_header.signature) - slash_validator(state, proposer_slashing.proposer_index) + slash_validator(state, header_1.proposer_index) ``` ##### Attester slashings diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index 0bde81e60..ffd75caf4 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -27,6 +27,7 @@ - [Block proposal](#block-proposal) - [Preparing for a `BeaconBlock`](#preparing-for-a-beaconblock) - [Slot](#slot) + - [Proposer index](#proposer-index) - [Parent root](#parent-root) - [Constructing the `BeaconBlockBody`](#constructing-the-beaconblockbody) - [Randao reveal](#randao-reveal) @@ -183,8 +184,7 @@ def get_committee_assignment(state: BeaconState, A validator can use the following function to see if they are supposed to propose during a slot. This function can only be run with a `state` of the slot in question. Proposer selection is only stable within the context of the current epoch. ```python -def is_proposer(state: BeaconState, - validator_index: ValidatorIndex) -> bool: +def is_proposer(state: BeaconState, validator_index: ValidatorIndex) -> bool: return get_beacon_proposer_index(state) == validator_index ``` @@ -224,11 +224,14 @@ Set `block.slot = slot` where `slot` is the current slot at which the validator *Note*: There might be "skipped" slots between the `parent` and `block`. These skipped slots are processed in the state transition function without per-block processing. +##### Proposer index + +Set `block.proposer_index = validator_index` where `validator_index` is the validator chosen to propose at this slot. The private key mapping to `state.validators[validator_index].pubkey` is used to sign the block. + ##### Parent root Set `block.parent_root = hash_tree_root(parent)`. - #### Constructing the `BeaconBlockBody` ##### Randao reveal diff --git a/specs/phase1/beacon-chain.md b/specs/phase1/beacon-chain.md index 78b3b3d25..f5084ef21 100644 --- a/specs/phase1/beacon-chain.md +++ b/specs/phase1/beacon-chain.md @@ -221,6 +221,7 @@ Note that the `body` has a new `BeaconBlockBody` definition. ```python class BeaconBlock(Container): slot: Slot + proposer_index: ValidatorIndex parent_root: Root state_root: Root body: BeaconBlockBody diff --git a/tests/core/pyspec/eth2spec/test/helpers/block.py b/tests/core/pyspec/eth2spec/test/helpers/block.py index 488e051bd..96cc30e35 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/block.py +++ b/tests/core/pyspec/eth2spec/test/helpers/block.py @@ -65,10 +65,22 @@ def apply_empty_block(spec, state): def build_empty_block(spec, state, slot=None): + """ + Build empty block for ``slot``, built upon the latest block header seen by ``state``. + Slot must be greater than or equal to the current slot in ``state``. + """ if slot is None: slot = state.slot + if slot < state.slot: + raise Exception("build_empty_block cannot build blocks for past slots") + if slot > state.slot: + # transition forward in copied state to grab relevant data from state + state = state.copy() + spec.process_slots(state, slot) + empty_block = spec.BeaconBlock() empty_block.slot = slot + empty_block.proposer_index = spec.get_beacon_proposer_index(state) empty_block.body.eth1_data.deposit_count = state.eth1_deposit_index previous_block_header = state.latest_block_header.copy() if previous_block_header.state_root == spec.Root(): diff --git a/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py b/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py index 79a0b9009..ac2ebcf9c 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py +++ b/tests/core/pyspec/eth2spec/test/helpers/proposer_slashings.py @@ -10,6 +10,7 @@ def get_valid_proposer_slashing(spec, state, signed_1=False, signed_2=False): header_1 = spec.BeaconBlockHeader( slot=slot, + proposer_index=validator_index, parent_root=b'\x33' * 32, state_root=b'\x44' * 32, body_root=b'\x55' * 32, @@ -27,7 +28,6 @@ def get_valid_proposer_slashing(spec, state, signed_1=False, signed_2=False): signed_header_2 = spec.SignedBeaconBlockHeader(message=header_2) return spec.ProposerSlashing( - proposer_index=validator_index, signed_header_1=signed_header_1, signed_header_2=signed_header_2, ) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_block_header.py b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_block_header.py index b51584ce5..a2eb744b9 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_block_header.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_block_header.py @@ -47,6 +47,18 @@ def test_invalid_slot_block_header(spec, state): yield from run_block_header_processing(spec, state, block, valid=False) +@with_all_phases +@spec_state_test +def test_invalid_proposer_index(spec, state): + block = build_empty_block_for_next_slot(spec, state) + + active_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state)) + active_indices = [i for i in active_indices if i != block.proposer_index] + block.proposer_index = active_indices[0] # invalid proposer index + + yield from run_block_header_processing(spec, state, block, valid=False) + + @with_all_phases @spec_state_test def test_invalid_parent_root(spec, state): diff --git a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py index 30b3c1fdd..5f1fca969 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/block_processing/test_process_proposer_slashing.py @@ -22,22 +22,20 @@ def run_proposer_slashing_processing(spec, state, proposer_slashing, valid=True) yield 'post', None return - pre_proposer_balance = get_balance(state, proposer_slashing.proposer_index) + proposer_index = proposer_slashing.signed_header_1.message.proposer_index + pre_proposer_balance = get_balance(state, proposer_index) spec.process_proposer_slashing(state, proposer_slashing) yield 'post', state # check if slashed - slashed_validator = state.validators[proposer_slashing.proposer_index] + slashed_validator = state.validators[proposer_index] assert slashed_validator.slashed assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH # lost whistleblower reward - assert ( - get_balance(state, proposer_slashing.proposer_index) < - pre_proposer_balance - ) + assert get_balance(state, proposer_index) < pre_proposer_balance @with_all_phases @@ -77,7 +75,24 @@ def test_invalid_sig_1_and_2(spec, state): def test_invalid_proposer_index(spec, state): proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True) # Index just too high (by 1) - proposer_slashing.proposer_index = len(state.validators) + proposer_slashing.signed_header_1.message.proposer_index = len(state.validators) + proposer_slashing.signed_header_2.message.proposer_index = len(state.validators) + + yield from run_proposer_slashing_processing(spec, state, proposer_slashing, False) + + +@with_all_phases +@spec_state_test +def test_invalid_different_proposer_indices(spec, state): + proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True) + # set different index and sign + header_1 = proposer_slashing.signed_header_1.message + header_2 = proposer_slashing.signed_header_2.message + active_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state)) + active_indices = [i for i in active_indices if i != header_1.proposer_index] + + header_2.proposer_index = active_indices[0] + proposer_slashing.signed_header_2 = sign_block_header(spec, state, header_2, privkeys[header_2.proposer_index]) yield from run_proposer_slashing_processing(spec, state, proposer_slashing, False) @@ -89,9 +104,9 @@ def test_epochs_are_different(spec, state): # set slots to be in different epochs header_2 = proposer_slashing.signed_header_2.message + proposer_index = header_2.proposer_index header_2.slot += spec.SLOTS_PER_EPOCH - proposer_slashing.signed_header_2 = sign_block_header( - spec, state, header_2, privkeys[proposer_slashing.proposer_index]) + proposer_slashing.signed_header_2 = sign_block_header(spec, state, header_2, privkeys[proposer_index]) yield from run_proposer_slashing_processing(spec, state, proposer_slashing, False) @@ -113,7 +128,8 @@ def test_proposer_is_not_activated(spec, state): proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True) # set proposer to be not active yet - state.validators[proposer_slashing.proposer_index].activation_epoch = spec.get_current_epoch(state) + 1 + proposer_index = proposer_slashing.signed_header_1.message.proposer_index + state.validators[proposer_index].activation_epoch = spec.get_current_epoch(state) + 1 yield from run_proposer_slashing_processing(spec, state, proposer_slashing, False) @@ -124,7 +140,8 @@ def test_proposer_is_slashed(spec, state): proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True) # set proposer to slashed - state.validators[proposer_slashing.proposer_index].slashed = True + proposer_index = proposer_slashing.signed_header_1.message.proposer_index + state.validators[proposer_index].slashed = True yield from run_proposer_slashing_processing(spec, state, proposer_slashing, False) @@ -138,7 +155,7 @@ def test_proposer_is_withdrawn(spec, state): state.slot += spec.SLOTS_PER_EPOCH # set proposer withdrawable_epoch in past current_epoch = spec.get_current_epoch(state) - proposer_index = proposer_slashing.proposer_index + proposer_index = proposer_slashing.signed_header_1.message.proposer_index state.validators[proposer_index].withdrawable_epoch = current_epoch - 1 yield from run_proposer_slashing_processing(spec, state, proposer_slashing, False) diff --git a/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py index 9027660ab..acfef9cd7 100644 --- a/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py @@ -187,7 +187,7 @@ def test_proposer_slashing(spec, state): # copy for later balance lookups. pre_state = deepcopy(state) proposer_slashing = get_valid_proposer_slashing(spec, state, signed_1=True, signed_2=True) - validator_index = proposer_slashing.proposer_index + validator_index = proposer_slashing.signed_header_1.message.proposer_index assert not state.validators[validator_index].slashed From 71be8940b60dcf6958907f796666464acc51df5b Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 18 Feb 2020 12:56:37 -0600 Subject: [PATCH 2/2] add a couple more sanity block tests for added rpoposer_index --- .../eth2spec/test/sanity/test_blocks.py | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py index acfef9cd7..ad7c20802 100644 --- a/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py @@ -119,6 +119,49 @@ def test_invalid_block_sig(spec, state): yield 'post', None +@with_all_phases +@spec_state_test +@always_bls +def test_invalid_proposer_index_sig_from_expected_proposer(spec, state): + yield 'pre', state + + block = build_empty_block_for_next_slot(spec, state) + expect_proposer_index = block.proposer_index + + # Set invalid proposer index but correct signature wrt expected proposer + active_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state)) + active_indices = [i for i in active_indices if i != block.proposer_index] + block.proposer_index = active_indices[0] # invalid proposer index + + invalid_signed_block = sign_block(spec, state, block, expect_proposer_index) + + expect_assertion_error(lambda: spec.state_transition(state, invalid_signed_block)) + + yield 'blocks', [invalid_signed_block] + yield 'post', None + + +@with_all_phases +@spec_state_test +@always_bls +def test_invalid_proposer_index_sig_from_proposer_index(spec, state): + yield 'pre', state + + block = build_empty_block_for_next_slot(spec, state) + + # Set invalid proposer index but correct signature wrt proposer_index + active_indices = spec.get_active_validator_indices(state, spec.get_current_epoch(state)) + active_indices = [i for i in active_indices if i != block.proposer_index] + block.proposer_index = active_indices[0] # invalid proposer index + + invalid_signed_block = sign_block(spec, state, block, block.proposer_index) + + expect_assertion_error(lambda: spec.state_transition(state, invalid_signed_block)) + + yield 'blocks', [invalid_signed_block] + yield 'post', None + + @with_all_phases @spec_state_test def test_skipped_slots(spec, state):