Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bounce attack resistance #1465

Merged
merged 8 commits into from
Nov 7, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions configs/mainnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 65536
MIN_GENESIS_TIME: 1578009600


# Fork Choice
# ---------------------------------------------------------------
# 2**3 (= 8)
SAFE_SLOTS_TO_UPDATE_JUSTIFIED: 8


# Deposit contract
# ---------------------------------------------------------------
Expand Down
6 changes: 6 additions & 0 deletions configs/minimal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 64
# Jan 3, 2020
MIN_GENESIS_TIME: 1578009600

#
#
# Fork Choice
# ---------------------------------------------------------------
# 2**1 (= 1)
SAFE_SLOTS_TO_UPDATE_JUSTIFIED: 2


# Deposit contract
Expand Down
65 changes: 64 additions & 1 deletion specs/core/0_fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ The head block root associated with a `store` is defined as `get_head(store)`. A
4) **Manual forks**: Manual forks may arbitrarily change the fork choice rule but are expected to be enacted at epoch transitions, with the fork details reflected in `state.fork`.
5) **Implementation**: The implementation found in this specification is constructed for ease of understanding rather than for optimization in computation, space, or any other resource. A number of optimized alternatives can be found [here](https://github.com/protolambda/lmd-ghost).

### Configuration

| Name | Value | Unit | Duration |
| - | - | :-: | :-: |
| `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds |

### Helpers

#### `LatestMessage`
Expand All @@ -60,8 +66,10 @@ class LatestMessage(object):
@dataclass
class Store(object):
time: uint64
genesis_time: uint64
justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
best_justified_checkpoint: Checkpoint
blocks: Dict[Hash, BeaconBlock] = field(default_factory=dict)
block_states: Dict[Hash, BeaconState] = field(default_factory=dict)
checkpoint_states: Dict[Checkpoint, BeaconState] = field(default_factory=dict)
Expand All @@ -78,14 +86,30 @@ def get_genesis_store(genesis_state: BeaconState) -> Store:
finalized_checkpoint = Checkpoint(epoch=GENESIS_EPOCH, root=root)
return Store(
time=genesis_state.genesis_time,
genesis_time=genesis_state.genesis_time,
justified_checkpoint=justified_checkpoint,
finalized_checkpoint=finalized_checkpoint,
best_justified_checkpoint=justified_checkpoint,
blocks={root: genesis_block},
block_states={root: genesis_state.copy()},
checkpoint_states={justified_checkpoint: genesis_state.copy()},
)
```

#### `get_current_slot`

```python
def get_current_slot(store: Store) -> Slot:
return Slot((store.time - store.genesis_time) // SECONDS_PER_SLOT)
```

#### `compute_slots_since_epoch_start`

```python
def compute_slots_since_epoch_start(slot: Slot) -> int:
return slot - compute_start_slot_at_epoch(compute_epoch_at_slot(slot))
```

#### `get_ancestor`

```python
Expand Down Expand Up @@ -130,13 +154,50 @@ def get_head(store: Store) -> Hash:
head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root))
```

#### `should_update_justified_checkpoint`

```python
def should_update_justified_checkpoint(store: Store, new_justified_checkpoint: Checkpoint) -> bool:
djrtwo marked this conversation as resolved.
Show resolved Hide resolved
"""
To address the bouncing attack, only update conflicting justified
checkpoints in the fork choice if in the early slots of the epoch.
Otherwise, delay incorporation of new justified checkpoint until next epoch boundary.

See https://ethresear.ch/t/prevention-of-bouncing-attack-on-ffg/6114 for more detailed analysis and discussion.
"""
if compute_slots_since_epoch_start(get_current_slot(store)) < SAFE_SLOTS_TO_UPDATE_JUSTIFIED:
return True

new_justified_block = store.blocks[new_justified_checkpoint.root]
if new_justified_block.slot <= compute_start_slot_at_epoch(store.justified_checkpoint.epoch):
return False
if not (
get_ancestor(store, new_justified_checkpoint.root, store.blocks[store.justified_checkpoint.root].slot) ==
store.justified_checkpoint.root
):
return False

return True
```

### Handlers

#### `on_tick`

```python
def on_tick(store: Store, time: uint64) -> None:
previous_slot = get_current_slot(store)

# update store time
store.time = time

current_slot = get_current_slot(store)
# Not a new epoch, return
if not (current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0):
return
# Update store.justified_checkpoint if a better checkpoint is known
if store.best_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
store.justified_checkpoint = store.best_justified_checkpoint
```

#### `on_block`
Expand Down Expand Up @@ -164,7 +225,9 @@ def on_block(store: Store, block: BeaconBlock) -> None:

# Update justified checkpoint
if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
store.justified_checkpoint = state.current_justified_checkpoint
store.best_justified_checkpoint = state.current_justified_checkpoint
if should_update_justified_checkpoint(store, state.current_justified_checkpoint):
store.justified_checkpoint = state.current_justified_checkpoint

# Update finalized checkpoint
if state.finalized_checkpoint.epoch > store.finalized_checkpoint.epoch:
Expand Down
71 changes: 71 additions & 0 deletions test_libs/pyspec/eth2spec/test/fork_choice/test_on_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,74 @@ def test_on_block_before_finalized(spec, state):
block = build_empty_block_for_next_slot(spec, state)
state_transition_and_sign_block(spec, state, block)
run_on_block(spec, store, block, False)


@with_all_phases
@spec_state_test
def test_on_block_update_justified_checkpoint_within_safe_slots(spec, state):
# Initialization
store = spec.get_genesis_store(state)
time = 100
spec.on_tick(store, time)

next_epoch(spec, state)
spec.on_tick(store, store.time + state.slot * spec.SECONDS_PER_SLOT)
state, store, last_block = apply_next_epoch_with_attestations(spec, state, store)
next_epoch(spec, state)
spec.on_tick(store, store.time + state.slot * spec.SECONDS_PER_SLOT)
last_block_root = signing_root(last_block)

# Mock the justified checkpoint
just_state = store.block_states[last_block_root]
new_justified = spec.Checkpoint(
epoch=just_state.current_justified_checkpoint.epoch + 1,
root=b'\x77' * 32,
)
just_state.current_justified_checkpoint = new_justified

block = build_empty_block_for_next_slot(spec, just_state)
state_transition_and_sign_block(spec, deepcopy(just_state), block)
assert spec.get_current_slot(store) % spec.SLOTS_PER_EPOCH < spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED
run_on_block(spec, store, block)

assert store.justified_checkpoint == new_justified


@with_all_phases
@spec_state_test
def test_on_block_outside_safe_slots_and_old_block(spec, state):
# Initialization
store = spec.get_genesis_store(state)
time = 100
spec.on_tick(store, time)

next_epoch(spec, state)
spec.on_tick(store, store.time + state.slot * spec.SECONDS_PER_SLOT)
state, store, last_block = apply_next_epoch_with_attestations(spec, state, store)
next_epoch(spec, state)
spec.on_tick(store, store.time + state.slot * spec.SECONDS_PER_SLOT)
last_block_root = signing_root(last_block)

# Mock justified block in store
just_block = build_empty_block_for_next_slot(spec, state)
# Slot is same as justified checkpoint so does not trigger an override in the store
just_block.slot = spec.compute_start_slot_at_epoch(store.justified_checkpoint.epoch)
store.blocks[just_block.hash_tree_root()] = just_block

# Mock the justified checkpoint
just_state = store.block_states[last_block_root]
new_justified = spec.Checkpoint(
epoch=just_state.current_justified_checkpoint.epoch + 1,
root=just_block.hash_tree_root(),
)
just_state.current_justified_checkpoint = new_justified

block = build_empty_block_for_next_slot(spec, just_state)
state_transition_and_sign_block(spec, deepcopy(just_state), block)

spec.on_tick(store, store.time + spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED * spec.SECONDS_PER_SLOT)
assert spec.get_current_slot(store) % spec.SLOTS_PER_EPOCH >= spec.SAFE_SLOTS_TO_UPDATE_JUSTIFIED
run_on_block(spec, store, block)

assert store.justified_checkpoint != new_justified
assert store.best_justified_checkpoint == new_justified
105 changes: 105 additions & 0 deletions test_libs/pyspec/eth2spec/test/fork_choice/test_on_tick.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from eth2spec.test.context import with_all_phases, spec_state_test


def run_on_tick(spec, store, time, new_justified_checkpoint=False):
previous_justified_checkpoint = store.justified_checkpoint

spec.on_tick(store, time)

assert store.time == time

if new_justified_checkpoint:
assert store.justified_checkpoint == store.best_justified_checkpoint
assert store.justified_checkpoint.epoch > previous_justified_checkpoint.epoch
assert store.justified_checkpoint.root != previous_justified_checkpoint.root
else:
assert store.justified_checkpoint == previous_justified_checkpoint


@with_all_phases
@spec_state_test
def test_basic(spec, state):
store = spec.get_genesis_store(state)
run_on_tick(spec, store, store.time + 1)


@with_all_phases
@spec_state_test
def test_update_justified_single(spec, state):
store = spec.get_genesis_store(state)
seconds_per_epoch = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH

store.best_justified_checkpoint = spec.Checkpoint(
epoch=store.justified_checkpoint.epoch + 1,
root=b'\x55' * 32,
)

run_on_tick(spec, store, store.time + seconds_per_epoch, True)


@with_all_phases
@spec_state_test
def test_no_update_same_slot_at_epoch_boundary(spec, state):
store = spec.get_genesis_store(state)
seconds_per_epoch = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH

store.best_justified_checkpoint = spec.Checkpoint(
epoch=store.justified_checkpoint.epoch + 1,
root=b'\x55' * 32,
)

# set store time to already be at epoch boundary
store.time = seconds_per_epoch

run_on_tick(spec, store, store.time + 1)


@with_all_phases
@spec_state_test
def test_no_update_not_epoch_boundary(spec, state):
store = spec.get_genesis_store(state)

store.best_justified_checkpoint = spec.Checkpoint(
epoch=store.justified_checkpoint.epoch + 1,
root=b'\x55' * 32,
)

run_on_tick(spec, store, store.time + spec.SECONDS_PER_SLOT)


@with_all_phases
@spec_state_test
def test_no_update_new_justified_equal_epoch(spec, state):
store = spec.get_genesis_store(state)
seconds_per_epoch = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH

store.best_justified_checkpoint = spec.Checkpoint(
epoch=store.justified_checkpoint.epoch + 1,
root=b'\x55' * 32,
)

store.justified_checkpoint = spec.Checkpoint(
epoch=store.best_justified_checkpoint.epoch,
root=b'\44' * 32,
)

run_on_tick(spec, store, store.time + seconds_per_epoch)


@with_all_phases
@spec_state_test
def test_no_update_new_justified_later_epoch(spec, state):
store = spec.get_genesis_store(state)
seconds_per_epoch = spec.SECONDS_PER_SLOT * spec.SLOTS_PER_EPOCH

store.best_justified_checkpoint = spec.Checkpoint(
epoch=store.justified_checkpoint.epoch + 1,
root=b'\x55' * 32,
)

store.justified_checkpoint = spec.Checkpoint(
epoch=store.best_justified_checkpoint.epoch + 1,
root=b'\44' * 32,
)

run_on_tick(spec, store, store.time + seconds_per_epoch)