diff --git a/README.md b/README.md index c5c88daf9..ce0ae8738 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This repo hosts the current eth2.0 specifications. Discussions about design rati Core specifications for eth2.0 client validation can be found in [specs/core](specs/core). These are divided into phases. Each subsequent phase depends upon the prior. The current phases specified are: * [Phase 0 -- The Beacon Chain](specs/core/0_beacon-chain.md) +* [Phase 1 -- Custody game](specs/core/1_custody-game.md) * [Phase 1 -- Shard Data Chains](specs/core/1_shard-data-chains.md) Accompanying documents can be found in [specs](specs) and include diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index fa0c97acb..f95cb29e8 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -18,7 +18,7 @@ - [Time parameters](#time-parameters) - [State list lengths](#state-list-lengths) - [Reward and penalty quotients](#reward-and-penalty-quotients) - - [Max transactions per block](#max-transactions-per-block) + - [Max operations per block](#max-operations-per-block) - [Signature domains](#signature-domains) - [Data structures](#data-structures) - [Misc dependencies](#misc-dependencies) @@ -34,7 +34,7 @@ - [`Validator`](#validator) - [`PendingAttestation`](#pendingattestation) - [`HistoricalBatch`](#historicalbatch) - - [Beacon transactions](#beacon-transactions) + - [Beacon operations](#beacon-operations) - [`ProposerSlashing`](#proposerslashing) - [`AttesterSlashing`](#attesterslashing) - [`Attestation`](#attestation) @@ -132,7 +132,7 @@ - [Block header](#block-header) - [RANDAO](#randao) - [Eth1 data](#eth1-data-1) - - [Transactions](#transactions) + - [Operations](#operations) - [Proposer slashings](#proposer-slashings) - [Attester slashings](#attester-slashings) - [Attestations](#attestations) @@ -261,8 +261,7 @@ Code snippets appearing in `this style` are to be interpreted as Python code. * The `BASE_REWARD_QUOTIENT` parameter dictates the per-epoch reward. It corresponds to ~2.54% annual interest assuming 10 million participating ETH in every epoch. * The `INACTIVITY_PENALTY_QUOTIENT` equals `INVERSE_SQRT_E_DROP_TIME**2` where `INVERSE_SQRT_E_DROP_TIME := 2**12 epochs` (~18 days) is the time it takes the inactivity penalty to reduce the balance of non-participating [validators](#dfn-validator) to about `1/sqrt(e) ~= 60.6%`. Indeed, the balance retained by offline [validators](#dfn-validator) after `n` epochs is about `(1 - 1/INACTIVITY_PENALTY_QUOTIENT)**(n**2/2)` so after `INVERSE_SQRT_E_DROP_TIME` epochs it is roughly `(1 - 1/INACTIVITY_PENALTY_QUOTIENT)**(INACTIVITY_PENALTY_QUOTIENT/2) ~= 1/sqrt(e)`. - -### Max transactions per block +### Max operations per block | Name | Value | | - | - | @@ -460,7 +459,7 @@ The types are defined topologically to aid in facilitating an executable version } ``` -### Beacon transactions +### Beacon operations #### `ProposerSlashing` @@ -1188,6 +1187,9 @@ def verify_indexed_attestation(state: BeaconState, indexed_attestation: IndexedA custody_bit_0_indices = indexed_attestation.custody_bit_0_indices custody_bit_1_indices = indexed_attestation.custody_bit_1_indices + # ensure no duplicate indices across custody bits + assert len(set(custody_bit_0_indices).intersection(set(custody_bit_1_indices))) == 0 + if len(custody_bit_1_indices) > 0: # [TO BE REMOVED IN PHASE 1] return False @@ -2243,7 +2245,7 @@ def process_eth1_data(state: BeaconState, block: BeaconBlock) -> None: state.eth1_data_votes.append(Eth1DataVote(eth1_data=block.body.eth1_data, vote_count=1)) ``` -#### Transactions +#### Operations ##### Proposer slashings @@ -2255,7 +2257,7 @@ For each `proposer_slashing` in `block.body.proposer_slashings`, run the followi def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: """ - Process ``ProposerSlashing`` transaction. + Process ``ProposerSlashing`` operation. Note that this function mutates ``state``. """ proposer = state.validator_registry[proposer_slashing.proposer_index] @@ -2286,7 +2288,7 @@ For each `attester_slashing` in `block.body.attester_slashings`, run the followi def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None: """ - Process ``AttesterSlashing`` transaction. + Process ``AttesterSlashing`` operation. Note that this function mutates ``state``. """ attestation1 = attester_slashing.attestation_1 @@ -2300,10 +2302,12 @@ def process_attester_slashing(state: BeaconState, assert verify_indexed_attestation(state, attestation1) assert verify_indexed_attestation(state, attestation2) + attesting_indices_1 = attestation1.custody_bit_0_indices + attestation1.custody_bit_1_indices + attesting_indices_2 = attestation2.custody_bit_0_indices + attestation2.custody_bit_1_indices slashable_indices = [ - index for index in attestation1.validator_indices + index for index in attesting_indices_1 if ( - index in attestation2.validator_indices and + index in attesting_indices_2 and is_slashable_validator(state.validator_registry[index], get_current_epoch(state)) ) ] @@ -2321,7 +2325,7 @@ For each `attestation` in `block.body.attestations`, run the following function: ```python def process_attestation(state: BeaconState, attestation: Attestation) -> None: """ - Process ``Attestation`` transaction. + Process ``Attestation`` operation. Note that this function mutates ``state``. """ assert max(GENESIS_SLOT, state.slot - SLOTS_PER_EPOCH) <= attestation.data.slot @@ -2370,7 +2374,7 @@ For each `exit` in `block.body.voluntary_exits`, run the following function: ```python def process_voluntary_exit(state: BeaconState, exit: VoluntaryExit) -> None: """ - Process ``VoluntaryExit`` transaction. + Process ``VoluntaryExit`` operation. Note that this function mutates ``state``. """ validator = state.validator_registry[exit.validator_index] @@ -2406,7 +2410,7 @@ For each `transfer` in `block.body.transfers`, run the following function: ```python def process_transfer(state: BeaconState, transfer: Transfer) -> None: """ - Process ``Transfer`` transaction. + Process ``Transfer`` operation. Note that this function mutates ``state``. """ # Verify the amount and fee aren't individually too big (for anti-overflow purposes) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index fd754634e..e28536d34 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -13,7 +13,7 @@ - [Constants](#constants) - [Misc](#misc) - [Time parameters](#time-parameters) - - [Max transactions per block](#max-transactions-per-block) + - [Max operations per block](#max-operations-per-block) - [Signature domains](#signature-domains) - [Data structures](#data-structures) - [Custody objects](#custody-objects) @@ -33,7 +33,7 @@ - [`epoch_to_custody_period`](#epoch_to_custody_period) - [`verify_custody_key`](#verify_custody_key) - [Per-block processing](#per-block-processing) - - [Transactions](#transactions) + - [Operations](#operations) - [Custody reveals](#custody-reveals) - [Chunk challenges](#chunk-challenges) - [Bit challenges](#bit-challenges) @@ -79,7 +79,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | `EPOCHS_PER_CUSTODY_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days | | `CUSTODY_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days | -### Max transactions per block +### Max operations per block | Name | Value | | - | - | @@ -259,9 +259,9 @@ def verify_custody_key(state: BeaconState, reveal: CustodyKeyReveal) -> bool: ## Per-block processing -### Transactions +### Operations -Add the following transactions to the per-block processing, in order the given below and after all other transactions in phase 0. +Add the following operations to the per-block processing, in order the given below and after all other operations in phase 0. #### Custody reveals diff --git a/tests/phase0/block_processing/test_process_attester_slashing.py b/tests/phase0/block_processing/test_process_attester_slashing.py new file mode 100644 index 000000000..06f214c4b --- /dev/null +++ b/tests/phase0/block_processing/test_process_attester_slashing.py @@ -0,0 +1,115 @@ +from copy import deepcopy +import pytest + +import build.phase0.spec as spec +from build.phase0.spec import ( + get_balance, + get_beacon_proposer_index, + process_attester_slashing, +) +from tests.phase0.helpers import ( + get_valid_attester_slashing, +) + +# mark entire file as 'attester_slashing' +pytestmark = pytest.mark.attester_slashings + + +def run_attester_slashing_processing(state, attester_slashing, valid=True): + """ + Run ``process_attester_slashing`` returning the pre and post state. + If ``valid == False``, run expecting ``AssertionError`` + """ + post_state = deepcopy(state) + + if not valid: + with pytest.raises(AssertionError): + process_attester_slashing(post_state, attester_slashing) + return state, None + + process_attester_slashing(post_state, attester_slashing) + + slashed_index = attester_slashing.attestation_1.custody_bit_0_indices[0] + slashed_validator = post_state.validator_registry[slashed_index] + assert not slashed_validator.initiated_exit + 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(post_state, slashed_index) < + get_balance(state, slashed_index) + ) + proposer_index = get_beacon_proposer_index(state, state.slot) + # gained whistleblower reward + assert ( + get_balance(post_state, proposer_index) > + get_balance(state, proposer_index) + ) + + return state, post_state + + +def test_success_double(state): + attester_slashing = get_valid_attester_slashing(state) + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing) + + return pre_state, attester_slashing, post_state + + +def test_success_surround(state): + attester_slashing = get_valid_attester_slashing(state) + + # set attestion1 to surround attestation 2 + attester_slashing.attestation_1.data.source_epoch = attester_slashing.attestation_2.data.source_epoch - 1 + attester_slashing.attestation_1.data.slot = attester_slashing.attestation_2.data.slot + spec.SLOTS_PER_EPOCH + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing) + + return pre_state, attester_slashing, post_state + + +def test_same_data(state): + attester_slashing = get_valid_attester_slashing(state) + + attester_slashing.attestation_1.data = attester_slashing.attestation_2.data + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) + + return pre_state, attester_slashing, post_state + + +def test_no_double_or_surround(state): + attester_slashing = get_valid_attester_slashing(state) + + attester_slashing.attestation_1.data.slot += spec.SLOTS_PER_EPOCH + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) + + return pre_state, attester_slashing, post_state + + +def test_participants_already_slashed(state): + attester_slashing = get_valid_attester_slashing(state) + + # set all indices to slashed + attestation_1 = attester_slashing.attestation_1 + validator_indices = attestation_1.custody_bit_0_indices + attestation_1.custody_bit_1_indices + for index in validator_indices: + state.validator_registry[index].slashed = True + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) + + return pre_state, attester_slashing, post_state + + +def test_custody_bit_0_and_1(state): + attester_slashing = get_valid_attester_slashing(state) + + attester_slashing.attestation_1.custody_bit_1_indices = ( + attester_slashing.attestation_1.custody_bit_0_indices + ) + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) + + return pre_state, attester_slashing, post_state diff --git a/tests/phase0/helpers.py b/tests/phase0/helpers.py index 083d31b80..0fd7bab73 100644 --- a/tests/phase0/helpers.py +++ b/tests/phase0/helpers.py @@ -12,6 +12,7 @@ from build.phase0.spec import ( Attestation, AttestationData, AttestationDataAndCustodyBit, + AttesterSlashing, BeaconBlockHeader, Deposit, DepositData, @@ -19,6 +20,7 @@ from build.phase0.spec import ( ProposerSlashing, VoluntaryExit, # functions + convert_to_indexed, get_active_validator_indices, get_attestation_participants, get_block_root, @@ -241,6 +243,17 @@ def get_valid_proposer_slashing(state): ) +def get_valid_attester_slashing(state): + attestation_1 = get_valid_attestation(state) + attestation_2 = deepcopy(attestation_1) + attestation_2.data.target_root = b'\x01'*32 + + return AttesterSlashing( + attestation_1=convert_to_indexed(state, attestation_1), + attestation_2=convert_to_indexed(state, attestation_2), + ) + + def get_valid_attestation(state, slot=None): if slot is None: slot = state.slot diff --git a/tests/phase0/test_sanity.py b/tests/phase0/test_sanity.py index a7a6b9961..e1308aebe 100644 --- a/tests/phase0/test_sanity.py +++ b/tests/phase0/test_sanity.py @@ -17,6 +17,7 @@ from build.phase0.spec import ( # functions get_active_validator_indices, get_balance, + get_beacon_proposer_index, get_block_root, get_current_epoch, get_domain, @@ -41,6 +42,7 @@ from tests.phase0.helpers import ( build_empty_block_for_next_slot, force_registry_change_at_next_epoch, get_valid_attestation, + get_valid_attester_slashing, get_valid_proposer_slashing, privkeys, pubkeys, @@ -141,6 +143,39 @@ def test_proposer_slashing(state): return state, [block], test_state +def test_attester_slashing(state): + test_state = deepcopy(state) + attester_slashing = get_valid_attester_slashing(state) + validator_index = attester_slashing.attestation_1.custody_bit_0_indices[0] + + # + # Add to state via block transition + # + block = build_empty_block_for_next_slot(test_state) + block.body.attester_slashings.append(attester_slashing) + state_transition(test_state, block) + + assert not state.validator_registry[validator_index].initiated_exit + assert not state.validator_registry[validator_index].slashed + + slashed_validator = test_state.validator_registry[validator_index] + assert not slashed_validator.initiated_exit + 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(test_state, validator_index) < get_balance(state, validator_index) + + proposer_index = get_beacon_proposer_index(test_state, test_state.slot) + # gained whistleblower reward + assert ( + get_balance(test_state, proposer_index) > + get_balance(state, proposer_index) + ) + + return state, [block], test_state + + def test_deposit_in_block(state): pre_state = deepcopy(state) test_deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) diff --git a/utils/phase0/state_transition.py b/utils/phase0/state_transition.py index cfd941c42..2c420014f 100644 --- a/utils/phase0/state_transition.py +++ b/utils/phase0/state_transition.py @@ -22,31 +22,31 @@ def expected_deposit_count(state: BeaconState) -> int: ) -def process_transaction_type(state: BeaconState, - transactions: List[Any], - max_transactions: int, - tx_fn: Callable[[BeaconState, Any], None]) -> None: - assert len(transactions) <= max_transactions - for transaction in transactions: - tx_fn(state, transaction) +def process_operation_type(state: BeaconState, + operations: List[Any], + max_operations: int, + tx_fn: Callable[[BeaconState, Any], None]) -> None: + assert len(operations) <= max_operations + for operation in operations: + tx_fn(state, operation) -def process_transactions(state: BeaconState, block: BeaconBlock) -> None: - process_transaction_type( +def process_operations(state: BeaconState, block: BeaconBlock) -> None: + process_operation_type( state, block.body.proposer_slashings, spec.MAX_PROPOSER_SLASHINGS, spec.process_proposer_slashing, ) - process_transaction_type( + process_operation_type( state, block.body.attester_slashings, spec.MAX_ATTESTER_SLASHINGS, spec.process_attester_slashing, ) - process_transaction_type( + process_operation_type( state, block.body.attestations, spec.MAX_ATTESTATIONS, @@ -54,14 +54,14 @@ def process_transactions(state: BeaconState, block: BeaconBlock) -> None: ) assert len(block.body.deposits) == expected_deposit_count(state) - process_transaction_type( + process_operation_type( state, block.body.deposits, spec.MAX_DEPOSITS, spec.process_deposit, ) - process_transaction_type( + process_operation_type( state, block.body.voluntary_exits, spec.MAX_VOLUNTARY_EXITS, @@ -69,7 +69,7 @@ def process_transactions(state: BeaconState, block: BeaconBlock) -> None: ) assert len(block.body.transfers) == len(set(block.body.transfers)) - process_transaction_type( + process_operation_type( state, block.body.transfers, spec.MAX_TRANSFERS, @@ -84,7 +84,7 @@ def process_block(state: BeaconState, spec.process_randao(state, block) spec.process_eth1_data(state, block) - process_transactions(state, block) + process_operations(state, block) if verify_state_root: spec.verify_block_state_root(state, block)