Merge pull request #1465 from ethereum/bounce-attack

Bounce attack resistance
This commit is contained in:
Danny Ryan 2019-11-08 02:56:46 +08:00 committed by GitHub
commit f331b55b9e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 251 additions and 1 deletions

View File

@ -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
# ---------------------------------------------------------------

View File

@ -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

View File

@ -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`
@ -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)
@ -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
@ -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:
"""
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`
@ -164,6 +225,8 @@ def on_block(store: Store, block: BeaconBlock) -> None:
# Update justified checkpoint
if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
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

View File

@ -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

View File

@ -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)