Skip to content

Commit

Permalink
Merge pull request #1773 from ethereum/hwwhww/shard_fork_choice
Browse files Browse the repository at this point in the history
Shard fork choice rule
  • Loading branch information
hwwhww authored Jun 8, 2020
2 parents dce82e7 + 2d895e9 commit f279e30
Show file tree
Hide file tree
Showing 13 changed files with 479 additions and 188 deletions.
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ def finalize_options(self):
specs/phase1/shard-transition.md
specs/phase1/fork-choice.md
specs/phase1/phase1-fork.md
specs/phase1/shard-fork-choice.md
"""
else:
raise Exception('no markdown files specified, and spec fork "%s" is unknown', self.spec_fork)
Expand Down
32 changes: 31 additions & 1 deletion specs/phase1/fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@

- [Introduction](#introduction)
- [Fork choice](#fork-choice)
- [Helpers](#helpers)
- [Extended `LatestMessage`](#extended-latestmessage)
- [Updated `update_latest_messages`](#updated-update_latest_messages)
- [Handlers](#handlers)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->
Expand All @@ -25,6 +28,33 @@ Due to the changes in the structure of `IndexedAttestation` in Phase 1, `on_atte

The rest of the fork choice remains stable.

### Helpers

#### Extended `LatestMessage`

```python
@dataclass(eq=True, frozen=True)
class LatestMessage(object):
epoch: Epoch
root: Root
shard: Shard
shard_root: Root
```

#### Updated `update_latest_messages`

```python
def update_latest_messages(store: Store, attesting_indices: Sequence[ValidatorIndex], attestation: Attestation) -> None:
target = attestation.data.target
beacon_block_root = attestation.data.beacon_block_root
shard = get_shard(store.block_states[beacon_block_root], attestation)
for i in attesting_indices:
if i not in store.latest_messages or target.epoch > store.latest_messages[i].epoch:
store.latest_messages[i] = LatestMessage(
epoch=target.epoch, root=beacon_block_root, shard=shard, shard_root=attestation.data.shard_head_root
)
```

### Handlers

```python
Expand All @@ -49,4 +79,4 @@ def on_attestation(store: Store, attestation: Attestation) -> None:
if attestation.aggregation_bits[i]
]
update_latest_messages(store, attesting_indices, attestation)
```
```
182 changes: 182 additions & 0 deletions specs/phase1/shard-fork-choice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
# Ethereum 2.0 Phase 1 -- Beacon Chain + Shard Chain Fork Choice

**Notice**: This document is a work-in-progress for researchers and implementers.

## Table of contents

<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->


- [Introduction](#introduction)
- [Fork choice](#fork-choice)
- [Helpers](#helpers)
- [`ShardStore`](#shardstore)
- [`get_forkchoice_shard_store`](#get_forkchoice_shard_store)
- [`get_shard_latest_attesting_balance`](#get_shard_latest_attesting_balance)
- [`get_shard_head`](#get_shard_head)
- [`get_shard_ancestor`](#get_shard_ancestor)
- [`get_pending_shard_blocks`](#get_pending_shard_blocks)
- [Handlers](#handlers)
- [`on_shard_block`](#on_shard_block)

<!-- END doctoc generated TOC please keep comment here to allow auto update -->

## Introduction

This document is the shard chain fork choice spec for part of Ethereum 2.0 Phase 1. It assumes the [beacon chain fork choice spec](./fork-choice.md).

## Fork choice

### Helpers

#### `ShardStore`

```python
@dataclass
class ShardStore:
shard: Shard
blocks: Dict[Root, ShardBlock] = field(default_factory=dict)
block_states: Dict[Root, ShardState] = field(default_factory=dict)
```

#### `get_forkchoice_shard_store`

```python
def get_forkchoice_shard_store(anchor_state: BeaconState, shard: Shard) -> ShardStore:
return ShardStore(
shard=shard,
blocks={anchor_state.shard_states[shard].latest_block_root: ShardBlock(slot=anchor_state.slot, shard=shard)},
block_states={anchor_state.shard_states[shard].latest_block_root: anchor_state.copy().shard_states[shard]},
)
```

#### `get_shard_latest_attesting_balance`

```python
def get_shard_latest_attesting_balance(store: Store, shard_store: ShardStore, root: Root) -> Gwei:
state = store.checkpoint_states[store.justified_checkpoint]
active_indices = get_active_validator_indices(state, get_current_epoch(state))
return Gwei(sum(
state.validators[i].effective_balance for i in active_indices
if (
i in store.latest_messages
# TODO: check the latest message logic: currently, validator's previous vote of another shard
# would be ignored once their newer vote is accepted. Check if it makes sense.
and store.latest_messages[i].shard == shard_store.shard
and get_shard_ancestor(
store, shard_store, store.latest_messages[i].shard_root, shard_store.blocks[root].slot
) == root
)
))
```

#### `get_shard_head`

```python
def get_shard_head(store: Store, shard_store: ShardStore) -> Root:
# Execute the LMD-GHOST fork choice
beacon_head_root = get_head(store)
shard_head_state = store.block_states[beacon_head_root].shard_states[shard_store.shard]
shard_head_root = shard_head_state.latest_block_root
shard_blocks = {
root: shard_block for root, shard_block in shard_store.blocks.items()
if shard_block.slot > shard_head_state.slot
}
while True:
# Find the valid child block roots
children = [
root for root, shard_block in shard_blocks.items()
if shard_block.shard_parent_root == shard_head_root
]
if len(children) == 0:
return shard_head_root
# Sort by latest attesting balance with ties broken lexicographically
shard_head_root = max(
children, key=lambda root: (get_shard_latest_attesting_balance(store, shard_store, root), root)
)
```

#### `get_shard_ancestor`

```python
def get_shard_ancestor(store: Store, shard_store: ShardStore, root: Root, slot: Slot) -> Root:
block = shard_store.blocks[root]
if block.slot > slot:
return get_shard_ancestor(store, shard_store, block.shard_parent_root, slot)
elif block.slot == slot:
return root
else:
# root is older than queried slot, thus a skip slot. Return most recent root prior to slot
return root
```

#### `get_pending_shard_blocks`

```python
def get_pending_shard_blocks(store: Store, shard_store: ShardStore) -> Sequence[ShardBlock]:
"""
Return the canonical shard block branch that has not yet been crosslinked.
"""
shard = shard_store.shard

beacon_head_root = get_head(store)
beacon_head_state = store.block_states[beacon_head_root]
latest_shard_block_root = beacon_head_state.shard_states[shard].latest_block_root

shard_head_root = get_shard_head(store, shard_store)
root = shard_head_root
shard_blocks = []
while root != latest_shard_block_root:
shard_block = shard_store.blocks[root]
shard_blocks.append(shard_block)
root = shard_block.shard_parent_root

shard_blocks.reverse()
return shard_blocks
```

### Handlers

#### `on_shard_block`

```python
def on_shard_block(store: Store, shard_store: ShardStore, signed_shard_block: SignedShardBlock) -> None:
shard_block = signed_shard_block.message
shard = shard_store.shard

# Check shard
# TODO: check it in networking spec
assert shard_block.shard == shard

# Check shard parent exists
assert shard_block.shard_parent_root in shard_store.block_states
shard_parent_state = shard_store.block_states[shard_block.shard_parent_root]

# Check beacon parent exists
assert shard_block.beacon_parent_root in store.block_states
beacon_parent_state = store.block_states[shard_block.beacon_parent_root]

# Check that block is later than the finalized shard state slot (optimization to reduce calls to get_ancestor)
finalized_beacon_state = store.block_states[store.finalized_checkpoint.root]
finalized_shard_state = finalized_beacon_state.shard_states[shard]
assert shard_block.slot > finalized_shard_state.slot

# Check block is a descendant of the finalized block at the checkpoint finalized slot
finalized_slot = compute_start_slot_at_epoch(store.finalized_checkpoint.epoch)
assert (
get_ancestor(store, shard_block.beacon_parent_root, finalized_slot) == store.finalized_checkpoint.root
)

# Check the block is valid and compute the post-state
assert verify_shard_block_message(beacon_parent_state, shard_parent_state, shard_block)
assert verify_shard_block_signature(beacon_parent_state, signed_shard_block)

post_state = get_post_shard_state(beacon_parent_state, shard_parent_state, shard_block)

# Add new block to the store
shard_store.blocks[hash_tree_root(shard_block)] = shard_block

# Add new state for this block to the store
shard_store.block_states[hash_tree_root(shard_block)] = post_state
```
89 changes: 33 additions & 56 deletions specs/phase1/shard-transition.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This document describes the shard transition function and fraud proofs as part o
### Misc

```python
def compute_shard_transition_digest(beacon_state: BeaconState,
def compute_shard_transition_digest(beacon_parent_state: BeaconState,
shard_state: ShardState,
beacon_parent_root: Root,
shard_body_root: Root) -> Bytes32:
Expand All @@ -43,15 +43,27 @@ def compute_shard_transition_digest(beacon_state: BeaconState,
### Shard block verification functions

```python
def verify_shard_block_message(beacon_state: BeaconState,
shard_state: ShardState,
block: ShardBlock,
slot: Slot,
shard: Shard) -> bool:
assert block.shard_parent_root == shard_state.latest_block_root
assert block.slot == slot
def verify_shard_block_message(beacon_parent_state: BeaconState,
shard_parent_state: ShardState,
block: ShardBlock) -> bool:
# Check `shard_parent_root` field
assert block.shard_parent_root == shard_parent_state.latest_block_root
# Check `beacon_parent_root` field
beacon_parent_block_header = beacon_parent_state.latest_block_header.copy()
if beacon_parent_block_header.state_root == Root():
beacon_parent_block_header.state_root = hash_tree_root(beacon_parent_state)
beacon_parent_root = hash_tree_root(beacon_parent_block_header)
assert block.beacon_parent_root == beacon_parent_root
# Check `slot` field
shard = block.shard
next_slot = Slot(block.slot + 1)
offset_slots = compute_offset_slots(get_latest_slot_for_shard(beacon_parent_state, shard), next_slot)
assert block.slot in offset_slots
# Check `shard` field
assert block.shard == shard
assert block.proposer_index == get_shard_proposer_index(beacon_state, slot, shard)
# Check `proposer_index` field
assert block.proposer_index == get_shard_proposer_index(beacon_parent_state, block.slot, shard)
# Check `body` field
assert 0 < len(block.body) <= MAX_SHARD_BLOCK_SIZE
return True
```
Expand Down Expand Up @@ -172,38 +184,9 @@ def compute_shard_body_roots(proposals: Sequence[SignedShardBlock]) -> Sequence[
return [hash_tree_root(proposal.message.body) for proposal in proposals]
```

```python
def get_proposal_choices_at_slot(beacon_state: BeaconState,
shard_state: ShardState,
slot: Slot,
shard: Shard,
shard_blocks: Sequence[SignedShardBlock],
validate_signature: bool=True) -> Sequence[SignedShardBlock]:
"""
Return the valid shard blocks at the given ``slot``.
Note that this function doesn't change the state.
"""
choices = []
shard_blocks_at_slot = [block for block in shard_blocks if block.message.slot == slot]
for block in shard_blocks_at_slot:
try:
# Verify block message and signature
# TODO these validations should have been checked upon receiving shard blocks.
assert verify_shard_block_message(beacon_state, shard_state, block.message, slot, shard)
if validate_signature:
assert verify_shard_block_signature(beacon_state, block)

shard_state = get_post_shard_state(beacon_state, shard_state, block.message)
except Exception:
pass # TODO: throw error in the test helper
else:
choices.append(block)
return choices
```

```python
def get_proposal_at_slot(beacon_state: BeaconState,
shard_state: ShardState,
shard_parent_state: ShardState,
slot: Shard,
shard: Shard,
shard_blocks: Sequence[SignedShardBlock],
Expand All @@ -212,24 +195,17 @@ def get_proposal_at_slot(beacon_state: BeaconState,
Return ``proposal``, ``shard_state`` of the given ``slot``.
Note that this function doesn't change the state.
"""
choices = get_proposal_choices_at_slot(
beacon_state=beacon_state,
shard_state=shard_state,
slot=slot,
shard=shard,
shard_blocks=shard_blocks,
validate_signature=validate_signature,
)
if len(choices) == 0:
block = ShardBlock(slot=slot)
shard_blocks = [block for block in shard_blocks if block.message.slot == slot]
if len(shard_blocks) == 0:
block = ShardBlock(slot=slot, shard=shard)
proposal = SignedShardBlock(message=block)
elif len(choices) == 1:
proposal = choices[0]
elif len(shard_blocks) == 1:
proposal = shard_blocks[0]
else:
proposal = get_winning_proposal(beacon_state, choices)
proposal = get_winning_proposal(beacon_state, shard_blocks)

# Apply state transition
shard_state = get_post_shard_state(beacon_state, shard_state, proposal.message)
shard_state = get_post_shard_state(beacon_state, shard_parent_state, proposal.message)

return proposal, shard_state
```
Expand All @@ -244,10 +220,11 @@ def get_shard_state_transition_result(
proposals = []
shard_states = []
shard_state = beacon_state.shard_states[shard]
for slot in get_offset_slots(beacon_state, shard):
offset_slots = compute_offset_slots(get_latest_slot_for_shard(beacon_state, shard), Slot(beacon_state.slot + 1))
for slot in offset_slots:
proposal, shard_state = get_proposal_at_slot(
beacon_state=beacon_state,
shard_state=shard_state,
shard_parent_state=shard_state,
slot=slot,
shard=shard,
shard_blocks=shard_blocks,
Expand All @@ -269,7 +246,7 @@ Suppose you are a committee member on shard `shard` at slot `current_slot` and y
def get_shard_transition(beacon_state: BeaconState,
shard: Shard,
shard_blocks: Sequence[SignedShardBlock]) -> ShardTransition:
offset_slots = get_offset_slots(beacon_state, shard)
offset_slots = compute_offset_slots(get_latest_slot_for_shard(beacon_state, shard), Slot(beacon_state.slot + 1))
proposals, shard_states, shard_data_roots = get_shard_state_transition_result(beacon_state, shard, shard_blocks)

shard_block_lengths = []
Expand Down
Loading

0 comments on commit f279e30

Please sign in to comment.