From 3371dcc23b1f412e4b93466436424ffaee96a8ae Mon Sep 17 00:00:00 2001 From: Vitalik Buterin Date: Wed, 13 Mar 2019 02:54:27 -0500 Subject: [PATCH 01/21] Added light client related files --- specs/light_client/merkle_proofs.md | 134 ++++++++++++++++++++++ specs/light_client/sync_protocol.md | 169 ++++++++++++++++++++++++++++ 2 files changed, 303 insertions(+) create mode 100644 specs/light_client/merkle_proofs.md create mode 100644 specs/light_client/sync_protocol.md diff --git a/specs/light_client/merkle_proofs.md b/specs/light_client/merkle_proofs.md new file mode 100644 index 000000000..cf4dad2e3 --- /dev/null +++ b/specs/light_client/merkle_proofs.md @@ -0,0 +1,134 @@ +### Generalized Merkle tree index + +In a binary Merkle tree, we define a "generalized index" of a node as `2**depth + index`. Visually, this looks as follows: + +``` + 1 + 2 3 +4 5 6 7 + ... +``` + +Note that the generalized index has the convenient property that the two children of node `k` are `2k` and `2k+1`, and also that it equals the position of a node in the linear representation of the Merkle tree that's computed by this function: + +```python +def merkle_tree(leaves): + o = [0] * len(leaves) + leaves + for i in range(len(leaves)-1, 0, -1): + o[i] = hash(o[i*2] + o[i*2+1]) + return o +``` + +We will define Merkle proofs in terms of generalized indices. + +### SSZ object to index + +We can describe the hash tree of any SSZ object, rooted in `hash_tree_root(object)`, as a binary Merkle tree whose depth may vary. For example, an object `{x: bytes32, y: List[uint64]}` would look as follows: + +``` + root + / \ + x y_root + / \ +y_data_root len(y) + / \ + /\ /\ + ....... +``` + +We can now define a concept of a "path", a way of describing a function that takes as input an SSZ object and outputs some specific (possibly deeply nested) member. For example, `foo -> foo.x` is a path, as are `foo -> len(foo.y)` and `foo -> foo[5]`. We'll describe paths as lists: in these three cases they are `["x"]`, `["y", "len"]` and `["y", 5]` respectively. We can now define a function `get_generalized_indices(object: Any, path: List[str OR int], root=1: int) -> int` that converts an object and a path to a set of generalized indices (note that for constant-sized objects, there is only one generalized index and it only depends on the path, but for dynamically sized objects the indices may depend on the object itself too). For dynamically-sized objects, the set of indices will have more than one member because of the need to access an array's length to determine the correct generalized index for some array access. + +```python +def get_generalized_indices(obj: Any, path: List[str or int], root=1) -> List[int]: + if len(path) == 0: + return [root] + elif isinstance(obj, StaticList): + items_per_chunk = (32 // len(serialize(x))) if isinstance(x, int) else 1 + new_root = root * next_power_of_2(len(obj) // items_per_chunk) + path[0] // items_per_chunk + return get_generalized_indices(obj[path[0]], path[1:], new_root) + elif isinstance(obj, DynamicList) and path[0] == "len": + return [root * 2 + 1] + elif isinstance(obj, DynamicList) and isinstance(path[0], int): + assert path[0] < len(obj) + items_per_chunk = (32 // len(serialize(x))) if isinstance(x, int) else 1 + new_root = root * 2 * next_power_of_2(len(obj) // items_per_chunk) + path[0] // items_per_chunk + return [root *2 + 1] + get_generalized_indices(obj[path[0]], path[1:], new_root) + elif hasattr(obj, "fields"): + index = list(fields.keys()).index(path[0]) + new_root = root * next_power_of_2(len(fields)) + index + return get_generalized_indices(getattr(obj, path[0]), path[1:], new_root) + else: + raise Exception("Unknown type / path") +``` + +### Merkle multiproofs + +We define a Merkle multiproof as a minimal subset of nodes in a Merkle tree needed to fully authenticate that a set of nodes actually are part of a Merkle tree with some specified root, at a particular set of generalized indices. For example, here is the Merkle multiproof for positions 0, 1, 6 in an 8-node Merkle tree (ie. generalized indices 8, 9, 14): + +``` + . + . . + . * * . +x x . . . . x * +``` + +. are unused nodes, * are used nodes, x are the values we are trying to prove. Notice how despite being a multiproof for 3 values, it requires only 3 auxiliary nodes, only one node more than would be required to prove a single value. Normally the efficiency gains are not quite that extreme, but the savings relative to individual Merkle proofs are still significant. As a rule of thumb, a multiproof for k nodes at the same level of an n-node tree has size `k * (n/k + log(n/k))`. + +Here is code for creating and verifying a multiproof. First a helper: + +```python +def log2(x): + return 0 if x == 1 else 1 + log2(x//2) +``` + +First, a method for computing the generalized indices of the auxiliary tree nodes that a proof of a given set of generalized indices will require: + +```python +def get_proof_indices(tree_indices: List[int]) -> List[int]: + # Get all indices touched by the proof + maximal_indices = set({}) + for i in tree_indices: + x = i + while x > 1: + maximal_indices.add(x ^ 1) + x //= 2 + maximal_indices = tree_indices + sorted(list(maximal_indices))[::-1] + # Get indices that cannot be recalculated from earlier indices + redundant_indices = set({}) + proof = [] + for index in maximal_indices: + if index not in redundant_indices: + proof.append(index) + while index > 1: + redundant_indices.add(index) + if (index ^ 1) not in redundant_indices: + break + index //= 2 + return [i for i in proof if i not in tree_indices] +```` + +Generating a proof is simply a matter of taking the node of the SSZ hash tree with the union of the given generalized indices for each index given by `get_proof_indices`, and outputting the list of nodes in the same order. + +```python +def verify_multi_proof(root, indices, leaves, proof): + tree = {} + for index, leaf in zip(indices, leaves): + tree[index] = leaf + for index, proofitem in zip(get_proof_indices(indices), proof): + tree[index] = proofitem + indexqueue = sorted(tree.keys())[:-1] + i = 0 + while i < len(indexqueue): + index = indexqueue[i] + if index >= 2 and index^1 in tree: + tree[index//2] = hash(tree[index - index%2] + tree[index - index%2 + 1]) + indexqueue.append(index//2) + i += 1 + return (indices == []) or (1 in tree and tree[1] == root) +``` + +#### Proofs for execution + +We define `MerklePartial(f, arg1, arg2...)` as being a list of Merkle multiproofs of the sets of nodes in the hash trees of the SSZ objects that are needed to authenticate the values needed to compute some function `f(arg1, arg2...)`. An individual Merkle multiproof is given as a dynamic sized list of `bytes32` values, a `MerklePartial` is a fixed-size list of objects `{proof: ["bytes32"], value: "bytes32"}`, one for each `arg` to `f` (if some `arg` is a base type, then the multiproof is empty). + +Ideally, any function which accepts an SSZ object should also be able to accept a `MerklePartial` object as a substitute. diff --git a/specs/light_client/sync_protocol.md b/specs/light_client/sync_protocol.md new file mode 100644 index 000000000..2a70dbb31 --- /dev/null +++ b/specs/light_client/sync_protocol.md @@ -0,0 +1,169 @@ +# Beacon chain light client syncing + +One of the design goals of the eth2 beacon chain is light-client friendlines, both to allow low-resource clients (mobile phones, IoT, etc) to maintain access to the blockchain in a reasonably safe way, but also to facilitate the development of "bridges" between the eth2 beacon chain and other chains. + +### Preliminaries + +We define an "expansion" of an object as an object where a field in an object that is meant to represent the `hash_tree_root` of another object is replaced by the object. Note that defining expansions is not a consensus-layer-change; it is merely a "re-interpretation" of the object. Particularly, the `hash_tree_root` of an expansion of an object is identical to that of the original object, and we can define expansions where, given a complete history, it is always possible to compute the expansion of any object in the history. The opposite of an expansion is a "summary" (eg. `BeaconBlockHeader` is a summary of `BeaconBlock`). + +We define two expansions: + +* `ExtendedBeaconBlock`, which is identical to a `BeaconBlock` except `state_root` is replaced with the corresponding `state: ExtendedBeaconState` +* `ExtendedBeaconState`, which is identical to a `BeaconState` except `latest_active_index_roots: List[Bytes32]` is replaced by `latest_active_indices: List[List[ValidatorIndex]]`, where `BeaconState.latest_active_index_roots[i] = hash_tree_root(ExtendedBeaconState.latest_active_indices[i])` + +Note that there is now a new way to compute `get_active_validator_indices`: + +```python +def get_active_validator_indices(state: BeaconState, epoch: Epoch) -> List[ValidatorIndex]: + return state.latest_active_indices[epoch % LATEST_ACTIVE_INDEX_ROOTS_LENGTH] +``` + +Note that it takes `state` instead of `state.validator_registry` as an argument. This does not affect its use in `get_shuffled_committee`, because `get_shuffled_committee` has access to the full `state` as one of its arguments. + +A `MerklePartial(f, *args)` is an object that contains a minimal Merkle proof needed to compute `f(*args)`. A `MerklePartial` can be used in place of a regular SSZ object, though a computation would return an error if it attempts to access part of the object that is not contained in the proof. + +We add a data type `PeriodData` and four helpers: + +```python +{ + 'validator_count': 'uint64', + 'seed': 'bytes32', + 'committee': [Validator] +} +``` + +```python +def get_earlier_start_epoch(slot: Slot) -> int: + return slot - slot % PERSISTENT_COMMITTEE_PERIOD - PERSISTENT_COMMITTEE_PERIOD * 2 +def get_later_start_epoch(slot: Slot) -> int: + return slot - slot % PERSISTENT_COMMITTEE_PERIOD - PERSISTENT_COMMITTEE_PERIOD + +def get_earlier_period_data(block: ExtendedBeaconBlock, shard_id: Shard) -> PeriodData: + period_start = get_earlier_start_epoch(header.slot) + validator_count = len(get_active_validator_indices(state, period_start)) + committee_count = validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE) + 1 + indices = get_shuffled_committee(block.state, shard_id, period_start, 0, committee_count) + return PeriodData( + validator_count, + generate_seed(block.state, period_start), + [block.state.validator_registry[i] for i in indices] + ) + +def get_later_period_data(block: ExtendedBeaconBlock, shard_id: Shard) -> PeriodData: + period_start = get_later_start_epoch(header.slot) + validator_count = len(get_active_validator_indices(state, period_start)) + committee_count = validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE) + 1 + indices = get_shuffled_committee(block.state, shard_id, period_start, 0, committee_count) + return PeriodData( + validator_count, + generate_seed(block.state, period_start), + [block.state.validator_registry[i] for i in indices] + ) +``` + +### Light client state + +A light client will keep track of: + +* A random `shard_id` in `[0...SHARD_COUNT-1]` (selected once and retained forever) +* A block header that they consider to be finalized (`finalized_header`) and do not expect to revert. +* `later_period_data = get_maximal_later_committee(finalized_header, shard_id)` +* `earlier_period_data = get_maximal_earlier_committee(finalized_header, shard_id)` + +We use the struct `validator_memory` to keep track of these variables. + +### Updating the shuffled committee + +If a client's `validator_memory.finalized_header` changes so that `header.slot // PERSISTENT_COMMITTEE_PERIOD` increases, then the client can ask the network for a `new_committee_proof = MerklePartial(get_maximal_later_committee, validator_memory.finalized_header, shard_id)`. It can then compute: + +```python +earlier_period_data = later_period_data +later_period_data = get_later_period_data(new_committee_proof, finalized_header, shard_id) +``` + +The maximum size of a proof is `128 * ((22-7) * 32 + 110) = 75520` bytes for validator records and `(22-7) * 32 + 128 * 8 = 1504` for the active index proof (much smaller because the relevant active indices are all beside each other in the Merkle tree). This needs to be done once per `PERSISTENT_COMMITTEE_PERIOD` epochs (2048 epochs / 9 days), or ~38 bytes per epoch. + +### Computing the current committee + +Here is a helper to compute the committee at a slot given the maximal earlier and later committees: + +```python +def compute_committee(header: BeaconBlockHeader, + validator_memory: ValidatorMemory): + + earlier_validator_count = validator_memory.earlier_period_data.validator_count + later_validator_count = validator_memory.later_period_data.validator_count + earlier_committee = validator_memory.earlier_period_data.committee + later_committee = validator_memory.later_period_data.committee + earlier_start_epoch = get_earlier_start_epoch(header.slot) + later_start_epoch = get_later_start_epoch(header.slot) + epoch = slot_to_epoch(header.slot) + + actual_committee_count = max( + earlier_validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE), + later_validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE), + ) + 1 + + def get_offset(count, end:bool): + return get_split_offset(count, + SHARD_COUNT * committee_count, + validator_memory.shard_id * committee_count + (1 if end else 0)) + + actual_earlier_committee = maximal_earlier_committee[ + 0:get_offset(earlier_validator_count, True) - get_offset(earlier_validator_count, False) + ] + actual_later_committee = maximal_later_committee[ + 0:get_offset(later_validator_count, True) - get_offset(later_validator_count, False) + ] + def get_switchover_epoch(index): + return ( + bytes_to_int(hash(validator_memory.earlier_period_data.seed + bytes3(index))[0:8]) % + PERSISTENT_COMMITTEE_PERIOD + ) + # Take not-yet-cycled-out validators from earlier committee and already-cycled-in validators from + # later committee; return a sorted list of the union of the two, deduplicated + return sorted(list(set( + [i for i in earlier_committee if epoch % PERSISTENT_COMMITTEE_PERIOD < get_switchover_epoch(i)] + + [i for i in later_committee if epoch % PERSISTENT_COMMITTEE_PERIOD >= get_switchover_epoch(i)] + ))) + +``` + +Note that this method makes use of the fact that the committee for any given shard always starts and ends at the same validator index independently of the committee count (this is because the validator set is split into `SHARD_COUNT * committee_count` slices but the first slice of a shard is a multiple `committee_count * i`, so the start of the slice is `n * committee_count * i // (SHARD_COUNT * committee_count) = n * i // SHARD_COUNT`, using the slightly nontrivial algebraic identity `(x * a) // ab == x // b`). + +### Verifying blocks + +If a client wants to update its `finalized_header` it asks the network for a `BlockValidityProof`, which is simply: + +```python +{ + 'header': BlockHeader, + 'shard_aggregate_signature': 'bytes96', + 'shard_bitfield': 'bytes', + 'shard_parent_block': ShardBlock +} +``` + +The verification procedure is as follows: + +```python +def verify_block_validity_proof(proof: BlockValidityProof, validator_memory: ValidatorMemory) -> bool: + assert proof.shard_parent_block.beacon_chain_ref == hash_tree_root(proof.header) + committee = compute_committee(proof.header, validator_memory) + # Verify that we have >=50% support + support_balance = sum([c.high_balance for i, c in enumerate(committee) if get_bitfield_bit(proof.shard_bitfield, i) is True]) + total_balance = sum([c.high_balance for i, c in enumerate(committee)] + assert support_balance * 2 > total_balance + # Verify shard attestations + group_public_key = bls_aggregate_pubkeys([ + v.pubkey for v, index in enumerate(committee) if + get_bitfield_bit(proof.shard_bitfield, i) is True + ]) + assert bls_verify( + pubkey=group_public_key, + message_hash=hash_tree_root(shard_parent_block), + signature=shard_aggregate_signature, + domain=get_domain(state, slot_to_epoch(shard_block.slot), DOMAIN_SHARD_ATTESTER) + ) +``` +The size of this proof is only 200 (header) + 96 (signature) + 16 (bitfield) + 352 (shard block) = 664 bytes. It can be reduced further by replacing `ShardBlock` with `MerklePartial(lambda x: x.beacon_chain_ref, ShardBlock)`, which would cut off ~220 bytes. From 154eec0d027468d1a228f7f76e233eec9c4320ff Mon Sep 17 00:00:00 2001 From: Vitalik Buterin Date: Wed, 13 Mar 2019 03:04:16 -0500 Subject: [PATCH 02/21] Added links to light client docs in the readme --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c5c88daf9..e37539e3b 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ Accompanying documents can be found in [specs](specs) and include * [BLS signature verification](specs/bls_signature.md) * [General test format](specs/test-format.md) * [Honest validator implementation doc](specs/validator/0_beacon-chain-validator.md) +* [Merkle proof formats](specs/light_client/merkle_proofs.md) +* [Light client syncing protocol](specs/light_client/sync_protocol.md) ## Design goals The following are the broad design goals for Ethereum 2.0: From b65601afdae18b35c9aad8dfa25c1c677f757ec0 Mon Sep 17 00:00:00 2001 From: Vitalik Buterin Date: Thu, 14 Mar 2019 08:29:03 -0500 Subject: [PATCH 03/21] Updated Merkle proof file --- specs/light_client/merkle_proofs.md | 63 +++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/specs/light_client/merkle_proofs.md b/specs/light_client/merkle_proofs.md index cf4dad2e3..f52941118 100644 --- a/specs/light_client/merkle_proofs.md +++ b/specs/light_client/merkle_proofs.md @@ -1,3 +1,9 @@ +### Constants + +| Name | Value | +| - | - | +| `LENGTH_FLAG` | `2**64 - 1` | + ### Generalized Merkle tree index In a binary Merkle tree, we define a "generalized index" of a node as `2**depth + index`. Visually, this looks as follows: @@ -36,17 +42,34 @@ y_data_root len(y) ....... ``` -We can now define a concept of a "path", a way of describing a function that takes as input an SSZ object and outputs some specific (possibly deeply nested) member. For example, `foo -> foo.x` is a path, as are `foo -> len(foo.y)` and `foo -> foo[5]`. We'll describe paths as lists: in these three cases they are `["x"]`, `["y", "len"]` and `["y", 5]` respectively. We can now define a function `get_generalized_indices(object: Any, path: List[str OR int], root=1: int) -> int` that converts an object and a path to a set of generalized indices (note that for constant-sized objects, there is only one generalized index and it only depends on the path, but for dynamically sized objects the indices may depend on the object itself too). For dynamically-sized objects, the set of indices will have more than one member because of the need to access an array's length to determine the correct generalized index for some array access. +We can now define a concept of a "path", a way of describing a function that takes as input an SSZ object and outputs some specific (possibly deeply nested) member. For example, `foo -> foo.x` is a path, as are `foo -> len(foo.y)` and `foo -> foo.y[5].w`. We'll describe paths as lists, which can have two representations. In "human-readable form", they are `["x"]`, `["y", "__len__"]` and `["y", 5, "w"]` respectively. In "encoded form", they are lists of `uint64` values, in these cases (assuming the fields of `foo` in order are `x` then `y`, and `w` is the first field of `y[i]`) `[0]`, `[1, 2**64-1]`, `[1, 5, 0]`. ```python -def get_generalized_indices(obj: Any, path: List[str or int], root=1) -> List[int]: +def path_to_encoded_form(obj: Any, path: List[str or int]) -> List[int]: + if len(path) == 0: + return [] + if isinstance(path[0], "__len__"): + assert len(path) == 1 + return [LENGTH_FLAG] + elif isinstance(path[0], str) and hasattr(obj, "fields"): + return [list(obj.fields.keys()).index(path[0])] + path_to_encoded_form(getattr(obj, path[0]), path[1:]) + elif isinstance(obj, (StaticList, DynamicList)): + return [path[0]] + path_to_encoded_form(obj[path[0]], path[1:]) + else: + raise Exception("Unknown type / path") +``` + +We can now define a function `get_generalized_indices(object: Any, path: List[int], root=1: int) -> int` that converts an object and a path to a set of generalized indices (note that for constant-sized objects, there is only one generalized index and it only depends on the path, but for dynamically sized objects the indices may depend on the object itself too). For dynamically-sized objects, the set of indices will have more than one member because of the need to access an array's length to determine the correct generalized index for some array access. + +```python +def get_generalized_indices(obj: Any, path: List[int], root=1) -> List[int]: if len(path) == 0: return [root] elif isinstance(obj, StaticList): items_per_chunk = (32 // len(serialize(x))) if isinstance(x, int) else 1 new_root = root * next_power_of_2(len(obj) // items_per_chunk) + path[0] // items_per_chunk return get_generalized_indices(obj[path[0]], path[1:], new_root) - elif isinstance(obj, DynamicList) and path[0] == "len": + elif isinstance(obj, DynamicList) and path[0] == LENGTH_FLAG: return [root * 2 + 1] elif isinstance(obj, DynamicList) and isinstance(path[0], int): assert path[0] < len(obj) @@ -54,9 +77,9 @@ def get_generalized_indices(obj: Any, path: List[str or int], root=1) -> List[in new_root = root * 2 * next_power_of_2(len(obj) // items_per_chunk) + path[0] // items_per_chunk return [root *2 + 1] + get_generalized_indices(obj[path[0]], path[1:], new_root) elif hasattr(obj, "fields"): - index = list(fields.keys()).index(path[0]) - new_root = root * next_power_of_2(len(fields)) + index - return get_generalized_indices(getattr(obj, path[0]), path[1:], new_root) + field = list(fields.keys())[path[0]] + new_root = root * next_power_of_2(len(fields)) + path[0] + return get_generalized_indices(getattr(obj, field), path[1:], new_root) else: raise Exception("Unknown type / path") ``` @@ -109,6 +132,8 @@ def get_proof_indices(tree_indices: List[int]) -> List[int]: Generating a proof is simply a matter of taking the node of the SSZ hash tree with the union of the given generalized indices for each index given by `get_proof_indices`, and outputting the list of nodes in the same order. +Here is the verification function: + ```python def verify_multi_proof(root, indices, leaves, proof): tree = {} @@ -127,8 +152,32 @@ def verify_multi_proof(root, indices, leaves, proof): return (indices == []) or (1 in tree and tree[1] == root) ``` +### MerklePartial + +We define: + +#### `MerklePartialLeaf` + +```python +{ + "path": ["uint64"], + "value": "bytes32" +} +``` + +#### `MerklePartial` + + +```python +{ + "root": "bytes32", + "values": [MerklePartialLeaf], + "proof": ["bytes32"] +} +``` + #### Proofs for execution -We define `MerklePartial(f, arg1, arg2...)` as being a list of Merkle multiproofs of the sets of nodes in the hash trees of the SSZ objects that are needed to authenticate the values needed to compute some function `f(arg1, arg2...)`. An individual Merkle multiproof is given as a dynamic sized list of `bytes32` values, a `MerklePartial` is a fixed-size list of objects `{proof: ["bytes32"], value: "bytes32"}`, one for each `arg` to `f` (if some `arg` is a base type, then the multiproof is empty). +We define `MerklePartial(f, arg1, arg2..., focus=0)` as being a `MerklePartial` object wrapping a Merkle multiproof of the set of nodes in the hash tree of the SSZ object `arg[focus]` that is needed to authenticate the parts of the object needed to compute `f(arg1, arg2...)`. Ideally, any function which accepts an SSZ object should also be able to accept a `MerklePartial` object as a substitute. From 22be21223b90160f4f772146d821b34848ee3572 Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 15 Mar 2019 11:24:59 +0000 Subject: [PATCH 04/21] Update merkle_proofs.md --- specs/light_client/merkle_proofs.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/light_client/merkle_proofs.md b/specs/light_client/merkle_proofs.md index f52941118..311a4aa5c 100644 --- a/specs/light_client/merkle_proofs.md +++ b/specs/light_client/merkle_proofs.md @@ -1,3 +1,5 @@ +**NOTICE**: This document is a work-in-progress for researchers and implementers. + ### Constants | Name | Value | From b566722b52ad2cd9f92a0b48953e3b784a04853d Mon Sep 17 00:00:00 2001 From: Justin Date: Fri, 15 Mar 2019 11:25:15 +0000 Subject: [PATCH 05/21] Update sync_protocol.md --- specs/light_client/sync_protocol.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/specs/light_client/sync_protocol.md b/specs/light_client/sync_protocol.md index 2a70dbb31..b84d55dcf 100644 --- a/specs/light_client/sync_protocol.md +++ b/specs/light_client/sync_protocol.md @@ -1,3 +1,5 @@ +**NOTICE**: This document is a work-in-progress for researchers and implementers. + # Beacon chain light client syncing One of the design goals of the eth2 beacon chain is light-client friendlines, both to allow low-resource clients (mobile phones, IoT, etc) to maintain access to the blockchain in a reasonably safe way, but also to facilitate the development of "bridges" between the eth2 beacon chain and other chains. From 1967a8939d54601e56946705453f304118042bb5 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Sun, 17 Mar 2019 06:25:56 -0500 Subject: [PATCH 06/21] Fixed some variable names --- specs/light_client/sync_protocol.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/specs/light_client/sync_protocol.md b/specs/light_client/sync_protocol.md index b84d55dcf..2a420abcb 100644 --- a/specs/light_client/sync_protocol.md +++ b/specs/light_client/sync_protocol.md @@ -69,14 +69,14 @@ A light client will keep track of: * A random `shard_id` in `[0...SHARD_COUNT-1]` (selected once and retained forever) * A block header that they consider to be finalized (`finalized_header`) and do not expect to revert. -* `later_period_data = get_maximal_later_committee(finalized_header, shard_id)` -* `earlier_period_data = get_maximal_earlier_committee(finalized_header, shard_id)` +* `later_period_data = get_later_period_data(finalized_header, shard_id)` +* `earlier_period_data = get_earlier_period_data(finalized_header, shard_id)` We use the struct `validator_memory` to keep track of these variables. ### Updating the shuffled committee -If a client's `validator_memory.finalized_header` changes so that `header.slot // PERSISTENT_COMMITTEE_PERIOD` increases, then the client can ask the network for a `new_committee_proof = MerklePartial(get_maximal_later_committee, validator_memory.finalized_header, shard_id)`. It can then compute: +If a client's `validator_memory.finalized_header` changes so that `header.slot // PERSISTENT_COMMITTEE_PERIOD` increases, then the client can ask the network for a `new_committee_proof = MerklePartial(get_later_period_data, validator_memory.finalized_header, shard_id)`. It can then compute: ```python earlier_period_data = later_period_data @@ -95,13 +95,13 @@ def compute_committee(header: BeaconBlockHeader, earlier_validator_count = validator_memory.earlier_period_data.validator_count later_validator_count = validator_memory.later_period_data.validator_count - earlier_committee = validator_memory.earlier_period_data.committee - later_committee = validator_memory.later_period_data.committee + maximal_earlier_committee = validator_memory.earlier_period_data.committee + maximal_later_committee = validator_memory.later_period_data.committee earlier_start_epoch = get_earlier_start_epoch(header.slot) later_start_epoch = get_later_start_epoch(header.slot) epoch = slot_to_epoch(header.slot) - actual_committee_count = max( + committee_count = max( earlier_validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE), later_validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE), ) + 1 From ced6208d55d26d63f532d4bb031869740b2a111c Mon Sep 17 00:00:00 2001 From: vbuterin Date: Wed, 20 Mar 2019 11:49:28 -0500 Subject: [PATCH 07/21] Edits * shuffled committee -> period committee * Reduced code redundancy --- specs/light_client/sync_protocol.md | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/specs/light_client/sync_protocol.md b/specs/light_client/sync_protocol.md index 2a420abcb..a8cdd50c2 100644 --- a/specs/light_client/sync_protocol.md +++ b/specs/light_client/sync_protocol.md @@ -37,25 +37,15 @@ We add a data type `PeriodData` and four helpers: ```python def get_earlier_start_epoch(slot: Slot) -> int: return slot - slot % PERSISTENT_COMMITTEE_PERIOD - PERSISTENT_COMMITTEE_PERIOD * 2 + def get_later_start_epoch(slot: Slot) -> int: return slot - slot % PERSISTENT_COMMITTEE_PERIOD - PERSISTENT_COMMITTEE_PERIOD -def get_earlier_period_data(block: ExtendedBeaconBlock, shard_id: Shard) -> PeriodData: - period_start = get_earlier_start_epoch(header.slot) +def get_period_data(block: ExtendedBeaconBlock, shard_id: Shard, later: bool) -> PeriodData: + period_start = get_later_start_epoch(header.slot) if later else get_earlier_start_epoch(header.slot) validator_count = len(get_active_validator_indices(state, period_start)) committee_count = validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE) + 1 - indices = get_shuffled_committee(block.state, shard_id, period_start, 0, committee_count) - return PeriodData( - validator_count, - generate_seed(block.state, period_start), - [block.state.validator_registry[i] for i in indices] - ) - -def get_later_period_data(block: ExtendedBeaconBlock, shard_id: Shard) -> PeriodData: - period_start = get_later_start_epoch(header.slot) - validator_count = len(get_active_validator_indices(state, period_start)) - committee_count = validator_count // (SHARD_COUNT * TARGET_COMMITTEE_SIZE) + 1 - indices = get_shuffled_committee(block.state, shard_id, period_start, 0, committee_count) + indices = get_period_committee(block.state, shard_id, period_start, 0, committee_count) return PeriodData( validator_count, generate_seed(block.state, period_start), @@ -69,18 +59,18 @@ A light client will keep track of: * A random `shard_id` in `[0...SHARD_COUNT-1]` (selected once and retained forever) * A block header that they consider to be finalized (`finalized_header`) and do not expect to revert. -* `later_period_data = get_later_period_data(finalized_header, shard_id)` -* `earlier_period_data = get_earlier_period_data(finalized_header, shard_id)` +* `later_period_data = get_period_data(finalized_header, shard_id, later=True)` +* `earlier_period_data = get_period_data(finalized_header, shard_id, later=False)` We use the struct `validator_memory` to keep track of these variables. ### Updating the shuffled committee -If a client's `validator_memory.finalized_header` changes so that `header.slot // PERSISTENT_COMMITTEE_PERIOD` increases, then the client can ask the network for a `new_committee_proof = MerklePartial(get_later_period_data, validator_memory.finalized_header, shard_id)`. It can then compute: +If a client's `validator_memory.finalized_header` changes so that `header.slot // PERSISTENT_COMMITTEE_PERIOD` increases, then the client can ask the network for a `new_committee_proof = MerklePartial(get_period_data, validator_memory.finalized_header, shard_id, later=True)`. It can then compute: ```python earlier_period_data = later_period_data -later_period_data = get_later_period_data(new_committee_proof, finalized_header, shard_id) +later_period_data = get_period_data(new_committee_proof, finalized_header, shard_id, later=True) ``` The maximum size of a proof is `128 * ((22-7) * 32 + 110) = 75520` bytes for validator records and `(22-7) * 32 + 128 * 8 = 1504` for the active index proof (much smaller because the relevant active indices are all beside each other in the Merkle tree). This needs to be done once per `PERSISTENT_COMMITTEE_PERIOD` epochs (2048 epochs / 9 days), or ~38 bytes per epoch. From b91dfd6244f7196827a311d879aa29ad7eb83f05 Mon Sep 17 00:00:00 2001 From: Vitalik Buterin Date: Tue, 26 Mar 2019 12:56:35 -0500 Subject: [PATCH 08/21] Add merkle_parial_from_paths --- specs/light_client/merkle_proofs.md | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/specs/light_client/merkle_proofs.md b/specs/light_client/merkle_proofs.md index 311a4aa5c..2e92488cb 100644 --- a/specs/light_client/merkle_proofs.md +++ b/specs/light_client/merkle_proofs.md @@ -158,26 +158,32 @@ def verify_multi_proof(root, indices, leaves, proof): We define: -#### `MerklePartialLeaf` - -```python -{ - "path": ["uint64"], - "value": "bytes32" -} -``` - #### `MerklePartial` ```python { "root": "bytes32", - "values": [MerklePartialLeaf], + "indices": ["uint64"], + "values": ["bytes32"], "proof": ["bytes32"] } ``` +#### `merkle_partial_from_paths` + +```python +def merkle_partial_from_paths(obj, paths): + indices = set() + for path in paths: + indices = indices.union(get_generalized_indices(obj, path)) + return MerklePartial( + root=hash_tree_root(obj), + indices=indices, + values= mk_multi_proof + ) +``` + #### Proofs for execution We define `MerklePartial(f, arg1, arg2..., focus=0)` as being a `MerklePartial` object wrapping a Merkle multiproof of the set of nodes in the hash tree of the SSZ object `arg[focus]` that is needed to authenticate the parts of the object needed to compute `f(arg1, arg2...)`. From e4c3c556d5bd820698896c76325ac5013351102f Mon Sep 17 00:00:00 2001 From: Justin Date: Sun, 31 Mar 2019 09:02:10 +0400 Subject: [PATCH 09/21] Rename "transaction" to "operation" Fix #822. --- specs/core/0_beacon-chain.md | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index d08828692..5be13cdb5 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -18,7 +18,7 @@ - [Time parameters](#time-parameters) - [State list lengths](#state-list-lengths) - [Reward and penalty quotients](#reward-and-penalty-quotients) - - [Max transactions per block](#max-transactions-per-block) + - [Max operations per block](#max-operations-per-block) - [Signature domains](#signature-domains) - [Data structures](#data-structures) - [Misc dependencies](#misc-dependencies) @@ -34,7 +34,7 @@ - [`Validator`](#validator) - [`PendingAttestation`](#pendingattestation) - [`HistoricalBatch`](#historicalbatch) - - [Beacon transactions](#beacon-transactions) + - [Beacon operations](#beacon-operations) - [`ProposerSlashing`](#proposerslashing) - [`AttesterSlashing`](#attesterslashing) - [`Attestation`](#attestation) @@ -132,7 +132,7 @@ - [Block header](#block-header) - [RANDAO](#randao) - [Eth1 data](#eth1-data-1) - - [Transactions](#transactions) + - [Operations](#operations) - [Proposer slashings](#proposer-slashings) - [Attester slashings](#attester-slashings) - [Attestations](#attestations) @@ -261,8 +261,7 @@ Code snippets appearing in `this style` are to be interpreted as Python code. * The `BASE_REWARD_QUOTIENT` parameter dictates the per-epoch reward. It corresponds to ~2.54% annual interest assuming 10 million participating ETH in every epoch. * The `INACTIVITY_PENALTY_QUOTIENT` equals `INVERSE_SQRT_E_DROP_TIME**2` where `INVERSE_SQRT_E_DROP_TIME := 2**12 epochs` (~18 days) is the time it takes the inactivity penalty to reduce the balance of non-participating [validators](#dfn-validator) to about `1/sqrt(e) ~= 60.6%`. Indeed, the balance retained by offline [validators](#dfn-validator) after `n` epochs is about `(1 - 1/INACTIVITY_PENALTY_QUOTIENT)**(n**2/2)` so after `INVERSE_SQRT_E_DROP_TIME` epochs it is roughly `(1 - 1/INACTIVITY_PENALTY_QUOTIENT)**(INACTIVITY_PENALTY_QUOTIENT/2) ~= 1/sqrt(e)`. - -### Max transactions per block +### Max operations per block | Name | Value | | - | - | @@ -460,7 +459,7 @@ The types are defined topologically to aid in facilitating an executable version } ``` -### Beacon transactions +### Beacon operations #### `ProposerSlashing` @@ -2234,7 +2233,7 @@ def process_eth1_data(state: BeaconState, block: BeaconBlock) -> None: state.eth1_data_votes.append(Eth1DataVote(eth1_data=block.body.eth1_data, vote_count=1)) ``` -#### Transactions +#### Operations ##### Proposer slashings @@ -2246,7 +2245,7 @@ For each `proposer_slashing` in `block.body.proposer_slashings`, run the followi def process_proposer_slashing(state: BeaconState, proposer_slashing: ProposerSlashing) -> None: """ - Process ``ProposerSlashing`` transaction. + Process ``ProposerSlashing`` operation. Note that this function mutates ``state``. """ proposer = state.validator_registry[proposer_slashing.proposer_index] @@ -2277,7 +2276,7 @@ For each `attester_slashing` in `block.body.attester_slashings`, run the followi def process_attester_slashing(state: BeaconState, attester_slashing: AttesterSlashing) -> None: """ - Process ``AttesterSlashing`` transaction. + Process ``AttesterSlashing`` operation. Note that this function mutates ``state``. """ attestation1 = attester_slashing.attestation_1 @@ -2312,7 +2311,7 @@ For each `attestation` in `block.body.attestations`, run the following function: ```python def process_attestation(state: BeaconState, attestation: Attestation) -> None: """ - Process ``Attestation`` transaction. + Process ``Attestation`` operation. Note that this function mutates ``state``. """ assert max(GENESIS_SLOT, state.slot - SLOTS_PER_EPOCH) <= attestation.data.slot @@ -2367,7 +2366,7 @@ For each `exit` in `block.body.voluntary_exits`, run the following function: ```python def process_voluntary_exit(state: BeaconState, exit: VoluntaryExit) -> None: """ - Process ``VoluntaryExit`` transaction. + Process ``VoluntaryExit`` operation. Note that this function mutates ``state``. """ validator = state.validator_registry[exit.validator_index] @@ -2403,7 +2402,7 @@ For each `transfer` in `block.body.transfers`, run the following function: ```python def process_transfer(state: BeaconState, transfer: Transfer) -> None: """ - Process ``Transfer`` transaction. + Process ``Transfer`` operation. Note that this function mutates ``state``. """ # Verify the amount and fee aren't individually too big (for anti-overflow purposes) From 77b0a4188bf573c3174db26d36a0e2348a33b5a2 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sun, 31 Mar 2019 22:54:46 +0800 Subject: [PATCH 10/21] Update `1_custody-game.md` as well --- specs/core/1_custody-game.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/specs/core/1_custody-game.md b/specs/core/1_custody-game.md index fd754634e..e28536d34 100644 --- a/specs/core/1_custody-game.md +++ b/specs/core/1_custody-game.md @@ -13,7 +13,7 @@ - [Constants](#constants) - [Misc](#misc) - [Time parameters](#time-parameters) - - [Max transactions per block](#max-transactions-per-block) + - [Max operations per block](#max-operations-per-block) - [Signature domains](#signature-domains) - [Data structures](#data-structures) - [Custody objects](#custody-objects) @@ -33,7 +33,7 @@ - [`epoch_to_custody_period`](#epoch_to_custody_period) - [`verify_custody_key`](#verify_custody_key) - [Per-block processing](#per-block-processing) - - [Transactions](#transactions) + - [Operations](#operations) - [Custody reveals](#custody-reveals) - [Chunk challenges](#chunk-challenges) - [Bit challenges](#bit-challenges) @@ -79,7 +79,7 @@ This document details the beacon chain additions and changes in Phase 1 of Ether | `EPOCHS_PER_CUSTODY_PERIOD` | `2**11` (= 2,048) | epochs | ~9 days | | `CUSTODY_RESPONSE_DEADLINE` | `2**14` (= 16,384) | epochs | ~73 days | -### Max transactions per block +### Max operations per block | Name | Value | | - | - | @@ -259,9 +259,9 @@ def verify_custody_key(state: BeaconState, reveal: CustodyKeyReveal) -> bool: ## Per-block processing -### Transactions +### Operations -Add the following transactions to the per-block processing, in order the given below and after all other transactions in phase 0. +Add the following operations to the per-block processing, in order the given below and after all other operations in phase 0. #### Custody reveals From 93540f9662a94d23bb993765adf8df1b24ad3346 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sun, 31 Mar 2019 23:00:21 +0800 Subject: [PATCH 11/21] Rename transaction to operation in tests --- utils/phase0/state_transition.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/utils/phase0/state_transition.py b/utils/phase0/state_transition.py index cfd941c42..2c420014f 100644 --- a/utils/phase0/state_transition.py +++ b/utils/phase0/state_transition.py @@ -22,31 +22,31 @@ def expected_deposit_count(state: BeaconState) -> int: ) -def process_transaction_type(state: BeaconState, - transactions: List[Any], - max_transactions: int, - tx_fn: Callable[[BeaconState, Any], None]) -> None: - assert len(transactions) <= max_transactions - for transaction in transactions: - tx_fn(state, transaction) +def process_operation_type(state: BeaconState, + operations: List[Any], + max_operations: int, + tx_fn: Callable[[BeaconState, Any], None]) -> None: + assert len(operations) <= max_operations + for operation in operations: + tx_fn(state, operation) -def process_transactions(state: BeaconState, block: BeaconBlock) -> None: - process_transaction_type( +def process_operations(state: BeaconState, block: BeaconBlock) -> None: + process_operation_type( state, block.body.proposer_slashings, spec.MAX_PROPOSER_SLASHINGS, spec.process_proposer_slashing, ) - process_transaction_type( + process_operation_type( state, block.body.attester_slashings, spec.MAX_ATTESTER_SLASHINGS, spec.process_attester_slashing, ) - process_transaction_type( + process_operation_type( state, block.body.attestations, spec.MAX_ATTESTATIONS, @@ -54,14 +54,14 @@ def process_transactions(state: BeaconState, block: BeaconBlock) -> None: ) assert len(block.body.deposits) == expected_deposit_count(state) - process_transaction_type( + process_operation_type( state, block.body.deposits, spec.MAX_DEPOSITS, spec.process_deposit, ) - process_transaction_type( + process_operation_type( state, block.body.voluntary_exits, spec.MAX_VOLUNTARY_EXITS, @@ -69,7 +69,7 @@ def process_transactions(state: BeaconState, block: BeaconBlock) -> None: ) assert len(block.body.transfers) == len(set(block.body.transfers)) - process_transaction_type( + process_operation_type( state, block.body.transfers, spec.MAX_TRANSFERS, @@ -84,7 +84,7 @@ def process_block(state: BeaconState, spec.process_randao(state, block) spec.process_eth1_data(state, block) - process_transactions(state, block) + process_operations(state, block) if verify_state_root: spec.verify_block_state_root(state, block) From 06d005999a3b8ebcde35ed89593e6efb573f3afc Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 2 Apr 2019 13:50:06 +1100 Subject: [PATCH 12/21] fix validator_indicies issue in process_attester_slashing --- specs/core/0_beacon-chain.md | 6 ++++-- tests/phase0/helpers.py | 13 +++++++++++++ tests/phase0/test_sanity.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 2 deletions(-) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index d08828692..ced7a7210 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -2291,10 +2291,12 @@ def process_attester_slashing(state: BeaconState, assert verify_indexed_attestation(state, attestation1) assert verify_indexed_attestation(state, attestation2) + validator_indices_1 = attestation1.custody_bit_0_indices + attestation1.custody_bit_1_indices + validator_indices_2 = attestation2.custody_bit_0_indices + attestation2.custody_bit_1_indices slashable_indices = [ - index for index in attestation1.validator_indices + index for index in validator_indices_1 if ( - index in attestation2.validator_indices and + index in validator_indices_2 and is_slashable_validator(state.validator_registry[index], get_current_epoch(state)) ) ] diff --git a/tests/phase0/helpers.py b/tests/phase0/helpers.py index e5e335d80..33f394def 100644 --- a/tests/phase0/helpers.py +++ b/tests/phase0/helpers.py @@ -12,6 +12,7 @@ from build.phase0.spec import ( Attestation, AttestationData, AttestationDataAndCustodyBit, + AttesterSlashing, BeaconBlockHeader, Deposit, DepositData, @@ -19,6 +20,7 @@ from build.phase0.spec import ( ProposerSlashing, VoluntaryExit, # functions + convert_to_indexed, get_active_validator_indices, get_attestation_participants, get_block_root, @@ -244,6 +246,17 @@ def get_valid_proposer_slashing(state): ) +def get_valid_attester_slashing(state): + attestation_1 = get_valid_attestation(state) + attestation_2 = deepcopy(attestation_1) + attestation_2.data.target_root = b'\x01'*32 + + return AttesterSlashing( + attestation_1=convert_to_indexed(state, attestation_1), + attestation_2=convert_to_indexed(state, attestation_2), + ) + + def get_valid_attestation(state, slot=None): if slot is None: slot = state.slot diff --git a/tests/phase0/test_sanity.py b/tests/phase0/test_sanity.py index 3b4497ca5..8e6bd2e94 100644 --- a/tests/phase0/test_sanity.py +++ b/tests/phase0/test_sanity.py @@ -40,6 +40,7 @@ from tests.phase0.helpers import ( build_empty_block_for_next_slot, force_registry_change_at_next_epoch, get_valid_attestation, + get_valid_attester_slashing, get_valid_proposer_slashing, privkeys, pubkeys, @@ -140,6 +141,33 @@ def test_proposer_slashing(state): return state, [block], test_state +def test_attester_slashing(state): + test_state = deepcopy(state) + attester_slashing = get_valid_attester_slashing(state) + validator_index = attester_slashing.attestation_1.custody_bit_0_indices[0] + + # + # Add to state via block transition + # + block = build_empty_block_for_next_slot(test_state) + block.body.attester_slashings.append(attester_slashing) + state_transition(test_state, block) + + assert not state.validator_registry[validator_index].initiated_exit + assert not state.validator_registry[validator_index].slashed + + slashed_validator = test_state.validator_registry[validator_index] + assert not slashed_validator.initiated_exit + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + # lost whistleblower reward + assert get_balance(test_state, validator_index) < get_balance(state, validator_index) + + return state, [block], test_state + + + def test_deposit_in_block(state): pre_state = deepcopy(state) test_deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) From e037412f94d7c24477cd7723470e1735b866d920 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 2 Apr 2019 14:04:04 +1100 Subject: [PATCH 13/21] add process attester slashing tests --- .../test_process_attester_slashing.py | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 tests/phase0/block_processing/test_process_attester_slashing.py diff --git a/tests/phase0/block_processing/test_process_attester_slashing.py b/tests/phase0/block_processing/test_process_attester_slashing.py new file mode 100644 index 000000000..ed6d9adae --- /dev/null +++ b/tests/phase0/block_processing/test_process_attester_slashing.py @@ -0,0 +1,97 @@ +from copy import deepcopy +import pytest + +import build.phase0.spec as spec +from build.phase0.spec import ( + get_balance, + get_current_epoch, + process_attester_slashing, +) +from tests.phase0.helpers import ( + get_valid_attester_slashing, +) + +# mark entire file as 'attester_slashing' +pytestmark = pytest.mark.attester_slashings + + +def run_attester_slashing_processing(state, attester_slashing, valid=True): + """ + Run ``process_attester_slashing`` returning the pre and post state. + If ``valid == False``, run expecting ``AssertionError`` + """ + post_state = deepcopy(state) + + if not valid: + with pytest.raises(AssertionError): + process_attester_slashing(post_state, attester_slashing) + return state, None + + process_attester_slashing(post_state, attester_slashing) + + validator_index = attester_slashing.attestation_1.custody_bit_0_indices[0] + slashed_validator = post_state.validator_registry[validator_index] + assert not slashed_validator.initiated_exit + assert slashed_validator.slashed + assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH + assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH + # lost whistleblower reward + assert ( + get_balance(post_state, validator_index) < + get_balance(state, validator_index) + ) + + return state, post_state + + +def test_success_double(state): + attester_slashing = get_valid_attester_slashing(state) + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing) + + return pre_state, attester_slashing, post_state + + +def test_success_surround(state): + attester_slashing = get_valid_attester_slashing(state) + + # set attestion1 to surround attestation 2 + attester_slashing.attestation_1.data.source_epoch = attester_slashing.attestation_2.data.source_epoch - 1 + attester_slashing.attestation_1.data.slot = attester_slashing.attestation_2.data.slot + spec.SLOTS_PER_EPOCH + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing) + + return pre_state, attester_slashing, post_state + + +def test_same_data(state): + attester_slashing = get_valid_attester_slashing(state) + + attester_slashing.attestation_1.data = attester_slashing.attestation_2.data + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) + + return pre_state, attester_slashing, post_state + + +def test_no_double_or_surround(state): + attester_slashing = get_valid_attester_slashing(state) + + attester_slashing.attestation_1.data.slot += spec.SLOTS_PER_EPOCH + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) + + return pre_state, attester_slashing, post_state + +def test_participants_already_slashed(state): + attester_slashing = get_valid_attester_slashing(state) + + # set all indices to slashed + attestation_1 = attester_slashing.attestation_1 + validator_indices = attestation_1.custody_bit_0_indices + attestation_1.custody_bit_1_indices + for index in validator_indices: + state.validator_registry[index].slashed = True + + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) + + return pre_state, attester_slashing, post_state From 577fc740d09dcbb66848e84dbb141b75ce6952df Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Tue, 2 Apr 2019 14:08:22 +1100 Subject: [PATCH 14/21] lint --- tests/phase0/block_processing/test_process_attester_slashing.py | 1 + tests/phase0/test_sanity.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phase0/block_processing/test_process_attester_slashing.py b/tests/phase0/block_processing/test_process_attester_slashing.py index ed6d9adae..fcbc8f78b 100644 --- a/tests/phase0/block_processing/test_process_attester_slashing.py +++ b/tests/phase0/block_processing/test_process_attester_slashing.py @@ -83,6 +83,7 @@ def test_no_double_or_surround(state): return pre_state, attester_slashing, post_state + def test_participants_already_slashed(state): attester_slashing = get_valid_attester_slashing(state) diff --git a/tests/phase0/test_sanity.py b/tests/phase0/test_sanity.py index 8e6bd2e94..0010cb22f 100644 --- a/tests/phase0/test_sanity.py +++ b/tests/phase0/test_sanity.py @@ -167,7 +167,6 @@ def test_attester_slashing(state): return state, [block], test_state - def test_deposit_in_block(state): pre_state = deepcopy(state) test_deposit_data_leaves = [ZERO_HASH] * len(pre_state.validator_registry) From 06ba5fedd7886d57b9794c4c61af33de040f8aa7 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Tue, 2 Apr 2019 13:18:41 -0500 Subject: [PATCH 15/21] Add link to custody game to readme (#867) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c5c88daf9..ce0ae8738 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ This repo hosts the current eth2.0 specifications. Discussions about design rati Core specifications for eth2.0 client validation can be found in [specs/core](specs/core). These are divided into phases. Each subsequent phase depends upon the prior. The current phases specified are: * [Phase 0 -- The Beacon Chain](specs/core/0_beacon-chain.md) +* [Phase 1 -- Custody game](specs/core/1_custody-game.md) * [Phase 1 -- Shard Data Chains](specs/core/1_shard-data-chains.md) Accompanying documents can be found in [specs](specs) and include From 014138baab78554e77e18620a9ba450787d800f1 Mon Sep 17 00:00:00 2001 From: Danny Ryan Date: Wed, 3 Apr 2019 11:04:12 +1100 Subject: [PATCH 16/21] pr feedback --- specs/core/0_beacon-chain.md | 11 +++++--- .../test_process_attester_slashing.py | 27 +++++++++++++++---- tests/phase0/test_sanity.py | 8 ++++++ 3 files changed, 37 insertions(+), 9 deletions(-) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index ced7a7210..8b3f51c0a 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -1187,6 +1187,9 @@ def verify_indexed_attestation(state: BeaconState, indexed_attestation: IndexedA custody_bit_0_indices = indexed_attestation.custody_bit_0_indices custody_bit_1_indices = indexed_attestation.custody_bit_1_indices + # ensure no duplicate indices across custody bits + assert len(set(custody_bit_0_indices).intersection(set(custody_bit_1_indices))) == 0 + if len(custody_bit_1_indices) > 0: # [TO BE REMOVED IN PHASE 1] return False @@ -2291,12 +2294,12 @@ def process_attester_slashing(state: BeaconState, assert verify_indexed_attestation(state, attestation1) assert verify_indexed_attestation(state, attestation2) - validator_indices_1 = attestation1.custody_bit_0_indices + attestation1.custody_bit_1_indices - validator_indices_2 = attestation2.custody_bit_0_indices + attestation2.custody_bit_1_indices + attesting_indices_1 = attestation1.custody_bit_0_indices + attestation1.custody_bit_1_indices + attesting_indices_2 = attestation2.custody_bit_0_indices + attestation2.custody_bit_1_indices slashable_indices = [ - index for index in validator_indices_1 + index for index in attesting_indices_1 if ( - index in validator_indices_2 and + index in attesting_indices_2 and is_slashable_validator(state.validator_registry[index], get_current_epoch(state)) ) ] diff --git a/tests/phase0/block_processing/test_process_attester_slashing.py b/tests/phase0/block_processing/test_process_attester_slashing.py index fcbc8f78b..06f214c4b 100644 --- a/tests/phase0/block_processing/test_process_attester_slashing.py +++ b/tests/phase0/block_processing/test_process_attester_slashing.py @@ -4,7 +4,7 @@ import pytest import build.phase0.spec as spec from build.phase0.spec import ( get_balance, - get_current_epoch, + get_beacon_proposer_index, process_attester_slashing, ) from tests.phase0.helpers import ( @@ -29,16 +29,22 @@ def run_attester_slashing_processing(state, attester_slashing, valid=True): process_attester_slashing(post_state, attester_slashing) - validator_index = attester_slashing.attestation_1.custody_bit_0_indices[0] - slashed_validator = post_state.validator_registry[validator_index] + slashed_index = attester_slashing.attestation_1.custody_bit_0_indices[0] + slashed_validator = post_state.validator_registry[slashed_index] assert not slashed_validator.initiated_exit assert slashed_validator.slashed assert slashed_validator.exit_epoch < spec.FAR_FUTURE_EPOCH assert slashed_validator.withdrawable_epoch < spec.FAR_FUTURE_EPOCH # lost whistleblower reward assert ( - get_balance(post_state, validator_index) < - get_balance(state, validator_index) + get_balance(post_state, slashed_index) < + get_balance(state, slashed_index) + ) + proposer_index = get_beacon_proposer_index(state, state.slot) + # gained whistleblower reward + assert ( + get_balance(post_state, proposer_index) > + get_balance(state, proposer_index) ) return state, post_state @@ -96,3 +102,14 @@ def test_participants_already_slashed(state): pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) return pre_state, attester_slashing, post_state + + +def test_custody_bit_0_and_1(state): + attester_slashing = get_valid_attester_slashing(state) + + attester_slashing.attestation_1.custody_bit_1_indices = ( + attester_slashing.attestation_1.custody_bit_0_indices + ) + pre_state, post_state = run_attester_slashing_processing(state, attester_slashing, False) + + return pre_state, attester_slashing, post_state diff --git a/tests/phase0/test_sanity.py b/tests/phase0/test_sanity.py index 0010cb22f..90825242f 100644 --- a/tests/phase0/test_sanity.py +++ b/tests/phase0/test_sanity.py @@ -17,6 +17,7 @@ from build.phase0.spec import ( # functions get_active_validator_indices, get_balance, + get_beacon_proposer_index, get_block_root, get_current_epoch, get_domain, @@ -164,6 +165,13 @@ def test_attester_slashing(state): # lost whistleblower reward assert get_balance(test_state, validator_index) < get_balance(state, validator_index) + proposer_index = get_beacon_proposer_index(test_state, test_state.slot) + # gained whistleblower reward + assert ( + get_balance(test_state, proposer_index) > + get_balance(state, proposer_index) + ) + return state, [block], test_state From bee740e834a04509c24a741d8f2e5abaaeb28e27 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Wed, 3 Apr 2019 01:06:18 -0500 Subject: [PATCH 17/21] Removed merkle partial from paths for now --- specs/light_client/merkle_proofs.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/specs/light_client/merkle_proofs.md b/specs/light_client/merkle_proofs.md index 2e92488cb..285445ca8 100644 --- a/specs/light_client/merkle_proofs.md +++ b/specs/light_client/merkle_proofs.md @@ -170,20 +170,6 @@ We define: } ``` -#### `merkle_partial_from_paths` - -```python -def merkle_partial_from_paths(obj, paths): - indices = set() - for path in paths: - indices = indices.union(get_generalized_indices(obj, path)) - return MerklePartial( - root=hash_tree_root(obj), - indices=indices, - values= mk_multi_proof - ) -``` - #### Proofs for execution We define `MerklePartial(f, arg1, arg2..., focus=0)` as being a `MerklePartial` object wrapping a Merkle multiproof of the set of nodes in the hash tree of the SSZ object `arg[focus]` that is needed to authenticate the parts of the object needed to compute `f(arg1, arg2...)`. From 76893cafaadb2610fdc171f07fe2cba58bdca132 Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 3 Apr 2019 23:22:27 +0400 Subject: [PATCH 18/21] Missing typehint for `convert_to_indexed` And some minor cleanups --- specs/core/0_beacon-chain.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 70c5b67f6..3a2bb3903 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -1160,9 +1160,9 @@ def verify_bitfield(bitfield: bytes, committee_size: int) -> bool: ### `convert_to_indexed` ```python -def convert_to_indexed(state: BeaconState, attestation: Attestation): +def convert_to_indexed(state: BeaconState, attestation: Attestation) -> IndexedAttestation: """ - Convert an attestation to (almost) indexed-verifiable form + Convert an ``attestation`` to (almost) indexed-verifiable form. """ attesting_indices = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) custody_bit_1_indices = get_attestation_participants(state, attestation.data, attestation.custody_bitfield) @@ -1172,7 +1172,7 @@ def convert_to_indexed(state: BeaconState, attestation: Attestation): custody_bit_0_indices=custody_bit_0_indices, custody_bit_1_indices=custody_bit_1_indices, data=attestation.data, - aggregate_signature=attestation.aggregate_signature + aggregate_signature=attestation.aggregate_signature, ) ``` From 6ca3c64526a1683aa72866d6b6f7a210cbea399f Mon Sep 17 00:00:00 2001 From: Justin Date: Wed, 3 Apr 2019 23:24:46 +0400 Subject: [PATCH 19/21] Update 0_beacon-chain.md --- specs/core/0_beacon-chain.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 3a2bb3903..9bec7841f 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -1162,7 +1162,7 @@ def verify_bitfield(bitfield: bytes, committee_size: int) -> bool: ```python def convert_to_indexed(state: BeaconState, attestation: Attestation) -> IndexedAttestation: """ - Convert an ``attestation`` to (almost) indexed-verifiable form. + Convert ``attestation`` to (almost) indexed-verifiable form. """ attesting_indices = get_attestation_participants(state, attestation.data, attestation.aggregation_bitfield) custody_bit_1_indices = get_attestation_participants(state, attestation.data, attestation.custody_bitfield) From 5a77e61160965b6c3c1714aa0b9aa0aa2528e144 Mon Sep 17 00:00:00 2001 From: terence tsao Date: Thu, 4 Apr 2019 08:11:12 -0700 Subject: [PATCH 20/21] Update 0_beacon-chain.md --- specs/core/0_beacon-chain.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 9bec7841f..3f690c888 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -1041,9 +1041,12 @@ def verify_merkle_branch(leaf: Bytes32, proof: List[Bytes32], depth: int, index: ```python def get_crosslink_committee_for_attestation(state: BeaconState, attestation_data: AttestationData) -> List[ValidatorIndex]: - # Find the committee in the list with the desired shard + """ + Return the crosslink committee corresponding to ``attestation_data``. + """ crosslink_committees = get_crosslink_committees_at_slot(state, attestation_data.slot) - + + # Find the committee in the list with the desired shard assert attestation_data.shard in [shard for _, shard in crosslink_committees] crosslink_committee = [committee for committee, shard in crosslink_committees if shard == attestation_data.shard][0] From 199e7849dafc48145dfbc6779ef2ccfa04e4a718 Mon Sep 17 00:00:00 2001 From: vbuterin Date: Sat, 6 Apr 2019 04:07:26 -0500 Subject: [PATCH 21/21] Clarify lexicographic hash favoring (#881) --- specs/core/0_beacon-chain.md | 1 + 1 file changed, 1 insertion(+) diff --git a/specs/core/0_beacon-chain.md b/specs/core/0_beacon-chain.md index 3f690c888..60e7864c2 100644 --- a/specs/core/0_beacon-chain.md +++ b/specs/core/0_beacon-chain.md @@ -1664,6 +1664,7 @@ def lmd_ghost(store: Store, start_state: BeaconState, start_block: BeaconBlock) children = get_children(store, head) if len(children) == 0: return head + # Ties broken by favoring block with lexicographically higher root head = max(children, key=lambda x: (get_vote_count(x), hash_tree_root(x))) ```