From 93249aadda184289b716484399b00797a7cb7bea Mon Sep 17 00:00:00 2001 From: protolambda Date: Tue, 4 Feb 2020 13:56:32 +0100 Subject: [PATCH 01/39] Proposal to focus on length-encoding SSZ contents, enable streaming of chunk contents, and put stricter DOS limits in place --- specs/phase0/p2p-interface.md | 44 +++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 674f2e2b8..8ac00cf06 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -403,9 +403,28 @@ The token of the negotiated protocol ID specifies the type of encoding to be use #### SSZ-encoding strategy (with or without Snappy) -The [SimpleSerialize (SSZ) specification](../../ssz/simple-serialize.md) outlines how objects are SSZ-encoded. If the Snappy variant is selected, we feed the serialized form to the Snappy compressor on encoding. The inverse happens on decoding. +The [SimpleSerialize (SSZ) specification](../../ssz/simple-serialize.md) outlines how objects are SSZ-encoded. -**Encoding-dependent header:** Req/Resp protocols using the `ssz` or `ssz_snappy` encoding strategies MUST prefix all encoded and compressed (if applicable) payloads with an unsigned [protobuf varint](https://developers.google.com/protocol-buffers/docs/encoding#varints). +If the Snappy variant is selected, we feed the serialized form of the object to the Snappy compressor on encoding. The inverse happens on decoding. + +Snappy has two formats: "block" and "frames" (streaming). To support large requests and response chunks, snappy-framing is used. + +Since snappy frame contents [have a maximum size of `65536` bytes](https://github.com/google/snappy/blob/master/framing_format.txt#L104) + and frame headers are just `identifier (1) + checksum (4)` bytes, the expected buffering of a single frame is acceptable. + +**Encoding-dependent header:** Req/Resp protocols using the `ssz` or `ssz_snappy` encoding strategies MUST encode the length of the raw SSZ bytes, encoded as an unsigned [protobuf varint](https://developers.google.com/protocol-buffers/docs/encoding#varints). + +*Writing*: By first computing and writing the SSZ byte length the SSZ encoder can then directly write the chunk contents to the stream. +If Snappy is applied, it can be passed through a buffered Snappy writer to compress frame by frame. + +*Reading*: After reading the expected SSZ byte length, the SSZ decoder can directly read the contents from the stream. +If snappy is applied, it can be passed through a buffered Snappy reader to decompress frame by frame. + +A reader: +- SHOULD not read more than `max_encoded_len(n)` bytes (`32 + n + n/6`) after reading the SSZ length prefix `n` from the header, [this is considered the worst-case compression result by Snappy](https://github.com/google/snappy/blob/537f4ad6240e586970fe554614542e9717df7902/snappy.cc#L98). +- SHOULD not accept a SSZ length prefix that is bigger than the expected maximum length for the SSZ type (derived from SSZ type information such as vector lengths and list limits). +- MUST consider remaining bytes, after having read the `n` SSZ bytes, as an invalid input. An EOF is expected. +- MUST consider an early EOF, before fully reading the declared length prefix worth of SSZ bytes, as an invalid input. All messages that contain only a single field MUST be encoded directly as the type of that field and MUST NOT be encoded as an SSZ container. @@ -829,23 +848,14 @@ Requests are segregated by protocol ID to: We are using single-use streams where each stream is closed at the end of the message. Thus, libp2p transparently handles message delimiting in the underlying stream. libp2p streams are full-duplex, and each party is responsible for closing their write side (like in TCP). We can therefore use stream closure to mark the end of the request and response independently. -Nevertheless, messages are still length-prefixed—this is now being considered for removal. - -Advantages of length-prefixing include: - -* Reader can prepare a correctly sized buffer before reading message +Nevertheless, in the case of `ssz` and `ssz_snappy`, messages are still length-prefixed with the length of the underlying data: +* A basic reader can prepare a correctly sized buffer before reading the message +* A more advanced reader can stream-decode SSZ given the length of the SSZ data. * Alignment with protocols like gRPC over HTTP/2 that prefix with length -* Sanity checking of stream closure / message length +* Sanity checking of message length, and enabling much stricter message length limiting based on SSZ type information, + to provide even more DOS protection than the global message length already does. E.g. a small `Status` message does not nearly require `MAX_CHUNK_SIZE` bytes. -Disadvantages include: - -* Redundant methods of message delimiting—both stream end marker and length prefix -* Harder to stream as length must be known up-front -* Additional code path required to verify length - -In some protocols, adding a length prefix serves as a form of DoS protection against very long messages, allowing the client to abort if an overlong message is about to be sent. In this protocol, we are globally limiting message sizes using `MAX_CHUNK_SIZE`, thus the length prefix does not afford any additional protection. - -[Protobuf varint](https://developers.google.com/protocol-buffers/docs/encoding#varints) is an efficient technique to encode variable-length ints. Instead of reserving a fixed-size field of as many bytes as necessary to convey the maximum possible value, this field is elastic in exchange for 1-bit overhead per byte. +[Protobuf varint](https://developers.google.com/protocol-buffers/docs/encoding#varints) is an efficient technique to encode variable-length (unsigned here) ints. Instead of reserving a fixed-size field of as many bytes as necessary to convey the maximum possible value, this field is elastic in exchange for 1-bit overhead per byte. ### Why do we version protocol strings with ordinals instead of semver? From 7ad710e2f36f50a6df32128a2f92fad5dee81131 Mon Sep 17 00:00:00 2001 From: nathaniel gentile Date: Sat, 15 Feb 2020 15:40:06 -0700 Subject: [PATCH 02/39] fix dev install example distutil command the option is now --spec-fork, not --spec-version see: 340549aed62caa15a9f04cf393805c2003bd5c8e --- tests/core/pyspec/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/pyspec/README.md b/tests/core/pyspec/README.md index 1f0bb6642..a9ee80105 100644 --- a/tests/core/pyspec/README.md +++ b/tests/core/pyspec/README.md @@ -20,7 +20,7 @@ Unlike the regular install, this outputs spec files to their original source loc Alternatively, you can build a sub-set of the pyspec with the distutil command: ```bash -python setup.py pyspec --spec-version=phase0 --md-doc-paths="specs/phase0/beacon-chain.md specs/phase0/fork-choice.md" --out-dir=my_spec_dir +python setup.py pyspec --spec-fork=phase0 --md-doc-paths="specs/phase0/beacon-chain.md specs/phase0/fork-choice.md" --out-dir=my_spec_dir ``` ## Py-tests From 52b45ab9de7a08c5a218a44d417aa7ba31e213ea Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Mon, 17 Feb 2020 10:03:32 +0100 Subject: [PATCH 03/39] Add fork version to topic Gossipsub peers are separate from the ETH2 RPC protocol, and thus cannot rely on the application-level `Status` negotiation to establish if they're on the same network. Segregating gossipsub topics by fork version decouples RPC from gossip further and allows peers to more easily listen only to the traffic of the network they're interested in, without further negotiation. --- specs/phase0/p2p-interface.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 7ad8759d3..40c9055ac 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -216,7 +216,17 @@ The following gossipsub [parameters](https://github.com/libp2p/specs/tree/master ### Topics and messages -Topics are plain UTF-8 strings and are encoded on the wire as determined by protobuf (gossipsub messages are enveloped in protobuf messages). Topic strings have form: `/eth2/TopicName/TopicEncoding`. This defines both the type of data being sent on the topic and how the data field of the message is encoded. +Topics are plain UTF-8 strings and are encoded on the wire as determined by protobuf (gossipsub messages are enveloped in protobuf messages). Topic strings have form: `/eth2/ForkVersion/Name/Encoding`. This defines both the type of data being sent on the topic and how the data field of the message is encoded. + +- `ForkVersion` - the hex-encoded bytes from `state.fork.current_version` of the head state of the client, as also seen in `Status.head_fork_version`. +- `Name` - see table below +- `Encoding` - the encoding strategy describes a specific representation of bytes that will be transmitted over the wire. See the [Encodings](#Encoding-strategies) section for further details. + +The fork version is hex-encoded using the following scheme: +```python + ForkVersion = ''.join('{:02x}'.format(x) for x in state.fork.current_version) +``` +For example, the fork version `[0, 1, 2, 10]` will be encoded as `0001020a`. Each gossipsub [message](https://github.com/libp2p/go-libp2p-pubsub/blob/master/pb/rpc.proto#L17-L24) has a maximum size of `GOSSIP_MAX_SIZE`. Clients MUST reject (fail validation) messages that are over this size limit. Likewise, clients MUST NOT emit or propagate messages larger than this limit. @@ -229,7 +239,7 @@ where `base64` is the [URL-safe base64 alphabet](https://tools.ietf.org/html/rfc The payload is carried in the `data` field of a gossipsub message, and varies depending on the topic: -| Topic | Message Type | +| Name | Message Type | |------------------------------------------------|-------------------------| | beacon_block | SignedBeaconBlock | | beacon_aggregate_and_proof | SignedAggregateAndProof | From cfcb7b2f015ddf637dae03b7bf262a6d8349a9e0 Mon Sep 17 00:00:00 2001 From: Anton Nashatyrev Date: Tue, 18 Feb 2020 16:12:43 +0300 Subject: [PATCH 04/39] Measure eth1 voting period in epochs instead of slots --- specs/phase0/beacon-chain.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index acf098663..0bf953c43 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -213,7 +213,7 @@ The following values are (non-configurable) constants used throughout the specif | `MIN_SEED_LOOKAHEAD` | `2**0` (= 1) | epochs | 6.4 minutes | | `MAX_SEED_LOOKAHEAD` | `2**2` (= 4) | epochs | 25.6 minutes | | `MIN_EPOCHS_TO_INACTIVITY_PENALTY` | `2**2` (= 4) | epochs | 25.6 minutes | -| `SLOTS_PER_ETH1_VOTING_PERIOD` | `2**10` (= 1,024) | slots | ~3.4 hours | +| `EPOCHS_PER_ETH1_VOTING_PERIOD` | `2**5` (= 32) | epochs | ~3.4 hours | | `SLOTS_PER_HISTORICAL_ROOT` | `2**13` (= 8,192) | slots | ~27 hours | | `MIN_VALIDATOR_WITHDRAWABILITY_DELAY` | `2**8` (= 256) | epochs | ~27 hours | | `PERSISTENT_COMMITTEE_PERIOD` | `2**11` (= 2,048) | epochs | 9 days | @@ -478,7 +478,7 @@ class BeaconState(Container): historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] # Eth1 eth1_data: Eth1Data - eth1_data_votes: List[Eth1Data, SLOTS_PER_ETH1_VOTING_PERIOD] + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index: uint64 # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] @@ -1397,7 +1397,7 @@ def process_final_updates(state: BeaconState) -> None: current_epoch = get_current_epoch(state) next_epoch = Epoch(current_epoch + 1) # Reset eth1 data votes - if (state.slot + 1) % SLOTS_PER_ETH1_VOTING_PERIOD == 0: + if next_epoch % EPOCHS_PER_ETH1_VOTING_PERIOD == 0: state.eth1_data_votes = [] # Update effective balances with hysteresis for index, validator in enumerate(state.validators): @@ -1468,7 +1468,7 @@ def process_randao(state: BeaconState, body: BeaconBlockBody) -> None: ```python def process_eth1_data(state: BeaconState, body: BeaconBlockBody) -> None: state.eth1_data_votes.append(body.eth1_data) - if state.eth1_data_votes.count(body.eth1_data) * 2 > SLOTS_PER_ETH1_VOTING_PERIOD: + if state.eth1_data_votes.count(body.eth1_data) * 2 > EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH: state.eth1_data = body.eth1_data ``` From 757f5a31ddad30381f53ca07bddab0a3156b3aec Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 17 Feb 2020 12:02:24 -0700 Subject: [PATCH 05/39] 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 a02aac43c290b82d0d647e7a54e439257fdab1bf Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 18 Feb 2020 12:36:20 -0600 Subject: [PATCH 06/39] adjust hysteresis to avoid initial over-deposit incentive --- specs/phase0/beacon-chain.md | 7 +++++-- .../test_process_final_updates.py | 19 +++++++++++-------- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index acf098663..2e6c64be5 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -1402,8 +1402,11 @@ def process_final_updates(state: BeaconState) -> None: # Update effective balances with hysteresis for index, validator in enumerate(state.validators): balance = state.balances[index] - HALF_INCREMENT = EFFECTIVE_BALANCE_INCREMENT // 2 - if balance < validator.effective_balance or validator.effective_balance + 3 * HALF_INCREMENT < balance: + QUARTER_INCREMENT = EFFECTIVE_BALANCE_INCREMENT // 4 + if ( + balance + QUARTER_INCREMENT < validator.effective_balance + or validator.effective_balance + 7 * QUARTER_INCREMENT < balance + ): validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) # Reset slashings state.slashings[next_epoch % EPOCHS_PER_SLASHINGS_VECTOR] = Gwei(0) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py index 58882a44f..b0d05427a 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py @@ -51,19 +51,22 @@ def test_effective_balance_hysteresis(spec, state): max = spec.MAX_EFFECTIVE_BALANCE min = spec.EJECTION_BALANCE inc = spec.EFFECTIVE_BALANCE_INCREMENT - half_inc = inc // 2 + quar_inc = inc // 4 cases = [ (max, max, max, "as-is"), - (max, max - 1, max - inc, "round down, step lower"), + (max, max - 1, max, "round up"), (max, max + 1, max, "round down"), + (max, max - quar_inc, max, "lower balance, but not low enough"), + (max, max - quar_inc, max, "lower balance, step down"), + (max, max + (3 * quar_inc) + 1, max, "already at max, as is"), (max, max - inc, max - inc, "exactly 1 step lower"), - (max, max - inc - 1, max - (2 * inc), "just 1 over 1 step lower"), + (max, max - inc - 1, max - (2 * inc), "past 1 step lower, double step"), (max, max - inc + 1, max - inc, "close to 1 step lower"), - (min, min + (half_inc * 3), min, "bigger balance, but not high enough"), - (min, min + (half_inc * 3) + 1, min + inc, "bigger balance, high enough, but small step"), - (min, min + (half_inc * 4) - 1, min + inc, "bigger balance, high enough, close to double step"), - (min, min + (half_inc * 4), min + (2 * inc), "exact two step balance increment"), - (min, min + (half_inc * 4) + 1, min + (2 * inc), "over two steps, round down"), + (min, min + (quar_inc * 7), min, "bigger balance, but not high enough"), + (min, min + (quar_inc * 7) + 1, min + inc, "bigger balance, high enough, but small step"), + (min, min + (quar_inc * 8) - 1, min + inc, "bigger balance, high enough, close to double step"), + (min, min + (quar_inc * 8), min + (2 * inc), "exact two step balance increment"), + (min, min + (quar_inc * 8) + 1, min + (2 * inc), "over two steps, round down"), ] current_epoch = spec.get_current_epoch(state) for i, (pre_eff, bal, _, _) in enumerate(cases): From 71be8940b60dcf6958907f796666464acc51df5b Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 18 Feb 2020 12:56:37 -0600 Subject: [PATCH 07/39] 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): From 61f661b3ecbc3e2895c4400a9f1362ae43fb6441 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Thu, 20 Feb 2020 08:23:09 +0100 Subject: [PATCH 08/39] Update specs/phase0/p2p-interface.md Co-Authored-By: Danny Ryan --- specs/phase0/p2p-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 40c9055ac..c5607c0c5 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -226,7 +226,7 @@ The fork version is hex-encoded using the following scheme: ```python ForkVersion = ''.join('{:02x}'.format(x) for x in state.fork.current_version) ``` -For example, the fork version `[0, 1, 2, 10]` will be encoded as `0001020a`. +For example, the fork version `Version('0x0001020a')` will be encoded as `0001020a`. Each gossipsub [message](https://github.com/libp2p/go-libp2p-pubsub/blob/master/pb/rpc.proto#L17-L24) has a maximum size of `GOSSIP_MAX_SIZE`. Clients MUST reject (fail validation) messages that are over this size limit. Likewise, clients MUST NOT emit or propagate messages larger than this limit. From 4d72dcf3abf80511b496d5b7c1540cb4e55e810e Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Thu, 27 Feb 2020 12:00:55 -0600 Subject: [PATCH 09/39] @hwwhww feedback Co-Authored-By: Hsiao-Wei Wang --- specs/phase0/p2p-interface.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 8ac00cf06..06e8191cd 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -414,15 +414,15 @@ Since snappy frame contents [have a maximum size of `65536` bytes](https://githu **Encoding-dependent header:** Req/Resp protocols using the `ssz` or `ssz_snappy` encoding strategies MUST encode the length of the raw SSZ bytes, encoded as an unsigned [protobuf varint](https://developers.google.com/protocol-buffers/docs/encoding#varints). -*Writing*: By first computing and writing the SSZ byte length the SSZ encoder can then directly write the chunk contents to the stream. +*Writing*: By first computing and writing the SSZ byte length, the SSZ encoder can then directly write the chunk contents to the stream. If Snappy is applied, it can be passed through a buffered Snappy writer to compress frame by frame. *Reading*: After reading the expected SSZ byte length, the SSZ decoder can directly read the contents from the stream. If snappy is applied, it can be passed through a buffered Snappy reader to decompress frame by frame. A reader: -- SHOULD not read more than `max_encoded_len(n)` bytes (`32 + n + n/6`) after reading the SSZ length prefix `n` from the header, [this is considered the worst-case compression result by Snappy](https://github.com/google/snappy/blob/537f4ad6240e586970fe554614542e9717df7902/snappy.cc#L98). -- SHOULD not accept a SSZ length prefix that is bigger than the expected maximum length for the SSZ type (derived from SSZ type information such as vector lengths and list limits). +- SHOULD NOT read more than `max_encoded_len(n)` bytes (`32 + n + n // 6`) after reading the SSZ length prefix `n` from the header, [this is considered the worst-case compression result by Snappy](https://github.com/google/snappy/blob/537f4ad6240e586970fe554614542e9717df7902/snappy.cc#L98). +- SHOULD NOT accept an SSZ length prefix that is bigger than the expected maximum length for the SSZ type (derived from SSZ type information such as vector lengths and list limits). - MUST consider remaining bytes, after having read the `n` SSZ bytes, as an invalid input. An EOF is expected. - MUST consider an early EOF, before fully reading the declared length prefix worth of SSZ bytes, as an invalid input. From bb82a051ff439b466ffe66ce374968778f82bdec Mon Sep 17 00:00:00 2001 From: protolambda Date: Thu, 27 Feb 2020 19:39:34 +0000 Subject: [PATCH 10/39] clean up, add invalid input handling --- specs/phase0/p2p-interface.md | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 06e8191cd..377f98869 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -420,11 +420,20 @@ If Snappy is applied, it can be passed through a buffered Snappy writer to compr *Reading*: After reading the expected SSZ byte length, the SSZ decoder can directly read the contents from the stream. If snappy is applied, it can be passed through a buffered Snappy reader to decompress frame by frame. -A reader: -- SHOULD NOT read more than `max_encoded_len(n)` bytes (`32 + n + n // 6`) after reading the SSZ length prefix `n` from the header, [this is considered the worst-case compression result by Snappy](https://github.com/google/snappy/blob/537f4ad6240e586970fe554614542e9717df7902/snappy.cc#L98). -- SHOULD NOT accept an SSZ length prefix that is bigger than the expected maximum length for the SSZ type (derived from SSZ type information such as vector lengths and list limits). -- MUST consider remaining bytes, after having read the `n` SSZ bytes, as an invalid input. An EOF is expected. -- MUST consider an early EOF, before fully reading the declared length prefix worth of SSZ bytes, as an invalid input. +A reader SHOULD NOT read more than `max_encoded_len(n)` bytes after reading the SSZ length prefix `n` from the header. +- For `ssz` this is: `n` +- For `ssz_snappy` this is: `32 + n + n // 6`. This is considered the [worst-case compression result](https://github.com/google/snappy/blob/537f4ad6240e586970fe554614542e9717df7902/snappy.cc#L98) by Snappy. + +A reader SHOULD consider the following cases as invalid input: +- A SSZ length prefix that, compared against the SSZ type information (vector lengths, list limits, integer sizes, etc.), is: + - Smaller than the expected minimum serialized length. + - Bigger than the expected maximum serialized length. +- Any remaining bytes, after having read the `n` SSZ bytes. An EOF is expected. +- An early EOF, before fully reading the declared length prefix worth of SSZ bytes. + +In case of an invalid input, a reader MUST: +- From requests: send back an error message, response code `InvalidRequest`. The request itself is ignored. +- From responses: ignore the response, the response MUST be considered bad server behavior. All messages that contain only a single field MUST be encoded directly as the type of that field and MUST NOT be encoded as an SSZ container. From 38323d8186793ba1cbbfa6c8000a4a4b60aa97b9 Mon Sep 17 00:00:00 2001 From: Jacek Sieka Date: Sun, 1 Mar 2020 17:17:29 +0100 Subject: [PATCH 11/39] Add faq --- specs/phase0/p2p-interface.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index c5607c0c5..85d9a1e9f 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -763,9 +763,9 @@ For future extensibility with almost zero overhead now (besides the extra bytes ### How do we upgrade gossip channels (e.g. changes in encoding, compression)? -Changing gossipsub/broadcasts requires a coordinated upgrade where all clients start publishing to the new topic together, for example during a hard fork. +Changing gossipsub/broadcasts requires a coordinated upgrade where all clients start publishing to the new topic together, during a hard fork. -One can envision a two-phase deployment as well where clients start listening to the new topic in the first phase then start publishing some time later, letting the traffic naturally move over to the new topic. +When a node is preparing for upcoming tasks (e.g. validator duty lookahead) on a gossipsub topic, the node should join the topic of the future epoch in which the task is to occur in addition to listening to the topics for the current epoch. ### Why must all clients use the same gossip topic instead of one negotiated between each peer pair? From 0122081d05cea01dbe70ceec383a8fc75b414f3c Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 2 Mar 2020 15:55:01 -0700 Subject: [PATCH 12/39] hysteresis to -0.25/+1.25 --- specs/phase0/beacon-chain.md | 2 +- .../phase_0/epoch_processing/test_process_final_updates.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 2e6c64be5..aa573ff16 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -1405,7 +1405,7 @@ def process_final_updates(state: BeaconState) -> None: QUARTER_INCREMENT = EFFECTIVE_BALANCE_INCREMENT // 4 if ( balance + QUARTER_INCREMENT < validator.effective_balance - or validator.effective_balance + 7 * QUARTER_INCREMENT < balance + or validator.effective_balance + 5 * QUARTER_INCREMENT < balance ): validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) # Reset slashings diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py index b0d05427a..36dae8fb7 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py @@ -62,8 +62,8 @@ def test_effective_balance_hysteresis(spec, state): (max, max - inc, max - inc, "exactly 1 step lower"), (max, max - inc - 1, max - (2 * inc), "past 1 step lower, double step"), (max, max - inc + 1, max - inc, "close to 1 step lower"), - (min, min + (quar_inc * 7), min, "bigger balance, but not high enough"), - (min, min + (quar_inc * 7) + 1, min + inc, "bigger balance, high enough, but small step"), + (min, min + (quar_inc * 5), min, "bigger balance, but not high enough"), + (min, min + (quar_inc * 5) + 1, min + inc, "bigger balance, high enough, but small step"), (min, min + (quar_inc * 8) - 1, min + inc, "bigger balance, high enough, close to double step"), (min, min + (quar_inc * 8), min + (2 * inc), "exact two step balance increment"), (min, min + (quar_inc * 8) + 1, min + (2 * inc), "over two steps, round down"), From b4c7481b35260587eb00c00f2da3b477f1780a85 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 3 Mar 2020 01:28:58 +0100 Subject: [PATCH 13/39] Fix the misc table --- specs/phase1/custody-game.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/phase1/custody-game.md b/specs/phase1/custody-game.md index 121f91f97..af3aadc96 100644 --- a/specs/phase1/custody-game.md +++ b/specs/phase1/custody-game.md @@ -48,8 +48,8 @@ This document details the beacon chain additions and changes in Phase 1 of Ether ### Misc | Name | Value | Unit | -| - | - | -| `BLS12_381_Q` | `4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787` | +| - | - | - | +| `BLS12_381_Q` | `4002409555221667393417789825735904156556882819939007885332058136124031650490837864442687629129015664037894272559787` | - | | `BYTES_PER_CUSTODY_ATOM` | `48` | bytes | ## Configuration From 2d4ec7d52f0759b5e1d2ea8337d369e300a8f301 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 3 Mar 2020 09:29:59 -0700 Subject: [PATCH 14/39] add REWARD_OVERFLOW_INCREMENT to avoid overflow in rewards calculation --- specs/phase0/beacon-chain.md | 5 ++++- .../epoch_processing/test_process_rewards_and_penalties.py | 7 +++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index acf098663..7c14550bb 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -194,6 +194,7 @@ The following values are (non-configurable) constants used throughout the specif | `MAX_EFFECTIVE_BALANCE` | `Gwei(2**5 * 10**9)` (= 32,000,000,000) | | `EJECTION_BALANCE` | `Gwei(2**4 * 10**9)` (= 16,000,000,000) | | `EFFECTIVE_BALANCE_INCREMENT` | `Gwei(2**0 * 10**9)` (= 1,000,000,000) | +| `REWARD_OVERFLOW_INCREMENT` | `Gwei(2**6)` (= 64) | ### Initial values @@ -1313,7 +1314,9 @@ def get_attestation_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence attesting_balance = get_total_balance(state, unslashed_attesting_indices) for index in eligible_validator_indices: if index in unslashed_attesting_indices: - rewards[index] += get_base_reward(state, index) * attesting_balance // total_balance + increment = REWARD_OVERFLOW_INCREMENT # Factored out from reward numerator to avoid uint64 overflow + reward_numerator = get_base_reward(state, index) // increment * attesting_balance + rewards[index] = reward_numerator // total_balance * increment else: penalties[index] += get_base_reward(state, index) diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py index fa394df56..b4f50179e 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py @@ -1,7 +1,10 @@ from copy import deepcopy -from eth2spec.test.context import spec_state_test, with_all_phases, spec_test, \ - misc_balances, with_custom_state, default_activation_threshold, single_phase +from eth2spec.test.context import ( + spec_state_test, with_all_phases, spec_test, + misc_balances, with_custom_state, default_activation_threshold, + single_phase, +) from eth2spec.test.helpers.state import ( next_epoch, next_slot, From 33e768083679999d4ec4b289c9a15b37c7f101dd Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 3 Mar 2020 10:58:47 -0700 Subject: [PATCH 15/39] make hysteresis calculations configurable --- configs/mainnet.yaml | 6 ++++++ configs/minimal.yaml | 7 +++++++ specs/phase0/beacon-chain.md | 12 ++++++++--- .../test_process_final_updates.py | 21 +++++++++++-------- 4 files changed, 34 insertions(+), 12 deletions(-) diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 74f062d9b..46977f087 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -21,6 +21,12 @@ SHUFFLE_ROUND_COUNT: 90 MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 16384 # Jan 3, 2020 MIN_GENESIS_TIME: 1578009600 +# 4 +HYSTERESIS_QUOTIENT: 4 +# 1 (minus 0.25) +HYSTERESIS_DOWNWARD_MULTIPLIER: 1 +# 5 (plus 1.25) +HYSTERESIS_UPWARD_MULTIPLIER: 5 # Fork Choice diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 42c63e301..202da8237 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -20,6 +20,13 @@ SHUFFLE_ROUND_COUNT: 10 MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 64 # Jan 3, 2020 MIN_GENESIS_TIME: 1578009600 +# 4 +HYSTERESIS_QUOTIENT: 4 +# 1 (minus 0.25) +HYSTERESIS_DOWNWARD_MULTIPLIER: 1 +# 5 (plus 1.25) +HYSTERESIS_UPWARD_MULTIPLIER: 5 + # Fork Choice diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index aa573ff16..c7a76d066 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -183,6 +183,10 @@ The following values are (non-configurable) constants used throughout the specif | `SHUFFLE_ROUND_COUNT` | `90` | | `MIN_GENESIS_ACTIVE_VALIDATOR_COUNT` | `2**14` (= 16,384) | | `MIN_GENESIS_TIME` | `1578009600` (Jan 3, 2020) | +| `HYSTERESIS_QUOTIENT` | `4` | +| `HYSTERESIS_DOWNWARD_MULTIPLIER` | `1` | +| `HYSTERESIS_UPWARD_MULTIPLIER` | `5` | + - For the safety of committees, `TARGET_COMMITTEE_SIZE` exceeds [the recommended minimum committee size of 111](http://web.archive.org/web/20190504131341/https://vitalik.ca/files/Ithaca201807_Sharding.pdf); with sufficient active validators (at least `SLOTS_PER_EPOCH * TARGET_COMMITTEE_SIZE`), the shuffling algorithm ensures committee sizes of at least `TARGET_COMMITTEE_SIZE`. (Unbiasable randomness with a Verifiable Delay Function (VDF) will improve committee robustness and lower the safe minimum committee size.) @@ -1402,10 +1406,12 @@ def process_final_updates(state: BeaconState) -> None: # Update effective balances with hysteresis for index, validator in enumerate(state.validators): balance = state.balances[index] - QUARTER_INCREMENT = EFFECTIVE_BALANCE_INCREMENT // 4 + HYSTERESIS_INCREMENT = EFFECTIVE_BALANCE_INCREMENT // HYSTERESIS_QUOTIENT + DOWNWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_DOWNWARD_MULTIPLIER + UPWARD_THRESHOLD = HYSTERESIS_INCREMENT * HYSTERESIS_UPWARD_MULTIPLIER if ( - balance + QUARTER_INCREMENT < validator.effective_balance - or validator.effective_balance + 5 * QUARTER_INCREMENT < balance + balance + DOWNWARD_THRESHOLD < validator.effective_balance + or validator.effective_balance + UPWARD_THRESHOLD < balance ): validator.effective_balance = min(balance - balance % EFFECTIVE_BALANCE_INCREMENT, MAX_EFFECTIVE_BALANCE) # Reset slashings diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py index 36dae8fb7..d959ce16e 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py @@ -51,22 +51,25 @@ def test_effective_balance_hysteresis(spec, state): max = spec.MAX_EFFECTIVE_BALANCE min = spec.EJECTION_BALANCE inc = spec.EFFECTIVE_BALANCE_INCREMENT - quar_inc = inc // 4 + div = spec.HYSTERESIS_QUOTIENT + hys_inc = inc // div + down = spec.HYSTERESIS_DOWNWARD_MULTIPLIER + up = spec.HYSTERESIS_UPWARD_MULTIPLIER cases = [ (max, max, max, "as-is"), (max, max - 1, max, "round up"), (max, max + 1, max, "round down"), - (max, max - quar_inc, max, "lower balance, but not low enough"), - (max, max - quar_inc, max, "lower balance, step down"), - (max, max + (3 * quar_inc) + 1, max, "already at max, as is"), + (max, max - down * hys_inc, max, "lower balance, but not low enough"), + (max, max - down * hys_inc - 1, max - inc, "lower balance, step down"), + (max, max + (up * hys_inc) + 1, max, "already at max, as is"), (max, max - inc, max - inc, "exactly 1 step lower"), (max, max - inc - 1, max - (2 * inc), "past 1 step lower, double step"), (max, max - inc + 1, max - inc, "close to 1 step lower"), - (min, min + (quar_inc * 5), min, "bigger balance, but not high enough"), - (min, min + (quar_inc * 5) + 1, min + inc, "bigger balance, high enough, but small step"), - (min, min + (quar_inc * 8) - 1, min + inc, "bigger balance, high enough, close to double step"), - (min, min + (quar_inc * 8), min + (2 * inc), "exact two step balance increment"), - (min, min + (quar_inc * 8) + 1, min + (2 * inc), "over two steps, round down"), + (min, min + (hys_inc * up), min, "bigger balance, but not high enough"), + (min, min + (hys_inc * up) + 1, min + inc, "bigger balance, high enough, but small step"), + (min, min + (hys_inc * div * 2) - 1, min + inc, "bigger balance, high enough, close to double step"), + (min, min + (hys_inc * div * 2), min + (2 * inc), "exact two step balance increment"), + (min, min + (hys_inc * div * 2) + 1, min + (2 * inc), "over two steps, round down"), ] current_epoch = spec.get_current_epoch(state) for i, (pre_eff, bal, _, _) in enumerate(cases): From f082aa6ca9dfac0b1e03fe91b0cdf1e8886f3199 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 3 Mar 2020 15:34:02 -0700 Subject: [PATCH 16/39] use EFFECTIVE_BALANCE_INCREMENT to normalize reward calculations --- specs/phase0/beacon-chain.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 7c14550bb..cd1d765fe 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -194,7 +194,6 @@ The following values are (non-configurable) constants used throughout the specif | `MAX_EFFECTIVE_BALANCE` | `Gwei(2**5 * 10**9)` (= 32,000,000,000) | | `EJECTION_BALANCE` | `Gwei(2**4 * 10**9)` (= 16,000,000,000) | | `EFFECTIVE_BALANCE_INCREMENT` | `Gwei(2**0 * 10**9)` (= 1,000,000,000) | -| `REWARD_OVERFLOW_INCREMENT` | `Gwei(2**6)` (= 64) | ### Initial values @@ -1314,9 +1313,9 @@ def get_attestation_deltas(state: BeaconState) -> Tuple[Sequence[Gwei], Sequence attesting_balance = get_total_balance(state, unslashed_attesting_indices) for index in eligible_validator_indices: if index in unslashed_attesting_indices: - increment = REWARD_OVERFLOW_INCREMENT # Factored out from reward numerator to avoid uint64 overflow - reward_numerator = get_base_reward(state, index) // increment * attesting_balance - rewards[index] = reward_numerator // total_balance * increment + increment = EFFECTIVE_BALANCE_INCREMENT # Factored out from balance totals to avoid uint64 overflow + reward_numerator = get_base_reward(state, index) * (attesting_balance // increment) + rewards[index] = reward_numerator // (total_balance // increment) else: penalties[index] += get_base_reward(state, index) From 5dae252f56f4e780dceb35476407d00a0ada597b Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 10 Feb 2020 15:53:10 -0700 Subject: [PATCH 17/39] add eth2 key/value ENR to phase 0 p2p --- specs/phase0/p2p-interface.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 7ad8759d3..651938a54 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -604,6 +604,12 @@ Nonetheless, ENRs MUST carry a generic `eth2` key with nil value, denoting that #### Mainnet +ENRs MUST carry a generic `eth2` with a 4-byte value of the node's current fork version to ensure connections are made with peers on the intended eth2 network. + +| Key | Value | +|:-------------|:--------------------| +| `eth2` | SSZ `Bytes4` | + On mainnet, ENRs MUST include a structure enumerating the capabilities offered by the peer in an efficient manner. The concrete solution is currently undefined. Proposals include using namespaced bloom filters mapping capabilities to specific protocol IDs supported under that capability. ### Topic advertisement From 37b1fed8ff03db4ad6d0113ea69551609067fbf6 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 4 Mar 2020 14:01:50 -0700 Subject: [PATCH 18/39] update eth2 ENR field to use ENRForkID --- specs/phase0/p2p-interface.md | 39 +++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 651938a54..c8d4bc1b4 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -55,6 +55,8 @@ It consists of four main sections: - [Attestation subnet bitfield](#attestation-subnet-bitfield) - [Interop](#interop-5) - [Mainnet](#mainnet-5) + - [`eth2` field](#eth2-field) + - [General capabilities](#general-capabilities) - [Topic advertisement](#topic-advertisement) - [Mainnet](#mainnet-6) - [Design decision rationale](#design-decision-rationale) @@ -88,6 +90,7 @@ It consists of four main sections: - [Why are we sending entire objects in the pubsub and not just hashes?](#why-are-we-sending-entire-objects-in-the-pubsub-and-not-just-hashes) - [Should clients gossip blocks if they *cannot* validate the proposer signature due to not yet being synced, not knowing the head block, etc?](#should-clients-gossip-blocks-if-they-cannot-validate-the-proposer-signature-due-to-not-yet-being-synced-not-knowing-the-head-block-etc) - [How are we going to discover peers in a gossipsub topic?](#how-are-we-going-to-discover-peers-in-a-gossipsub-topic) + - [How should fork version be used in practice?](#how-should-fork-version-be-used-in-practice) - [Req/Resp](#reqresp) - [Why segregate requests into dedicated protocol IDs?](#why-segregate-requests-into-dedicated-protocol-ids) - [Why are messages length-prefixed with a protobuf varint in the SSZ-encoding?](#why-are-messages-length-prefixed-with-a-protobuf-varint-in-the-ssz-encoding) @@ -604,11 +607,35 @@ Nonetheless, ENRs MUST carry a generic `eth2` key with nil value, denoting that #### Mainnet -ENRs MUST carry a generic `eth2` with a 4-byte value of the node's current fork version to ensure connections are made with peers on the intended eth2 network. +##### `eth2` field + +ENRs MUST carry a generic `eth2` key with an 16-byte value of the node's current fork version, next fork version, and next fork epoch to ensure connections are made with peers on the intended eth2 network. | Key | Value | |:-------------|:--------------------| -| `eth2` | SSZ `Bytes4` | +| `eth2` | SSZ `ENRForkID` | + +Specifically, the value of the `eth2` key MUST be the following SSZ encoded object (`ENRForkID`), where + +``` +( + current_fork_version: Fork + next_fork_version: Fork + next_fork_epoch: Epoch +) +``` + +where the fields of `ENRForkID` are defined as + +* `current_fork_version` is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync) +* `next_fork_version` is the fork version corresponding to the next planned hard fork at a future epoch. If no future fork is planned, set `next_fork_version = current_fork_version` to signal this fact +* `next_fork_epoch` is the epoch at which the next fork is planned and the `current_fork_version` will be updated. If no future fork is planned, set `next_fork_epoch = FAR_FUTURE_EPOCH` to signal this fact + +Clients SHOULD connect to peers with `current_fork_version`, `next_fork_version`, and `next_fork_epoch` that match local values. + +Clients MAY connect to peers with the same `current_fork_version` but a different `next_fork_version`/`next_fork_epoch`. Unless `ENRForkID` is manually updated to matching prior to the earlier `next_fork_epoch` of the two clients, these type of connecting clients will be unable to successfully interact starting at the earlier `next_fork_epoch`. + +##### General capabilities On mainnet, ENRs MUST include a structure enumerating the capabilities offered by the peer in an efficient manner. The concrete solution is currently undefined. Proposals include using namespaced bloom filters mapping capabilities to specific protocol IDs supported under that capability. @@ -829,6 +856,14 @@ In Phase 0, peers for attestation subnets will be found using the `attnets` entr Although this method will be sufficient for early phases of Eth2, we aim to use the more appropriate discv5 topics for this and other similar tasks in the future. ENRs should ultimately not be used for this purpose. They are best suited to store identity, location, and capability information, rather than more volatile advertisements. +### How should fork version be used in practice? + +Fork versions are to be manually updated (likely via incrementing or using the less collision-prone git spec hash) at each hard fork. This is to provide native domain separation for signatures as well as to aid in usefulness for identitying peers (via ENRs) and versioning network protocols (e.g. using fork version to naturally version gossipsub topics). + +To reap the full benefit of the native versioning scheme, networks SHOULD avoid collisions. For example, a testnet might us mainnet versioning but use a unique higher order byte to signal the testnet. + +A node locally stores all previous and future planned fork versions along with the each fork epoch. This allows for handling sync starting from past forks/epochs and for connections to safely be made with peers syncing from past forks/epochs. + ## Req/Resp ### Why segregate requests into dedicated protocol IDs? From 7e04989e29306c8d6049338457e3ba89c7959218 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Thu, 5 Mar 2020 09:21:32 -0700 Subject: [PATCH 19/39] add genesis_validators_root to beaconstate and utilize in sig domain separation as well as fork separation --- setup.py | 4 ++-- specs/phase0/beacon-chain.md | 14 +++++++---- specs/phase0/p2p-interface.md | 24 +++++++++++++++---- specs/phase1/beacon-chain.md | 1 + .../pyspec/eth2spec/test/helpers/genesis.py | 3 +++ 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 444489d2e..6dd4de861 100644 --- a/setup.py +++ b/setup.py @@ -95,7 +95,7 @@ from dataclasses import ( from eth2spec.utils.ssz.ssz_impl import hash_tree_root from eth2spec.utils.ssz.ssz_typing import ( View, boolean, Container, List, Vector, uint64, - Bytes1, Bytes4, Bytes8, Bytes32, Bytes48, Bytes96, Bitlist, Bitvector, + Bytes1, Bytes4, Bytes32, Bytes48, Bytes96, Bitlist, Bitvector, ) from eth2spec.utils import bls @@ -117,7 +117,7 @@ from dataclasses import ( from eth2spec.utils.ssz.ssz_impl import hash_tree_root from eth2spec.utils.ssz.ssz_typing import ( View, boolean, Container, List, Vector, uint64, uint8, bit, - ByteList, Bytes1, Bytes4, Bytes8, Bytes32, Bytes48, Bytes96, Bitlist, Bitvector, + ByteList, Bytes1, Bytes4, Bytes32, Bytes48, Bytes96, Bitlist, Bitvector, ) from eth2spec.utils import bls diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index c7a76d066..731b02b1a 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -149,7 +149,7 @@ We define the following Python custom types for type hinting and readability: | `Root` | `Bytes32` | a Merkle root | | `Version` | `Bytes4` | a fork version number | | `DomainType` | `Bytes4` | a domain type | -| `Domain` | `Bytes8` | a signature domain | +| `Domain` | `Bytes32` | a signature domain | | `BLSPubkey` | `Bytes48` | a BLS12-381 public key | | `BLSSignature` | `Bytes96` | a BLS12-381 signature | @@ -473,6 +473,7 @@ class BeaconBlock(Container): class BeaconState(Container): # Versioning genesis_time: uint64 + genesis_validators_root: Root slot: Slot fork: Fork # History @@ -795,13 +796,15 @@ def compute_activation_exit_epoch(epoch: Epoch) -> Epoch: #### `compute_domain` ```python -def compute_domain(domain_type: DomainType, fork_version: Optional[Version]=None) -> Domain: +def compute_domain(domain_type: DomainType, fork_version: Optional[Version]=None, genesis_root: Root=None) -> Domain: """ Return the domain for the ``domain_type`` and ``fork_version``. """ if fork_version is None: fork_version = GENESIS_FORK_VERSION - return Domain(domain_type + fork_version) + if genesis_root is None: + genesis_root = Root() + return Domain(domain_type + fork_version + genesis_root[:24]) ``` #### `compute_signing_root` @@ -977,7 +980,7 @@ def get_domain(state: BeaconState, domain_type: DomainType, epoch: Epoch=None) - """ epoch = get_current_epoch(state) if epoch is None else epoch fork_version = state.fork.previous_version if epoch < state.fork.epoch else state.fork.current_version - return compute_domain(domain_type, fork_version) + return compute_domain(domain_type, fork_version, state.genesis_validators_root) ``` #### `get_indexed_attestation` @@ -1122,6 +1125,9 @@ def initialize_beacon_state_from_eth1(eth1_block_hash: Bytes32, validator.activation_eligibility_epoch = GENESIS_EPOCH validator.activation_epoch = GENESIS_EPOCH + # Set genesis validators root for domain separation and chain versioning + state.genesis_validators_root = hash_tree_root(state.validators) + return state ``` diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index c8d4bc1b4..f2dcc2324 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -615,23 +615,37 @@ ENRs MUST carry a generic `eth2` key with an 16-byte value of the node's current |:-------------|:--------------------| | `eth2` | SSZ `ENRForkID` | -Specifically, the value of the `eth2` key MUST be the following SSZ encoded object (`ENRForkID`), where +First we define `current_fork` as the following SSZ encoded object ``` ( - current_fork_version: Fork - next_fork_version: Fork + current_fork_version: Version + genesis_validators_root: Root +) +``` + +where + +* `current_fork_version` is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync) +* `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root` + +Specifically, the value of the `eth2` key MUST be the following SSZ encoded object (`ENRForkID`) + +``` +( + current_fork_digest: Bytes4 + next_fork_version: Version next_fork_epoch: Epoch ) ``` where the fields of `ENRForkID` are defined as -* `current_fork_version` is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync) +* `current_fork_digest` is `hash_tree_root(current_fork)[:4]` * `next_fork_version` is the fork version corresponding to the next planned hard fork at a future epoch. If no future fork is planned, set `next_fork_version = current_fork_version` to signal this fact * `next_fork_epoch` is the epoch at which the next fork is planned and the `current_fork_version` will be updated. If no future fork is planned, set `next_fork_epoch = FAR_FUTURE_EPOCH` to signal this fact -Clients SHOULD connect to peers with `current_fork_version`, `next_fork_version`, and `next_fork_epoch` that match local values. +Clients SHOULD connect to peers with `current_fork_digest`, `next_fork_version`, and `next_fork_epoch` that match local values. Clients MAY connect to peers with the same `current_fork_version` but a different `next_fork_version`/`next_fork_epoch`. Unless `ENRForkID` is manually updated to matching prior to the earlier `next_fork_epoch` of the two clients, these type of connecting clients will be unable to successfully interact starting at the earlier `next_fork_epoch`. diff --git a/specs/phase1/beacon-chain.md b/specs/phase1/beacon-chain.md index 78b3b3d25..095bbafc1 100644 --- a/specs/phase1/beacon-chain.md +++ b/specs/phase1/beacon-chain.md @@ -244,6 +244,7 @@ Note that aside from the new additions, `Validator` and `PendingAttestation` hav class BeaconState(Container): # Versioning genesis_time: uint64 + genesis_validators_root: Root slot: Slot fork: Fork # History diff --git a/tests/core/pyspec/eth2spec/test/helpers/genesis.py b/tests/core/pyspec/eth2spec/test/helpers/genesis.py index c60787b92..46bc62fe5 100644 --- a/tests/core/pyspec/eth2spec/test/helpers/genesis.py +++ b/tests/core/pyspec/eth2spec/test/helpers/genesis.py @@ -43,4 +43,7 @@ def create_genesis_state(spec, validator_balances, activation_threshold): validator.activation_eligibility_epoch = spec.GENESIS_EPOCH validator.activation_epoch = spec.GENESIS_EPOCH + # Set genesis validators root for domain separation and chain versioning + state.genesis_validators_root = spec.hash_tree_root(state.validators) + return state From c5aca062b435825cb33731b19c5ce933eafe5016 Mon Sep 17 00:00:00 2001 From: Herman Junge Date: Mon, 9 Mar 2020 17:16:02 +0000 Subject: [PATCH 20/39] Update reference to Gasper paper --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3f58b3dc4..83dbbd3fe 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The following are the broad design goals for Ethereum 2.0: * [Design Rationale](https://notes.ethereum.org/s/rkhCgQteN#) * [Phase 0 Onboarding Document](https://notes.ethereum.org/s/Bkn3zpwxB) -* [Gasper paper](https://github.com/ethereum/research/blob/master/papers/ffg%2Bghost/paper.pdf) +* [Gasper paper](https://arxiv.org/abs/2003.03052) ## For spec contributors @@ -67,4 +67,3 @@ The following are the broad design goals for Ethereum 2.0: Documentation on the different components used during spec writing can be found here: * [YAML Test Generators](tests/generators/README.md) * [Executable Python Spec, with Py-tests](tests/core/pyspec/README.md) - From 7d4997f0afa1bee02d5fc9922ad3f09b8c446363 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Mon, 9 Mar 2020 12:03:12 -0600 Subject: [PATCH 21/39] bump version to v0.11.0 for coming release --- tests/core/pyspec/eth2spec/VERSION.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/pyspec/eth2spec/VERSION.txt b/tests/core/pyspec/eth2spec/VERSION.txt index 9f20097b6..142464bf2 100644 --- a/tests/core/pyspec/eth2spec/VERSION.txt +++ b/tests/core/pyspec/eth2spec/VERSION.txt @@ -1 +1 @@ -0.10.2.dev0 \ No newline at end of file +0.11.0 \ No newline at end of file From da5720f9d1c3e5e1314aa34278cd4856e042f92a Mon Sep 17 00:00:00 2001 From: Sly Gryphon Date: Tue, 10 Mar 2020 21:31:03 +1000 Subject: [PATCH 22/39] Put back in a requirement to store recent signed blocks that was removed when SignedBeaconBlock was introduced (prior to that the signature was in BeaconBlock, which was recorded in Store). --- specs/phase0/p2p-interface.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 8bde2b80a..b214c458d 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -547,7 +547,8 @@ Requests count beacon blocks from the peer starting from `start_slot`, leading u The request MUST be encoded as an SSZ-container. The response MUST consist of zero or more `response_chunk`. Each _successful_ `response_chunk` MUST contain a single `SignedBeaconBlock` payload. -Clients MUST support requesting blocks since the start of the weak subjectivity period and up to the given `head_block_root`. + +Clients MUST keep a record of signed blocks seen since the since the start of the weak subjectivity period and MUST support requesting blocks up to the given `head_block_root`. Clients MUST respond with at least one block, if they have it and it exists in the range. Clients MAY limit the number of blocks in the response. From 2d7a292d3663b398ecb222b873ac700726ddf57a Mon Sep 17 00:00:00 2001 From: protolambda Date: Tue, 10 Mar 2020 18:36:53 +0100 Subject: [PATCH 23/39] eth1 vote period constant in epochs: update configs, phase1, tests --- configs/mainnet.yaml | 4 +-- configs/minimal.yaml | 2 +- specs/phase0/validator.md | 4 +-- specs/phase1/beacon-chain.md | 2 +- .../test_process_final_updates.py | 4 +-- .../eth2spec/test/sanity/test_blocks.py | 26 +++++++++++-------- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/configs/mainnet.yaml b/configs/mainnet.yaml index 74f062d9b..69bbbff22 100644 --- a/configs/mainnet.yaml +++ b/configs/mainnet.yaml @@ -82,8 +82,8 @@ SLOTS_PER_EPOCH: 32 MIN_SEED_LOOKAHEAD: 1 # 2**2 (= 4) epochs 25.6 minutes MAX_SEED_LOOKAHEAD: 4 -# 2**10 (= 1,024) slots ~1.7 hours -SLOTS_PER_ETH1_VOTING_PERIOD: 1024 +# 2**5 (= 32) epochs ~3.4 hours +EPOCHS_PER_ETH1_VOTING_PERIOD: 32 # 2**13 (= 8,192) slots ~13 hours SLOTS_PER_HISTORICAL_ROOT: 8192 # 2**8 (= 256) epochs ~27 hours diff --git a/configs/minimal.yaml b/configs/minimal.yaml index 42c63e301..23ebbfbbc 100644 --- a/configs/minimal.yaml +++ b/configs/minimal.yaml @@ -82,7 +82,7 @@ MIN_SEED_LOOKAHEAD: 1 # 2**2 (= 4) epochs MAX_SEED_LOOKAHEAD: 4 # [customized] higher frequency new deposits from eth1 for testing -SLOTS_PER_ETH1_VOTING_PERIOD: 16 +EPOCHS_PER_ETH1_VOTING_PERIOD: 2 # [customized] smaller state SLOTS_PER_HISTORICAL_ROOT: 64 # 2**8 (= 256) epochs diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index 0bde81e60..7865e3921 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -129,7 +129,7 @@ To submit a deposit: ### Process deposit -Deposits cannot be processed into the beacon chain until the Eth1 block in which they were deposited or any of its descendants is added to the beacon chain `state.eth1_data`. This takes _a minimum_ of `ETH1_FOLLOW_DISTANCE` Eth1 blocks (~4 hours) plus `SLOTS_PER_ETH1_VOTING_PERIOD` slots (~3.4 hours). Once the requisite Eth1 data is added, the deposit will normally be added to a beacon chain block and processed into the `state.validators` within an epoch or two. The validator is then in a queue to be activated. +Deposits cannot be processed into the beacon chain until the Eth1 block in which they were deposited or any of its descendants is added to the beacon chain `state.eth1_data`. This takes _a minimum_ of `ETH1_FOLLOW_DISTANCE` Eth1 blocks (~4 hours) plus `EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH` slots (~3.4 hours). Once the requisite Eth1 data is added, the deposit will normally be added to a beacon chain block and processed into the `state.validators` within an epoch or two. The validator is then in a queue to be activated. ### Validator index @@ -269,7 +269,7 @@ def compute_time_at_slot(state: BeaconState, slot: Slot) -> uint64: ```python def voting_period_start_time(state: BeaconState) -> uint64: - eth1_voting_period_start_slot = Slot(state.slot - state.slot % SLOTS_PER_ETH1_VOTING_PERIOD) + eth1_voting_period_start_slot = Slot(state.slot - state.slot % (EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH)) return compute_time_at_slot(state, eth1_voting_period_start_slot) ``` diff --git a/specs/phase1/beacon-chain.md b/specs/phase1/beacon-chain.md index 78b3b3d25..259d18601 100644 --- a/specs/phase1/beacon-chain.md +++ b/specs/phase1/beacon-chain.md @@ -253,7 +253,7 @@ class BeaconState(Container): historical_roots: List[Root, HISTORICAL_ROOTS_LIMIT] # Eth1 eth1_data: Eth1Data - eth1_data_votes: List[Eth1Data, SLOTS_PER_ETH1_VOTING_PERIOD] + eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index: uint64 # Registry validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py index 58882a44f..110bd35a7 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_final_updates.py @@ -11,7 +11,7 @@ def run_process_final_updates(spec, state): @with_all_phases @spec_state_test def test_eth1_vote_no_reset(spec, state): - assert spec.SLOTS_PER_ETH1_VOTING_PERIOD > spec.SLOTS_PER_EPOCH + assert spec.EPOCHS_PER_ETH1_VOTING_PERIOD > 1 # skip ahead to the end of the epoch state.slot = spec.SLOTS_PER_EPOCH - 1 for i in range(state.slot + 1): # add a vote for each skipped slot. @@ -29,7 +29,7 @@ def test_eth1_vote_no_reset(spec, state): @spec_state_test def test_eth1_vote_reset(spec, state): # skip ahead to the end of the voting period - state.slot = spec.SLOTS_PER_ETH1_VOTING_PERIOD - 1 + state.slot = (spec.EPOCHS_PER_ETH1_VOTING_PERIOD * spec.SLOTS_PER_EPOCH) - 1 for i in range(state.slot + 1): # add a vote for each skipped slot. state.eth1_data_votes.append( spec.Eth1Data(deposit_root=b'\xaa' * 32, diff --git a/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py b/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py index 9027660ab..c6f9d8576 100644 --- a/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py +++ b/tests/core/pyspec/eth2spec/test/sanity/test_blocks.py @@ -486,10 +486,12 @@ def test_historical_batch(spec, state): @spec_state_test def test_eth1_data_votes_consensus(spec, state): # Don't run when it will take very, very long to simulate. Minimal configuration suffices. - if spec.SLOTS_PER_ETH1_VOTING_PERIOD > 16: + if spec.EPOCHS_PER_ETH1_VOTING_PERIOD > 2: return - offset_block = build_empty_block(spec, state, slot=spec.SLOTS_PER_ETH1_VOTING_PERIOD - 1) + voting_period_slots = spec.EPOCHS_PER_ETH1_VOTING_PERIOD * spec.SLOTS_PER_EPOCH + + offset_block = build_empty_block(spec, state, slot=voting_period_slots - 1) state_transition_and_sign_block(spec, state, offset_block) yield 'pre', state @@ -499,14 +501,14 @@ def test_eth1_data_votes_consensus(spec, state): blocks = [] - for i in range(0, spec.SLOTS_PER_ETH1_VOTING_PERIOD): + for i in range(0, voting_period_slots): block = build_empty_block_for_next_slot(spec, state) # wait for over 50% for A, then start voting B - block.body.eth1_data.block_hash = b if i * 2 > spec.SLOTS_PER_ETH1_VOTING_PERIOD else a + block.body.eth1_data.block_hash = b if i * 2 > voting_period_slots else a signed_block = state_transition_and_sign_block(spec, state, block) blocks.append(signed_block) - assert len(state.eth1_data_votes) == spec.SLOTS_PER_ETH1_VOTING_PERIOD + assert len(state.eth1_data_votes) == voting_period_slots assert state.eth1_data.block_hash == a # transition to next eth1 voting period @@ -519,7 +521,7 @@ def test_eth1_data_votes_consensus(spec, state): yield 'post', state assert state.eth1_data.block_hash == a - assert state.slot % spec.SLOTS_PER_ETH1_VOTING_PERIOD == 0 + assert state.slot % voting_period_slots == 0 assert len(state.eth1_data_votes) == 1 assert state.eth1_data_votes[0].block_hash == c @@ -528,12 +530,14 @@ def test_eth1_data_votes_consensus(spec, state): @spec_state_test def test_eth1_data_votes_no_consensus(spec, state): # Don't run when it will take very, very long to simulate. Minimal configuration suffices. - if spec.SLOTS_PER_ETH1_VOTING_PERIOD > 16: + if spec.EPOCHS_PER_ETH1_VOTING_PERIOD > 2: return + voting_period_slots = spec.EPOCHS_PER_ETH1_VOTING_PERIOD * spec.SLOTS_PER_EPOCH + pre_eth1_hash = state.eth1_data.block_hash - offset_block = build_empty_block(spec, state, slot=spec.SLOTS_PER_ETH1_VOTING_PERIOD - 1) + offset_block = build_empty_block(spec, state, slot=voting_period_slots - 1) state_transition_and_sign_block(spec, state, offset_block) yield 'pre', state @@ -542,14 +546,14 @@ def test_eth1_data_votes_no_consensus(spec, state): blocks = [] - for i in range(0, spec.SLOTS_PER_ETH1_VOTING_PERIOD): + for i in range(0, voting_period_slots): block = build_empty_block_for_next_slot(spec, state) # wait for precisely 50% for A, then start voting B for other 50% - block.body.eth1_data.block_hash = b if i * 2 >= spec.SLOTS_PER_ETH1_VOTING_PERIOD else a + block.body.eth1_data.block_hash = b if i * 2 >= voting_period_slots else a signed_block = state_transition_and_sign_block(spec, state, block) blocks.append(signed_block) - assert len(state.eth1_data_votes) == spec.SLOTS_PER_ETH1_VOTING_PERIOD + assert len(state.eth1_data_votes) == voting_period_slots assert state.eth1_data.block_hash == pre_eth1_hash yield 'blocks', blocks From 55d436db517f95fa39dd202619c02adf92b61c8a Mon Sep 17 00:00:00 2001 From: protolambda Date: Tue, 10 Mar 2020 18:55:59 +0100 Subject: [PATCH 24/39] simplify description of voting period time --- specs/phase0/validator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index 7865e3921..009816c2e 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -129,7 +129,7 @@ To submit a deposit: ### Process deposit -Deposits cannot be processed into the beacon chain until the Eth1 block in which they were deposited or any of its descendants is added to the beacon chain `state.eth1_data`. This takes _a minimum_ of `ETH1_FOLLOW_DISTANCE` Eth1 blocks (~4 hours) plus `EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH` slots (~3.4 hours). Once the requisite Eth1 data is added, the deposit will normally be added to a beacon chain block and processed into the `state.validators` within an epoch or two. The validator is then in a queue to be activated. +Deposits cannot be processed into the beacon chain until the Eth1 block in which they were deposited or any of its descendants is added to the beacon chain `state.eth1_data`. This takes _a minimum_ of `ETH1_FOLLOW_DISTANCE` Eth1 blocks (~4 hours) plus `EPOCHS_PER_ETH1_VOTING_PERIOD` epochs (~3.4 hours). Once the requisite Eth1 data is added, the deposit will normally be added to a beacon chain block and processed into the `state.validators` within an epoch or two. The validator is then in a queue to be activated. ### Validator index From 1818f349ad9200c0d9e1d8b9633fa4efc9330950 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 10 Mar 2020 11:59:34 -0600 Subject: [PATCH 25/39] add ForkDigest type, clarify how genesis_validators_root is mixed into domains for chain isolation in p2p faq --- specs/phase0/beacon-chain.md | 3 ++- specs/phase0/p2p-interface.md | 10 +++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 731b02b1a..8c93df7ef 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -149,6 +149,7 @@ We define the following Python custom types for type hinting and readability: | `Root` | `Bytes32` | a Merkle root | | `Version` | `Bytes4` | a fork version number | | `DomainType` | `Bytes4` | a domain type | +| `ForkDigest` | `Bytes4` | a digest of the current fork data | | `Domain` | `Bytes32` | a signature domain | | `BLSPubkey` | `Bytes48` | a BLS12-381 public key | | `BLSSignature` | `Bytes96` | a BLS12-381 signature | @@ -803,7 +804,7 @@ def compute_domain(domain_type: DomainType, fork_version: Optional[Version]=None if fork_version is None: fork_version = GENESIS_FORK_VERSION if genesis_root is None: - genesis_root = Root() + genesis_root = Root() # all bytes zero by default return Domain(domain_type + fork_version + genesis_root[:24]) ``` diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index f2dcc2324..0b6a5798a 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -615,7 +615,7 @@ ENRs MUST carry a generic `eth2` key with an 16-byte value of the node's current |:-------------|:--------------------| | `eth2` | SSZ `ENRForkID` | -First we define `current_fork` as the following SSZ encoded object +First we define `current_fork_data` as the following SSZ encoded object ``` ( @@ -633,7 +633,7 @@ Specifically, the value of the `eth2` key MUST be the following SSZ encoded obje ``` ( - current_fork_digest: Bytes4 + current_fork_digest: ForkDigest next_fork_version: Version next_fork_epoch: Epoch ) @@ -641,7 +641,7 @@ Specifically, the value of the `eth2` key MUST be the following SSZ encoded obje where the fields of `ENRForkID` are defined as -* `current_fork_digest` is `hash_tree_root(current_fork)[:4]` +* `current_fork_digest` is `ForkDigest(hash_tree_root(current_fork_data)[:4])` * `next_fork_version` is the fork version corresponding to the next planned hard fork at a future epoch. If no future fork is planned, set `next_fork_version = current_fork_version` to signal this fact * `next_fork_epoch` is the epoch at which the next fork is planned and the `current_fork_version` will be updated. If no future fork is planned, set `next_fork_epoch = FAR_FUTURE_EPOCH` to signal this fact @@ -872,9 +872,9 @@ Although this method will be sufficient for early phases of Eth2, we aim to use ### How should fork version be used in practice? -Fork versions are to be manually updated (likely via incrementing or using the less collision-prone git spec hash) at each hard fork. This is to provide native domain separation for signatures as well as to aid in usefulness for identitying peers (via ENRs) and versioning network protocols (e.g. using fork version to naturally version gossipsub topics). +Fork versions are to be manually updated (likely via incrementing) at each hard fork. This is to provide native domain separation for signatures as well as to aid in usefulness for identitying peers (via ENRs) and versioning network protocols (e.g. using fork version to naturally version gossipsub topics). -To reap the full benefit of the native versioning scheme, networks SHOULD avoid collisions. For example, a testnet might us mainnet versioning but use a unique higher order byte to signal the testnet. +`BeaconState.genesis_validators_root` is mixed into signature and ENR fork domains to aid in the ease of domain separation between chains. This allows fork versions to safely be reused across chains except for the case of contentious forks using the same genesis. In these cases, extra care should be taken to isolate fork versions (e.g. flip a high order bit in all future versions of one of the chains). A node locally stores all previous and future planned fork versions along with the each fork epoch. This allows for handling sync starting from past forks/epochs and for connections to safely be made with peers syncing from past forks/epochs. From d6eedd95c034736ebee274a5bcf18c71b8ca893e Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 10 Mar 2020 13:04:28 -0600 Subject: [PATCH 26/39] fix wording to be clear it is about serving blocks Co-Authored-By: Diederik Loerakker --- specs/phase0/p2p-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index b214c458d..c0580fafd 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -548,7 +548,7 @@ The request MUST be encoded as an SSZ-container. The response MUST consist of zero or more `response_chunk`. Each _successful_ `response_chunk` MUST contain a single `SignedBeaconBlock` payload. -Clients MUST keep a record of signed blocks seen since the since the start of the weak subjectivity period and MUST support requesting blocks up to the given `head_block_root`. +Clients MUST keep a record of signed blocks seen since the since the start of the weak subjectivity period and MUST support serving requests of blocks up to their own `head_block_root`. Clients MUST respond with at least one block, if they have it and it exists in the range. Clients MAY limit the number of blocks in the response. From 1579072e15e564904e41252f7db4ed5d1a04c057 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 10 Mar 2020 13:12:17 -0600 Subject: [PATCH 27/39] add note about total balance overflowing --- specs/phase0/beacon-chain.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index a90a8aa1a..b2b96de9d 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -954,6 +954,7 @@ def get_beacon_proposer_index(state: BeaconState) -> ValidatorIndex: def get_total_balance(state: BeaconState, indices: Set[ValidatorIndex]) -> Gwei: """ Return the combined effective balance of the ``indices``. (1 Gwei minimum to avoid divisions by zero.) + Math safe up to ~10B ETH, afterwhich this overflows uint64. """ return Gwei(max(1, sum([state.validators[index].effective_balance for index in indices]))) ``` From c91eee6bdf046a800cd2c0155f8fd1dd5ff976bd Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 10 Mar 2020 13:20:57 -0600 Subject: [PATCH 28/39] revert fork choice store.blocks to store BeaconBlock --- specs/phase0/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 79d37f28d..0ccabec75 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -83,7 +83,7 @@ class Store(object): justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint best_justified_checkpoint: Checkpoint - blocks: Dict[Root, BeaconBlockHeader] = field(default_factory=dict) + blocks: Dict[Root, BeaconBlock] = field(default_factory=dict) block_states: Dict[Root, BeaconState] = field(default_factory=dict) checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict) latest_messages: Dict[ValidatorIndex, LatestMessage] = field(default_factory=dict) From 415544bf038315c82d40035c79a3872c38efec75 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 10 Mar 2020 14:41:33 -0600 Subject: [PATCH 29/39] modify gossip topics to use ForkDigest --- specs/phase0/beacon-chain.md | 26 +++++++++++++++++++++- specs/phase0/p2p-interface.md | 42 +++++++++++------------------------ 2 files changed, 38 insertions(+), 30 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index ce29ceda3..489e9b55f 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -24,6 +24,7 @@ - [Containers](#containers) - [Misc dependencies](#misc-dependencies) - [`Fork`](#fork) + - [`ForkData`](#forkdata) - [`Checkpoint`](#checkpoint) - [`Validator`](#validator) - [`AttestationData`](#attestationdata) @@ -75,6 +76,7 @@ - [`compute_epoch_at_slot`](#compute_epoch_at_slot) - [`compute_start_slot_at_epoch`](#compute_start_slot_at_epoch) - [`compute_activation_exit_epoch`](#compute_activation_exit_epoch) + - [`compute_fork_data_root`](#compute_fork_data_root) - [`compute_domain`](#compute_domain) - [`compute_signing_root`](#compute_signing_root) - [Beacon state accessors](#beacon-state-accessors) @@ -286,6 +288,14 @@ class Fork(Container): epoch: Epoch # Epoch of latest fork ``` +#### `ForkData` + +```python +class ForkData(Container): + current_version: Version + genesis_root: Root +``` + #### `Checkpoint` ```python @@ -794,6 +804,19 @@ def compute_activation_exit_epoch(epoch: Epoch) -> Epoch: return Epoch(epoch + 1 + MAX_SEED_LOOKAHEAD) ``` +#### `compute_fork_data_root` + +```python +def compute_fork_data_root(current_version: Version, genesis_validators_root: Root) -> Root: + """ + Return the fork digest for the ``current_fork_version`` and ``genesis_validators_root`` + """ + return hash_tree_root(ForkData( + current_version=current_version, + genesis_validators_root=genesis_validators_root, + )) +``` + #### `compute_domain` ```python @@ -805,7 +828,8 @@ def compute_domain(domain_type: DomainType, fork_version: Optional[Version]=None fork_version = GENESIS_FORK_VERSION if genesis_root is None: genesis_root = Root() # all bytes zero by default - return Domain(domain_type + fork_version + genesis_root[:24]) + fork_data_root = compute_fork_data_root(fork_version, genesis_root) + return Domain(domain_type + fork_data_root[:28]) ``` #### `compute_signing_root` diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 7e7d38341..8b6423277 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -219,18 +219,14 @@ The following gossipsub [parameters](https://github.com/libp2p/specs/tree/master ### Topics and messages -Topics are plain UTF-8 strings and are encoded on the wire as determined by protobuf (gossipsub messages are enveloped in protobuf messages). Topic strings have form: `/eth2/ForkVersion/Name/Encoding`. This defines both the type of data being sent on the topic and how the data field of the message is encoded. +Topics are plain UTF-8 strings and are encoded on the wire as determined by protobuf (gossipsub messages are enveloped in protobuf messages). Topic strings have form: `/eth2/ForkDigest/Name/Encoding`. This defines both the type of data being sent on the topic and how the data field of the message is encoded. -- `ForkVersion` - the hex-encoded bytes from `state.fork.current_version` of the head state of the client, as also seen in `Status.head_fork_version`. +- `ForkDigest` - the hex-encoded bytes of `ForkDigest(compute_fork_data_root(current_fork_version, genesis_validators_root)[:4])` where + - `current_fork_version` is the fork version of the epoch of the message to be sent on the topic + - `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root` - `Name` - see table below - `Encoding` - the encoding strategy describes a specific representation of bytes that will be transmitted over the wire. See the [Encodings](#Encoding-strategies) section for further details. -The fork version is hex-encoded using the following scheme: -```python - ForkVersion = ''.join('{:02x}'.format(x) for x in state.fork.current_version) -``` -For example, the fork version `Version('0x0001020a')` will be encoded as `0001020a`. - Each gossipsub [message](https://github.com/libp2p/go-libp2p-pubsub/blob/master/pb/rpc.proto#L17-L24) has a maximum size of `GOSSIP_MAX_SIZE`. Clients MUST reject (fail validation) messages that are over this size limit. Likewise, clients MUST NOT emit or propagate messages larger than this limit. The `message-id` of a gossipsub message MUST be: @@ -260,7 +256,7 @@ When processing incoming gossip, clients MAY descore or disconnect peers who fai #### Global topics -There are two primary global topics used to propagate beacon blocks and aggregate attestations to all nodes on the network. Their `TopicName`s are: +There are two primary global topics used to propagate beacon blocks and aggregate attestations to all nodes on the network. Their `Name`s are: - `beacon_block` - This topic is used solely for propagating new signed beacon blocks to all nodes on the networks. Signed blocks are sent in their entirety. The following validations MUST pass before forwarding the `signed_beacon_block` on the network - The block is not from a future slot (with a `MAXIMUM_GOSSIP_CLOCK_DISPARITY` allowance) -- i.e. validate that `signed_beacon_block.message.slot <= current_slot` (a client MAY queue future blocks for processing at the appropriate slot). @@ -278,7 +274,7 @@ There are two primary global topics used to propagate beacon blocks and aggregat - The aggregator signature, `signed_aggregate_and_proof.signature`, is valid. - The signature of `aggregate` is valid. -Additional global topics are used to propagate lower frequency validator messages. Their `TopicName`s are: +Additional global topics are used to propagate lower frequency validator messages. Their `Name`s are: - `voluntary_exit` - This topic is used solely for propagating signed voluntary validator exits to proposers on the network. Signed voluntary exits are sent in their entirety. The following validations MUST pass before forwarding the `signed_voluntary_exit` on to the network - The voluntary exit is the first valid voluntary exit received for the validator with index `signed_voluntary_exit.message.validator_index`. @@ -293,7 +289,7 @@ Additional global topics are used to propagate lower frequency validator message #### Attestation subnets -Attestation subnets are used to propagate unaggregated attestations to subsections of the network. Their `TopicName`s are: +Attestation subnets are used to propagate unaggregated attestations to subsections of the network. Their `Name`s are: - `committee_index{subnet_id}_beacon_attestation` - These topics are used to propagate unaggregated attestations to the subnet `subnet_id` (typically beacon and persistent committees) to be aggregated before being gossiped to `beacon_aggregate_and_proof`. The following validations MUST pass before forwarding the `attestation` on the subnet. - The attestation's committee index (`attestation.data.index`) is for the correct subnet. @@ -654,25 +650,11 @@ ENRs MUST carry a generic `eth2` key with an 16-byte value of the node's current |:-------------|:--------------------| | `eth2` | SSZ `ENRForkID` | -First we define `current_fork_data` as the following SSZ encoded object - -``` -( - current_fork_version: Version - genesis_validators_root: Root -) -``` - -where - -* `current_fork_version` is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync) -* `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root` - Specifically, the value of the `eth2` key MUST be the following SSZ encoded object (`ENRForkID`) ``` ( - current_fork_digest: ForkDigest + fork_digest: ForkDigest next_fork_version: Version next_fork_epoch: Epoch ) @@ -680,13 +662,15 @@ Specifically, the value of the `eth2` key MUST be the following SSZ encoded obje where the fields of `ENRForkID` are defined as -* `current_fork_digest` is `ForkDigest(hash_tree_root(current_fork_data)[:4])` +* `fork_digest` is `ForkDigest(compute_fork_data_root(current_fork_version, genesis_validators_root)[:4])` where + * `current_fork_version` is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync) + * `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root` * `next_fork_version` is the fork version corresponding to the next planned hard fork at a future epoch. If no future fork is planned, set `next_fork_version = current_fork_version` to signal this fact * `next_fork_epoch` is the epoch at which the next fork is planned and the `current_fork_version` will be updated. If no future fork is planned, set `next_fork_epoch = FAR_FUTURE_EPOCH` to signal this fact -Clients SHOULD connect to peers with `current_fork_digest`, `next_fork_version`, and `next_fork_epoch` that match local values. +Clients SHOULD connect to peers with `fork_digest`, `next_fork_version`, and `next_fork_epoch` that match local values. -Clients MAY connect to peers with the same `current_fork_version` but a different `next_fork_version`/`next_fork_epoch`. Unless `ENRForkID` is manually updated to matching prior to the earlier `next_fork_epoch` of the two clients, these type of connecting clients will be unable to successfully interact starting at the earlier `next_fork_epoch`. +Clients MAY connect to peers with the same `fork_digest` but a different `next_fork_version`/`next_fork_epoch`. Unless `ENRForkID` is manually updated to matching prior to the earlier `next_fork_epoch` of the two clients, these type of connecting clients will be unable to successfully interact starting at the earlier `next_fork_epoch`. ##### General capabilities From fccd3ab1ce13bc54f35bbe760637176d8e71f973 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 10 Mar 2020 15:04:44 -0600 Subject: [PATCH 30/39] clarify hex-encoded bytes string representation for fork digest in gossip topic Co-Authored-By: Diederik Loerakker --- specs/phase0/p2p-interface.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 8b6423277..580e42183 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -221,7 +221,7 @@ The following gossipsub [parameters](https://github.com/libp2p/specs/tree/master Topics are plain UTF-8 strings and are encoded on the wire as determined by protobuf (gossipsub messages are enveloped in protobuf messages). Topic strings have form: `/eth2/ForkDigest/Name/Encoding`. This defines both the type of data being sent on the topic and how the data field of the message is encoded. -- `ForkDigest` - the hex-encoded bytes of `ForkDigest(compute_fork_data_root(current_fork_version, genesis_validators_root)[:4])` where +- `ForkDigest` - the lowercase hex-encoded (no "0x" prefix) bytes of `ForkDigest(compute_fork_data_root(current_fork_version, genesis_validators_root)[:4])` where - `current_fork_version` is the fork version of the epoch of the message to be sent on the topic - `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root` - `Name` - see table below From baee6731241b21244ec5f9ca44567b96108cfe0d Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 10 Mar 2020 15:09:15 -0600 Subject: [PATCH 31/39] add note about preparing for subnet backbone for forks --- specs/phase0/validator.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index 009816c2e..b74a57486 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -519,6 +519,8 @@ Because Phase 0 does not have shards and thus does not have Shard Committees, th * Maintain advertisement of the randomly selected subnets in their node's ENR `attnets` entry by setting the randomly selected `subnet_id` bits to `True` (e.g. `ENR["attnets"][subnet_id] = True`) for all persistent attestation subnets * Set the lifetime of each random subscription to a random number of epochs between `EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` and `2 * EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION]`. At the end of life for a subscription, select a new random subnet, update subnet subscriptions, and publish an updated ENR +*Note*: When preparing for a hard fork, a validator must select and subscribe to random subnets of the future fork versioning at least `EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` epochs in advance of the fork. These new subnets for the fork are maintained in addition to those for the current fork until the fork occurs. After the fork occurs, let the subnets from the previous fork reach end of life with no replacements. + ## How to avoid slashing "Slashing" is the burning of some amount of validator funds and immediate ejection from the active validator set. In Phase 0, there are two ways in which funds can be slashed: [proposer slashing](#proposer-slashing) and [attester slashing](#attester-slashing). Although being slashed has serious repercussions, it is simple enough to avoid being slashed all together by remaining _consistent_ with respect to the messages a validator has previously signed. From 0881e21dc5461af1d04b5bea6b2729c03791b72a Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 11 Mar 2020 11:51:31 -0600 Subject: [PATCH 32/39] cleanup gossip topic fork digest based on PR feedback --- setup.py | 2 +- specs/phase0/beacon-chain.md | 26 ++++++++++++++++++++------ specs/phase0/p2p-interface.md | 34 ++++++++++++++++++---------------- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/setup.py b/setup.py index 6dd4de861..5edff0164 100644 --- a/setup.py +++ b/setup.py @@ -106,7 +106,7 @@ SSZObject = TypeVar('SSZObject', bound=View) PHASE1_IMPORTS = '''from eth2spec.phase0 import spec as phase0 from eth2spec.config.config_util import apply_constants_config from typing import ( - Any, Dict, Set, Sequence, NewType, Tuple, TypeVar, Callable, Optional + Any, Dict, Set, Sequence, NewType, Tuple, TypeVar, Callable ) from dataclasses import ( diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 489e9b55f..b14da493b 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -77,6 +77,7 @@ - [`compute_start_slot_at_epoch`](#compute_start_slot_at_epoch) - [`compute_activation_exit_epoch`](#compute_activation_exit_epoch) - [`compute_fork_data_root`](#compute_fork_data_root) + - [`compute_fork_digest`](#compute_fork_digest) - [`compute_domain`](#compute_domain) - [`compute_signing_root`](#compute_signing_root) - [Beacon state accessors](#beacon-state-accessors) @@ -293,7 +294,7 @@ class Fork(Container): ```python class ForkData(Container): current_version: Version - genesis_root: Root + genesis_validators_root: Root ``` #### `Checkpoint` @@ -809,7 +810,8 @@ def compute_activation_exit_epoch(epoch: Epoch) -> Epoch: ```python def compute_fork_data_root(current_version: Version, genesis_validators_root: Root) -> Root: """ - Return the fork digest for the ``current_fork_version`` and ``genesis_validators_root`` + Return the 32-byte fork data root for the ``current_version`` and ``genesis_validators_root``. + This is used primarily in signature domains to avoid collisions across forks/chains. """ return hash_tree_root(ForkData( current_version=current_version, @@ -817,18 +819,30 @@ def compute_fork_data_root(current_version: Version, genesis_validators_root: Ro )) ``` +#### `compute_fork_digest` + +```python +def compute_fork_digest(current_version: Version, genesis_validators_root: Root) -> Root: + """ + Return the 4-byte fork digest for the ``current_version`` and ``genesis_validators_root``. + This is a digest primarily used for domain separation on the p2p layer. + 4-bytes suffices for practical separation of forks/chains. + """ + return ForkDigest(compute_fork_data_root(current_version, genesis_validators_root)[:4]) +``` + #### `compute_domain` ```python -def compute_domain(domain_type: DomainType, fork_version: Optional[Version]=None, genesis_root: Root=None) -> Domain: +def compute_domain(domain_type: DomainType, fork_version: Version=None, genesis_validators_root: Root=None) -> Domain: """ Return the domain for the ``domain_type`` and ``fork_version``. """ if fork_version is None: fork_version = GENESIS_FORK_VERSION - if genesis_root is None: - genesis_root = Root() # all bytes zero by default - fork_data_root = compute_fork_data_root(fork_version, genesis_root) + if genesis_validators_root is None: + genesis_validators_root = Root() # all bytes zero by default + fork_data_root = compute_fork_data_root(fork_version, genesis_validators_root) return Domain(domain_type + fork_data_root[:28]) ``` diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 580e42183..5a4464f5c 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -219,9 +219,9 @@ The following gossipsub [parameters](https://github.com/libp2p/specs/tree/master ### Topics and messages -Topics are plain UTF-8 strings and are encoded on the wire as determined by protobuf (gossipsub messages are enveloped in protobuf messages). Topic strings have form: `/eth2/ForkDigest/Name/Encoding`. This defines both the type of data being sent on the topic and how the data field of the message is encoded. +Topics are plain UTF-8 strings and are encoded on the wire as determined by protobuf (gossipsub messages are enveloped in protobuf messages). Topic strings have form: `/eth2/ForkDigestValue/Name/Encoding`. This defines both the type of data being sent on the topic and how the data field of the message is encoded. -- `ForkDigest` - the lowercase hex-encoded (no "0x" prefix) bytes of `ForkDigest(compute_fork_data_root(current_fork_version, genesis_validators_root)[:4])` where +- `ForkDigestValue` - the lowercase hex-encoded (no "0x" prefix) bytes of `compute_fork_digest(current_fork_version, genesis_validators_root)` where - `current_fork_version` is the fork version of the epoch of the message to be sent on the topic - `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root` - `Name` - see table below @@ -423,7 +423,7 @@ Here, `result` represents the 1-byte response code. The token of the negotiated protocol ID specifies the type of encoding to be used for the req/resp interaction. Two values are possible at this time: -- `ssz`: the contents are [SSZ-encoded](../../ssz/simple-serialize.md). This encoding type MUST be supported by all clients. For objects containing a single field, only the field is SSZ-encoded not a container with a single field. For example, the `BeaconBlocksByRoot` request is an SSZ-encoded list of `Bytes32`'s. +- `ssz`: the contents are [SSZ-encoded](../../ssz/simple-serialize.md). This encoding type MUST be supported by all clients. For objects containing a single field, only the field is SSZ-encoded not a container with a single field. For example, the `BeaconBlocksByRoot` request is an SSZ-encoded list of `Root`'s. - `ssz_snappy`: The contents are SSZ-encoded and then compressed with [Snappy](https://github.com/google/snappy). MAY be supported in the interoperability testnet; MUST be supported in mainnet. #### SSZ-encoding strategy (with or without Snappy) @@ -475,16 +475,18 @@ constituents individually as `response_chunk`s. For example, the Request, Response Content: ``` ( - head_fork_version: Bytes4 - finalized_root: Bytes32 - finalized_epoch: uint64 - head_root: Bytes32 - head_slot: uint64 + fork_digest: ForkDigest + finalized_root: Root + finalized_epoch: Epoch + head_root: Root + head_slot: Slot ) ``` The fields are, as seen by the client at the time of sending the message: -- `head_fork_version`: The beacon_state `Fork` version. +- `fork_digest`: The node's `ForkDigest` (`compute_fork_digest(current_fork_version, genesis_validators_root)`) where + - `current_fork_version` is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync) + - `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root` - `finalized_root`: `state.finalized_checkpoint.root` for the state corresponding to the head block. - `finalized_epoch`: `state.finalized_checkpoint.epoch` for the state corresponding to the head block. - `head_root`: The hash_tree_root root of the current head block. @@ -498,7 +500,7 @@ The response MUST consist of a single `response_chunk`. Clients SHOULD immediately disconnect from one another following the handshake above under the following conditions: -1. If `head_fork_version` does not match the expected fork version at the epoch of the `head_slot`, since the client’s chain is on another fork. `head_fork_version` can also be used to segregate testnets. +1. If `fork_digest` does not match the node's local `fork_digest`, since the client’s chain is on another fork. 2. If the (`finalized_root`, `finalized_epoch`) shared by the peer is not in the client's chain at the expected epoch. For example, if Peer 1 sends (root, epoch) of (A, 5) and Peer 2 sends (B, 3) but Peer 1 has root C at epoch 3, then Peer 1 would disconnect because it knows that their chains are irreparably disjoint. Once the handshake completes, the client with the lower `finalized_epoch` or `head_slot` (if the clients have equal `finalized_epoch`s) SHOULD request beacon blocks from its counterparty via the `BeaconBlocksByRange` request. @@ -536,7 +538,7 @@ The response MUST consist of a single `response_chunk`. Request Content: ``` ( - start_slot: uint64 + start_slot: Slot count: uint64 step: uint64 ) @@ -575,7 +577,7 @@ Request Content: ``` ( - []Bytes32 + []Root ) ``` @@ -662,7 +664,7 @@ Specifically, the value of the `eth2` key MUST be the following SSZ encoded obje where the fields of `ENRForkID` are defined as -* `fork_digest` is `ForkDigest(compute_fork_data_root(current_fork_version, genesis_validators_root)[:4])` where +* `fork_digest` is `compute_fork_digest(current_fork_version, genesis_validators_root)` where * `current_fork_version` is the fork version at the node's current epoch defined by the wall-clock time (not necessarily the epoch to which the node is sync) * `genesis_validators_root` is the static `Root` found in `state.genesis_validators_root` * `next_fork_version` is the fork version corresponding to the next planned hard fork at a future epoch. If no future fork is planned, set `next_fork_version = current_fork_version` to signal this fact @@ -670,7 +672,7 @@ where the fields of `ENRForkID` are defined as Clients SHOULD connect to peers with `fork_digest`, `next_fork_version`, and `next_fork_epoch` that match local values. -Clients MAY connect to peers with the same `fork_digest` but a different `next_fork_version`/`next_fork_epoch`. Unless `ENRForkID` is manually updated to matching prior to the earlier `next_fork_epoch` of the two clients, these type of connecting clients will be unable to successfully interact starting at the earlier `next_fork_epoch`. +Clients MAY connect to peers with the same `fork_digest` but a different `next_fork_version`/`next_fork_epoch`. Unless `ENRForkID` is manually updated to matching prior to the earlier `next_fork_epoch` of the two clients, these connecting clients will be unable to successfully interact starting at the earlier `next_fork_epoch`. ##### General capabilities @@ -897,9 +899,9 @@ Although this method will be sufficient for early phases of Eth2, we aim to use Fork versions are to be manually updated (likely via incrementing) at each hard fork. This is to provide native domain separation for signatures as well as to aid in usefulness for identitying peers (via ENRs) and versioning network protocols (e.g. using fork version to naturally version gossipsub topics). -`BeaconState.genesis_validators_root` is mixed into signature and ENR fork domains to aid in the ease of domain separation between chains. This allows fork versions to safely be reused across chains except for the case of contentious forks using the same genesis. In these cases, extra care should be taken to isolate fork versions (e.g. flip a high order bit in all future versions of one of the chains). +`BeaconState.genesis_validators_root` is mixed into signature and ENR fork domains (`ForkDigest`) to aid in the ease of domain separation between chains. This allows fork versions to safely be reused across chains except for the case of contentious forks using the same genesis. In these cases, extra care should be taken to isolate fork versions (e.g. flip a high order bit in all future versions of one of the chains). -A node locally stores all previous and future planned fork versions along with the each fork epoch. This allows for handling sync starting from past forks/epochs and for connections to safely be made with peers syncing from past forks/epochs. +A node locally stores all previous and future planned fork versions along with the each fork epoch. This allows for handling sync and processing messages starting from past forks/epochs. ## Req/Resp From 36e48fba99be3db6fb64a25497a226f8bebab819 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 11 Mar 2020 12:46:47 -0600 Subject: [PATCH 33/39] enforce must match target to match head to avoid perverse incentive path --- setup.py | 12 +++++++++- specs/phase0/beacon-chain.md | 2 +- .../test_process_rewards_and_penalties.py | 23 +++++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 6dd4de861..084f72901 100644 --- a/setup.py +++ b/setup.py @@ -182,7 +182,17 @@ get_active_validator_indices = cache_this( _get_beacon_committee = get_beacon_committee get_beacon_committee = cache_this( lambda state, slot, index: (state.validators.hash_tree_root(), state.randao_mixes.hash_tree_root(), slot, index), - _get_beacon_committee)''' + _get_beacon_committee) + +_get_matching_target_attestations = get_matching_target_attestations +get_matching_target_attestations = cache_this( + lambda state, epoch: (state.hash_tree_root(), epoch), + _get_matching_target_attestations) + +_get_matching_head_attestations = get_matching_head_attestations +get_matching_head_attestations = cache_this( + lambda state, epoch: (state.hash_tree_root(), epoch), + _get_matching_head_attestations)''' def objects_to_spec(spec_object: SpecObject, imports: str, fork: str) -> str: diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index 91cb9f714..ff80360c6 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -1235,7 +1235,7 @@ def get_matching_target_attestations(state: BeaconState, epoch: Epoch) -> Sequen ```python def get_matching_head_attestations(state: BeaconState, epoch: Epoch) -> Sequence[PendingAttestation]: return [ - a for a in get_matching_source_attestations(state, epoch) + a for a in get_matching_target_attestations(state, epoch) if a.data.beacon_block_root == get_block_root_at_slot(state, a.data.slot) ] ``` diff --git a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py index b4f50179e..111033799 100644 --- a/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py +++ b/tests/core/pyspec/eth2spec/test/phase_0/epoch_processing/test_process_rewards_and_penalties.py @@ -97,6 +97,29 @@ def test_full_attestations(spec, state): assert state.balances[index] < pre_state.balances[index] +@with_all_phases +@spec_state_test +def test_full_attestations_random_incorrect_fields(spec, state): + attestations = prepare_state_with_full_attestations(spec, state) + for i, attestation in enumerate(state.previous_epoch_attestations): + if i % 3 == 0: + # Mess up some head votes + attestation.data.beacon_block_root = b'\x56' * 32 + if i % 3 == 1: + # Message up some target votes + attestation.data.target.root = b'\x23' * 32 + if i % 3 == 2: + # Keep some votes 100% correct + pass + + yield from run_process_rewards_and_penalties(spec, state) + + attesting_indices = spec.get_unslashed_attesting_indices(state, attestations) + assert len(attesting_indices) > 0 + # No balance checks, non-trivial base on group rewards + # Mainly for consensus tests + + @with_all_phases @spec_test @with_custom_state(balances_fn=misc_balances, threshold_fn=default_activation_threshold) From 4bcdf91e8b1cf0e36d26b30ddf6d52144b9eb2f1 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 11 Mar 2020 13:24:30 -0600 Subject: [PATCH 34/39] Apply suggestions from code review PR feedback Co-Authored-By: Hsiao-Wei Wang --- specs/phase0/beacon-chain.md | 2 +- specs/phase0/validator.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/specs/phase0/beacon-chain.md b/specs/phase0/beacon-chain.md index b14da493b..5b5a13f93 100644 --- a/specs/phase0/beacon-chain.md +++ b/specs/phase0/beacon-chain.md @@ -822,7 +822,7 @@ def compute_fork_data_root(current_version: Version, genesis_validators_root: Ro #### `compute_fork_digest` ```python -def compute_fork_digest(current_version: Version, genesis_validators_root: Root) -> Root: +def compute_fork_digest(current_version: Version, genesis_validators_root: Root) -> ForkDigest: """ Return the 4-byte fork digest for the ``current_version`` and ``genesis_validators_root``. This is a digest primarily used for domain separation on the p2p layer. diff --git a/specs/phase0/validator.md b/specs/phase0/validator.md index b74a57486..d082ed04e 100644 --- a/specs/phase0/validator.md +++ b/specs/phase0/validator.md @@ -519,7 +519,7 @@ Because Phase 0 does not have shards and thus does not have Shard Committees, th * Maintain advertisement of the randomly selected subnets in their node's ENR `attnets` entry by setting the randomly selected `subnet_id` bits to `True` (e.g. `ENR["attnets"][subnet_id] = True`) for all persistent attestation subnets * Set the lifetime of each random subscription to a random number of epochs between `EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` and `2 * EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION]`. At the end of life for a subscription, select a new random subnet, update subnet subscriptions, and publish an updated ENR -*Note*: When preparing for a hard fork, a validator must select and subscribe to random subnets of the future fork versioning at least `EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` epochs in advance of the fork. These new subnets for the fork are maintained in addition to those for the current fork until the fork occurs. After the fork occurs, let the subnets from the previous fork reach end of life with no replacements. +*Note*: When preparing for a hard fork, a validator must select and subscribe to random subnets of the future fork versioning at least `EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION` epochs in advance of the fork. These new subnets for the fork are maintained in addition to those for the current fork until the fork occurs. After the fork occurs, let the subnets from the previous fork reach the end of life with no replacements. ## How to avoid slashing From a49fc814eb80c850c1161e81e8c0ad2cfe1f08e1 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 11 Mar 2020 14:52:31 -0600 Subject: [PATCH 35/39] change 'gasper' to 'combining ghost and casper' for paper description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 83dbbd3fe..b6e25d570 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ The following are the broad design goals for Ethereum 2.0: * [Design Rationale](https://notes.ethereum.org/s/rkhCgQteN#) * [Phase 0 Onboarding Document](https://notes.ethereum.org/s/Bkn3zpwxB) -* [Gasper paper](https://arxiv.org/abs/2003.03052) +* [Combining GHOST and Casper paper](https://arxiv.org/abs/2003.03052) ## For spec contributors From 47bbffa0d6a0c26a150f3fc61ebf8ba09f55ff8b Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 11 Mar 2020 15:03:14 -0600 Subject: [PATCH 36/39] 'get_checkpoint_store' -> 'get_forkchoice_store' typo --- specs/phase0/fork-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index 0ccabec75..aeef277f2 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -42,7 +42,7 @@ This document is the beacon chain fork choice spec, part of Ethereum 2.0 Phase 0 ## Fork choice -The head block root associated with a `store` is defined as `get_head(store)`. At genesis, let `store = get_checkpoint_store(genesis_state)` and update `store` by running: +The head block root associated with a `store` is defined as `get_head(store)`. At genesis, let `store = get_forkchoice_store(genesis_state)` and update `store` by running: - `on_tick(time)` whenever `time > store.time` where `time` is the current Unix time - `on_block(block)` whenever a block `block: SignedBeaconBlock` is received From a612df1119c06921fa4ad11129a00f44b7056070 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 11 Mar 2020 15:51:07 -0600 Subject: [PATCH 37/39] minor typos and clarifications in fork choice --- specs/phase0/p2p-interface.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/specs/phase0/p2p-interface.md b/specs/phase0/p2p-interface.md index 5a4464f5c..77eeb21df 100644 --- a/specs/phase0/p2p-interface.md +++ b/specs/phase0/p2p-interface.md @@ -97,7 +97,7 @@ It consists of four main sections: - [Why do we version protocol strings with ordinals instead of semver?](#why-do-we-version-protocol-strings-with-ordinals-instead-of-semver) - [Why is it called Req/Resp and not RPC?](#why-is-it-called-reqresp-and-not-rpc) - [Why do we allow empty responses in block requests?](#why-do-we-allow-empty-responses-in-block-requests) - - [Why does `BeaconBlocksByRange` let the server choose which chain to send blocks from?](#why-does-beaconblocksbyrange-let-the-server-choose-which-chain-to-send-blocks-from) + - [Why does `BeaconBlocksByRange` let the server choose which branch to send blocks from?](#why-does-beaconblocksbyrange-let-the-server-choose-which-branch-to-send-blocks-from) - [What's the effect of empty slots on the sync algorithm?](#whats-the-effect-of-empty-slots-on-the-sync-algorithm) - [Discovery](#discovery) - [Why are we using discv5 and not libp2p Kademlia DHT?](#why-are-we-using-discv5-and-not-libp2p-kademlia-dht) @@ -646,7 +646,7 @@ Nonetheless, ENRs MUST carry a generic `eth2` key with nil value, denoting that ##### `eth2` field -ENRs MUST carry a generic `eth2` key with an 16-byte value of the node's current fork version, next fork version, and next fork epoch to ensure connections are made with peers on the intended eth2 network. +ENRs MUST carry a generic `eth2` key with an 16-byte value of the node's current fork digest, next fork version, and next fork epoch to ensure connections are made with peers on the intended eth2 network. | Key | Value | |:-------------|:--------------------| @@ -973,17 +973,17 @@ Assuming option 0 with no special `null` encoding, consider a request for slots Failing to provide blocks that nodes "should" have is reason to trust a peer less - for example, if a particular peer gossips a block, it should have access to its parent. If a request for the parent fails, it's indicative of poor peer quality since peers should validate blocks before gossiping them. -### Why does `BeaconBlocksByRange` let the server choose which chain to send blocks from? +### Why does `BeaconBlocksByRange` let the server choose which branch to send blocks from? When connecting, the `Status` message gives an idea about the sync status of a particular peer, but this changes over time. By the time a subsequent `BeaconBlockByRange` request is processed, the information may be stale, and the responding side might have moved on to a new finalization point and pruned blocks around the previous head and finalized blocks. -To avoid this race condition, we allow the responding side to choose which chain to send to the requesting client. The requesting client then goes on to validate the blocks and incorporate them in their own database - because they follow the same rules, they should at this point arrive at the same chain. +To avoid this race condition, we allow the responding side to choose which branch to send to the requesting client. The requesting client then goes on to validate the blocks and incorporate them in their own database - because they follow the same rules, they should at this point arrive at the same canonical chain. ### What's the effect of empty slots on the sync algorithm? -When syncing one can only tell that a slot has been skipped on a particular chain by examining subsequent blocks and analyzing the graph formed by the parent root. Because the server side may choose to omit blocks in the response for any reason, clients must validate the graph and be prepared to fill in gaps. +When syncing one can only tell that a slot has been skipped on a particular branch by examining subsequent blocks and analyzing the graph formed by the parent root. Because the server side may choose to omit blocks in the response for any reason, clients must validate the graph and be prepared to fill in gaps. -For example, if a peer responds with blocks [2, 3] when asked for [2, 3, 4], clients may not assume that block 4 doesn't exist - it merely means that the responding peer did not send it (they may not have it yet or may maliciously be trying to hide it) and successive blocks will be needed to determine if there exists a block at slot 4 in this particular chain. +For example, if a peer responds with blocks [2, 3] when asked for [2, 3, 4], clients may not assume that block 4 doesn't exist - it merely means that the responding peer did not send it (they may not have it yet or may maliciously be trying to hide it) and successive blocks will be needed to determine if there exists a block at slot 4 in this particular branch. ## Discovery From 22620bfe5dec9b75acc686d025c375ad72dfeafc Mon Sep 17 00:00:00 2001 From: protolambda Date: Wed, 11 Mar 2020 23:18:06 +0100 Subject: [PATCH 38/39] Fix generic SSZ tests, update remerkleable with small bugfix --- setup.py | 2 +- .../ssz_generic/ssz_basic_vector.py | 17 +++++++---- tests/generators/ssz_generic/ssz_container.py | 8 ++--- tests/generators/ssz_generic/ssz_test_case.py | 4 +-- tests/generators/ssz_generic/ssz_uints.py | 29 +++++++++++-------- 5 files changed, 35 insertions(+), 25 deletions(-) diff --git a/setup.py b/setup.py index 6dd4de861..e0f2880d9 100644 --- a/setup.py +++ b/setup.py @@ -470,7 +470,7 @@ setup( "pycryptodome==3.9.4", "py_ecc==2.0.0", "dataclasses==0.6", - "remerkleable==0.1.11", + "remerkleable==0.1.12", "ruamel.yaml==0.16.5" ] ) diff --git a/tests/generators/ssz_generic/ssz_basic_vector.py b/tests/generators/ssz_generic/ssz_basic_vector.py index 6e7e08daa..51dfd4ba1 100644 --- a/tests/generators/ssz_generic/ssz_basic_vector.py +++ b/tests/generators/ssz_generic/ssz_basic_vector.py @@ -1,19 +1,19 @@ from ssz_test_case import invalid_test_case, valid_test_case -from eth2spec.utils.ssz.ssz_typing import boolean, uint8, uint16, uint32, uint64, uint128, uint256, Vector, BasicType +from eth2spec.utils.ssz.ssz_typing import boolean, uint8, uint16, uint32, uint64, uint128, uint256, Vector, BasicView from eth2spec.utils.ssz.ssz_impl import serialize from random import Random -from typing import Dict +from typing import Dict, Type from eth2spec.debug.random_value import RandomizationMode, get_random_ssz_object -def basic_vector_case_fn(rng: Random, mode: RandomizationMode, elem_type: BasicType, length: int): +def basic_vector_case_fn(rng: Random, mode: RandomizationMode, elem_type: Type[BasicView], length: int): return get_random_ssz_object(rng, Vector[elem_type, length], max_bytes_length=length * 8, max_list_length=length, mode=mode, chaos=False) -BASIC_TYPES: Dict[str, BasicType] = { +BASIC_TYPES: Dict[str, Type[BasicView]] = { 'bool': boolean, 'uint8': uint8, 'uint16': uint16, @@ -49,8 +49,13 @@ def invalid_cases(): for length in [1, 2, 3, 4, 5, 8, 16, 31, 512, 513]: yield f'vec_{name}_{length}_nil', invalid_test_case(lambda: b'') for mode in random_modes: - yield f'vec_{name}_{length}_{mode.to_name()}_one_less', \ - invalid_test_case(lambda: serialize(basic_vector_case_fn(rng, mode, typ, length - 1))) + if length == 1: + # empty bytes, no elements. It may seem valid, but empty fixed-size elements are not valid SSZ. + yield f'vec_{name}_{length}_{mode.to_name()}_one_less', \ + invalid_test_case(lambda: b"") + else: + yield f'vec_{name}_{length}_{mode.to_name()}_one_less', \ + invalid_test_case(lambda: serialize(basic_vector_case_fn(rng, mode, typ, length - 1))) yield f'vec_{name}_{length}_{mode.to_name()}_one_more', \ invalid_test_case(lambda: serialize(basic_vector_case_fn(rng, mode, typ, length + 1))) yield f'vec_{name}_{length}_{mode.to_name()}_one_byte_less', \ diff --git a/tests/generators/ssz_generic/ssz_container.py b/tests/generators/ssz_generic/ssz_container.py index ecb2d8c34..cf7c33839 100644 --- a/tests/generators/ssz_generic/ssz_container.py +++ b/tests/generators/ssz_generic/ssz_container.py @@ -1,9 +1,9 @@ from ssz_test_case import invalid_test_case, valid_test_case -from eth2spec.utils.ssz.ssz_typing import SSZType, Container, byte, uint8, uint16, \ +from eth2spec.utils.ssz.ssz_typing import View, Container, byte, uint8, uint16, \ uint32, uint64, List, ByteList, Vector, Bitvector, Bitlist from eth2spec.utils.ssz.ssz_impl import serialize from random import Random -from typing import Dict, Tuple, Sequence, Callable +from typing import Dict, Tuple, Sequence, Callable, Type from eth2spec.debug.random_value import RandomizationMode, get_random_ssz_object @@ -46,14 +46,14 @@ class BitsStruct(Container): E: Bitvector[8] -def container_case_fn(rng: Random, mode: RandomizationMode, typ: SSZType): +def container_case_fn(rng: Random, mode: RandomizationMode, typ: Type[View]): return get_random_ssz_object(rng, typ, max_bytes_length=2000, max_list_length=2000, mode=mode, chaos=False) -PRESET_CONTAINERS: Dict[str, Tuple[SSZType, Sequence[int]]] = { +PRESET_CONTAINERS: Dict[str, Tuple[Type[View], Sequence[int]]] = { 'SingleFieldTestStruct': (SingleFieldTestStruct, []), 'SmallTestStruct': (SmallTestStruct, []), 'FixedTestStruct': (FixedTestStruct, []), diff --git a/tests/generators/ssz_generic/ssz_test_case.py b/tests/generators/ssz_generic/ssz_test_case.py index 42955bd3e..6cef4960b 100644 --- a/tests/generators/ssz_generic/ssz_test_case.py +++ b/tests/generators/ssz_generic/ssz_test_case.py @@ -1,10 +1,10 @@ from eth2spec.utils.ssz.ssz_impl import serialize, hash_tree_root from eth2spec.debug.encode import encode -from eth2spec.utils.ssz.ssz_typing import SSZValue, Container +from eth2spec.utils.ssz.ssz_typing import View from typing import Callable -def valid_test_case(value_fn: Callable[[], SSZValue]): +def valid_test_case(value_fn: Callable[[], View]): def case_fn(): value = value_fn() yield "value", "data", encode(value) diff --git a/tests/generators/ssz_generic/ssz_uints.py b/tests/generators/ssz_generic/ssz_uints.py index b21fb251c..896443f4c 100644 --- a/tests/generators/ssz_generic/ssz_uints.py +++ b/tests/generators/ssz_generic/ssz_uints.py @@ -1,12 +1,13 @@ from ssz_test_case import invalid_test_case, valid_test_case -from eth2spec.utils.ssz.ssz_typing import BasicType, uint8, uint16, uint32, uint64, uint128, uint256 +from eth2spec.utils.ssz.ssz_typing import BasicView, uint8, uint16, uint32, uint64, uint128, uint256 from random import Random +from typing import Type from eth2spec.debug.random_value import RandomizationMode, get_random_ssz_object -def uint_case_fn(rng: Random, mode: RandomizationMode, typ: BasicType): +def uint_case_fn(rng: Random, mode: RandomizationMode, typ: Type[BasicView]): return get_random_ssz_object(rng, typ, - max_bytes_length=typ.byte_len, + max_bytes_length=typ.type_byte_length(), max_list_length=1, mode=mode, chaos=False) @@ -17,21 +18,25 @@ UINT_TYPES = [uint8, uint16, uint32, uint64, uint128, uint256] def valid_cases(): rng = Random(1234) for uint_type in UINT_TYPES: - yield f'uint_{uint_type.byte_len * 8}_last_byte_empty', \ - valid_test_case(lambda: uint_type((2 ** ((uint_type.byte_len - 1) * 8)) - 1)) + byte_len = uint_type.type_byte_length() + yield f'uint_{byte_len * 8}_last_byte_empty', \ + valid_test_case(lambda: uint_type((2 ** ((byte_len - 1) * 8)) - 1)) for variation in range(5): for mode in [RandomizationMode.mode_random, RandomizationMode.mode_zero, RandomizationMode.mode_max]: - yield f'uint_{uint_type.byte_len * 8}_{mode.to_name()}_{variation}', \ + yield f'uint_{byte_len * 8}_{mode.to_name()}_{variation}', \ valid_test_case(lambda: uint_case_fn(rng, mode, uint_type)) def invalid_cases(): for uint_type in UINT_TYPES: - yield f'uint_{uint_type.byte_len * 8}_one_too_high', \ - invalid_test_case(lambda: (2 ** (uint_type.byte_len * 8)).to_bytes(uint_type.byte_len + 1, 'little')) + byte_len = uint_type.type_byte_length() + yield f'uint_{byte_len * 8}_one_too_high', \ + invalid_test_case(lambda: (2 ** (byte_len * 8)).to_bytes(byte_len + 1, 'little')) for uint_type in [uint8, uint16, uint32, uint64, uint128, uint256]: - yield f'uint_{uint_type.byte_len * 8}_one_byte_longer', \ - invalid_test_case(lambda: (2 ** (uint_type.byte_len * 8) - 1).to_bytes(uint_type.byte_len + 1, 'little')) + byte_len = uint_type.type_byte_length() + yield f'uint_{byte_len * 8}_one_byte_longer', \ + invalid_test_case(lambda: (2 ** (byte_len * 8) - 1).to_bytes(byte_len + 1, 'little')) for uint_type in [uint8, uint16, uint32, uint64, uint128, uint256]: - yield f'uint_{uint_type.byte_len * 8}_one_byte_shorter', \ - invalid_test_case(lambda: (2 ** ((uint_type.byte_len - 1) * 8) - 1).to_bytes(uint_type.byte_len - 1, 'little')) + byte_len = uint_type.type_byte_length() + yield f'uint_{byte_len * 8}_one_byte_shorter', \ + invalid_test_case(lambda: (2 ** ((byte_len - 1) * 8) - 1).to_bytes(byte_len - 1, 'little')) From c894f5ecece827425c29aed94d122bf7d8f3f70d Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 11 Mar 2020 16:41:27 -0600 Subject: [PATCH 39/39] fork choice error note --- specs/phase0/fork-choice.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/specs/phase0/fork-choice.md b/specs/phase0/fork-choice.md index aeef277f2..c42609be0 100644 --- a/specs/phase0/fork-choice.md +++ b/specs/phase0/fork-choice.md @@ -94,6 +94,10 @@ class Store(object): The provided anchor-state will be regarded as a trusted state, to not roll back beyond. This should be the genesis state for a full client. +*Note* With regards to fork choice, block headers are interchangeable with blocks. The spec is likely to move to headers for reduced overhead in test vectors and better encapsulation. Full implementations store blocks as part of their database and will often use full blocks when dealing with production fork choice. + +_The block for `anchor_root` is incorrectly initialized to the block header, rather than the full block. This does not affect functionality but will be cleaned up in subsequent releases._ + ```python def get_forkchoice_store(anchor_state: BeaconState) -> Store: anchor_block_header = anchor_state.latest_block_header.copy()