Merge pull request #1465 from ethereum/bounce-attack
Bounce attack resistance
This commit is contained in:
commit
f331b55b9e
|
@ -23,6 +23,11 @@ MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 65536
|
||||||
MIN_GENESIS_TIME: 1578009600
|
MIN_GENESIS_TIME: 1578009600
|
||||||
|
|
||||||
|
|
||||||
|
# Fork Choice
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# 2**3 (= 8)
|
||||||
|
SAFE_SLOTS_TO_UPDATE_JUSTIFIED: 8
|
||||||
|
|
||||||
|
|
||||||
# Deposit contract
|
# Deposit contract
|
||||||
# ---------------------------------------------------------------
|
# ---------------------------------------------------------------
|
||||||
|
|
|
@ -21,6 +21,12 @@ MIN_GENESIS_ACTIVE_VALIDATOR_COUNT: 64
|
||||||
# Jan 3, 2020
|
# Jan 3, 2020
|
||||||
MIN_GENESIS_TIME: 1578009600
|
MIN_GENESIS_TIME: 1578009600
|
||||||
|
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# Fork Choice
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# 2**1 (= 1)
|
||||||
|
SAFE_SLOTS_TO_UPDATE_JUSTIFIED: 2
|
||||||
|
|
||||||
|
|
||||||
# Deposit contract
|
# Deposit contract
|
||||||
|
|
|
@ -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`.
|
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).
|
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
|
### Helpers
|
||||||
|
|
||||||
#### `LatestMessage`
|
#### `LatestMessage`
|
||||||
|
@ -60,8 +66,10 @@ class LatestMessage(object):
|
||||||
@dataclass
|
@dataclass
|
||||||
class Store(object):
|
class Store(object):
|
||||||
time: uint64
|
time: uint64
|
||||||
|
genesis_time: uint64
|
||||||
justified_checkpoint: Checkpoint
|
justified_checkpoint: Checkpoint
|
||||||
finalized_checkpoint: Checkpoint
|
finalized_checkpoint: Checkpoint
|
||||||
|
best_justified_checkpoint: Checkpoint
|
||||||
blocks: Dict[Hash, BeaconBlock] = field(default_factory=dict)
|
blocks: Dict[Hash, BeaconBlock] = field(default_factory=dict)
|
||||||
block_states: Dict[Hash, BeaconState] = field(default_factory=dict)
|
block_states: Dict[Hash, BeaconState] = field(default_factory=dict)
|
||||||
checkpoint_states: Dict[Checkpoint, 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)
|
finalized_checkpoint = Checkpoint(epoch=GENESIS_EPOCH, root=root)
|
||||||
return Store(
|
return Store(
|
||||||
time=genesis_state.genesis_time,
|
time=genesis_state.genesis_time,
|
||||||
|
genesis_time=genesis_state.genesis_time,
|
||||||
justified_checkpoint=justified_checkpoint,
|
justified_checkpoint=justified_checkpoint,
|
||||||
finalized_checkpoint=finalized_checkpoint,
|
finalized_checkpoint=finalized_checkpoint,
|
||||||
|
best_justified_checkpoint=justified_checkpoint,
|
||||||
blocks={root: genesis_block},
|
blocks={root: genesis_block},
|
||||||
block_states={root: genesis_state.copy()},
|
block_states={root: genesis_state.copy()},
|
||||||
checkpoint_states={justified_checkpoint: 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`
|
#### `get_ancestor`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -130,13 +154,50 @@ def get_head(store: Store) -> Hash:
|
||||||
head = max(children, key=lambda root: (get_latest_attesting_balance(store, root), root))
|
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
|
### Handlers
|
||||||
|
|
||||||
#### `on_tick`
|
#### `on_tick`
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def on_tick(store: Store, time: uint64) -> None:
|
def on_tick(store: Store, time: uint64) -> None:
|
||||||
|
previous_slot = get_current_slot(store)
|
||||||
|
|
||||||
|
# update store time
|
||||||
store.time = 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`
|
#### `on_block`
|
||||||
|
@ -164,6 +225,8 @@ def on_block(store: Store, block: BeaconBlock) -> None:
|
||||||
|
|
||||||
# Update justified checkpoint
|
# Update justified checkpoint
|
||||||
if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
|
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
|
store.justified_checkpoint = state.current_justified_checkpoint
|
||||||
|
|
||||||
# Update finalized checkpoint
|
# Update finalized checkpoint
|
||||||
|
|
|
@ -132,3 +132,74 @@ def test_on_block_before_finalized(spec, state):
|
||||||
block = build_empty_block_for_next_slot(spec, state)
|
block = build_empty_block_for_next_slot(spec, state)
|
||||||
state_transition_and_sign_block(spec, state, block)
|
state_transition_and_sign_block(spec, state, block)
|
||||||
run_on_block(spec, store, block, False)
|
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
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue