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

Proposer LMD Score Boosting #2353

Closed
wants to merge 12 commits into from
29 changes: 28 additions & 1 deletion specs/phase0/fork-choice.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Any of the above handlers that trigger an unhandled exception (e.g. a failed ass
| Name | Value | Unit | Duration |
| - | - | :-: | :-: |
| `SAFE_SLOTS_TO_UPDATE_JUSTIFIED` | `2**3` (= 8) | slots | 96 seconds |
| `ATTESTATION_OFFSET_QUOTIENT` | `3` | - | - |

### Helpers

Expand Down Expand Up @@ -112,6 +113,7 @@ class Store(object):
justified_checkpoint: Checkpoint
finalized_checkpoint: Checkpoint
best_justified_checkpoint: Checkpoint
proposer_score_boost: LatestMessage
blocks: Dict[Root, BeaconBlock] = field(default_factory=dict)
block_tree: Dict[Root, BlockTreeNode] = field(default_factory=dict)
block_states: Dict[Root, BeaconState] = field(default_factory=dict)
Expand All @@ -135,12 +137,14 @@ def get_forkchoice_store(anchor_state: BeaconState, anchor_block: BeaconBlock) -
anchor_node_id = get_node_id(anchor_root, anchor_epoch)
justified_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root)
finalized_checkpoint = Checkpoint(epoch=anchor_epoch, root=anchor_root)
proposer_score_boost = LatestMessage(root=Root(), epoch=Epoch(0))
return Store(
time=uint64(anchor_state.genesis_time + SECONDS_PER_SLOT * anchor_state.slot),
genesis_time=anchor_state.genesis_time,
justified_checkpoint=justified_checkpoint,
finalized_checkpoint=finalized_checkpoint,
best_justified_checkpoint=justified_checkpoint,
proposer_score_boost=proposer_score_boost,
blocks={anchor_root: copy(anchor_block)},
block_tree={anchor_node_id: anchor_node},
block_states={anchor_root: copy(anchor_state)},
Expand Down Expand Up @@ -207,14 +211,24 @@ def get_latest_attesting_balance(store: Store, node_id: Root) -> Gwei:
node = store.block_tree[node_id]
block = store.blocks[node.block_root]
block_slot = block.slot
return Gwei(sum(
attestation_score = Gwei(sum(
state.validators[i].effective_balance for i in active_indices
if (i in store.latest_messages
and get_ancestor_node_id(store,
get_node_id(store.latest_messages[i].root,
store.latest_messages[i].epoch),
block_slot) == node_id)
))
proposer_score = Gwei(0)
if store.proposer_score_boost.root != Root():
committee_weight = Gwei(sum(state.validators[i].effective_balance for i in active_indices))
if get_ancestor_node_id(store,
get_node_id(store.proposer_score_boost.root,
store.proposer_score_boost.epoch),
block_slot) == node_id:
proposer_score = Gwei(committee_weight // 4)
print(f"Applying proposer boost for {hash_tree_root(block)}")
return attestation_score + proposer_score
```

#### `filter_block_tree`
Expand Down Expand Up @@ -441,6 +455,11 @@ def on_tick(store: Store, time: uint64) -> None:
store.time = time

current_slot = get_current_slot(store)
# Reset store.proposer_score_boost if this is a new slot
if store.proposer_score_boost.root != Root():
if current_slot != store.blocks[store.proposer_score_boost.root].slot:
print(f"Removing proposer boost for {store.proposer_score_boost.root}")
store.proposer_score_boost = LatestMessage(root=Root(), epoch=Epoch(0))
# Not a new epoch, return
if not (current_slot > previous_slot and compute_slots_since_epoch_start(current_slot) == 0):
return
Expand Down Expand Up @@ -477,6 +496,14 @@ def on_block(store: Store, signed_block: SignedBeaconBlock) -> None:
# Add new state for this block to the store
store.block_states[hash_tree_root(block)] = state

# Add LMD score boosting if the block is timely
if (get_current_slot(store) == block.slot and
store.time % SECONDS_PER_SLOT <= SECONDS_PER_SLOT // ATTESTATION_OFFSET_QUOTIENT):
store.proposer_score_boost = LatestMessage(
root=hash_tree_root(block),
epoch=compute_epoch_at_slot(block.slot)
)

# Update justified checkpoint
if state.current_justified_checkpoint.epoch > store.justified_checkpoint.epoch:
if state.current_justified_checkpoint.epoch > store.best_justified_checkpoint.epoch:
Expand Down
2 changes: 1 addition & 1 deletion specs/phase0/validator.md
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def get_block_signature(state: BeaconState, block: BeaconBlock, privkey: int) ->

A validator is expected to create, sign, and broadcast an attestation during each epoch. The `committee`, assigned `index`, and assigned `slot` for which the validator performs this role during an epoch are defined by `get_committee_assignment(state, epoch, validator_index)`.

A validator should create and broadcast the `attestation` to the associated attestation subnet when either (a) the validator has received a valid block from the expected block proposer for the assigned `slot` or (b) one-third of the `slot` has transpired (`SECONDS_PER_SLOT / 3` seconds after the start of `slot`) -- whichever comes _first_.
A validator should create and broadcast the `attestation` to the associated attestation subnet when either (a) the validator has received a valid block from the expected block proposer for the assigned `slot` or (b) one-third of the `slot` has transpired (`SECONDS_PER_SLOT / ATTESTATION_OFFSET_QUOTIENT` seconds after the start of `slot`) -- whichever comes _first_.

*Note*: Although attestations during `GENESIS_EPOCH` do not count toward FFG finality, these initial attestations do give weight to the fork choice, are rewarded, and should be made.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_finality_delay_fix(spec, state):

# Build attestation from honest node, for an empty slot
next_slots(spec, state, attack_length + 1)
spec.on_tick(store, state.slot * spec.SECONDS_PER_SLOT)
spec.on_tick(store, store.genesis_time + state.slot * spec.SECONDS_PER_SLOT)
attestation = get_valid_late_attestation(spec, state, slot=attacker_blocks[-1].slot,
index=None, signed=True)
spec.on_attestation(store, attestation)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import random
from eth_utils import encode_hex

from eth2spec.test.context import (
Expand All @@ -19,6 +20,7 @@
run_on_block,
)
from eth2spec.test.helpers.state import (
next_slots,
next_epoch,
state_transition_and_sign_block,
)
Expand Down Expand Up @@ -103,18 +105,20 @@ def test_split_tie_breaker_no_attestations(spec, state):
}
})

spec.on_tick(store, store.genesis_time + 10 * spec.SECONDS_PER_SLOT)

# block at slot 1
block_1_state = genesis_state.copy()
block_1 = build_empty_block_for_next_slot(spec, block_1_state)
signed_block_1 = state_transition_and_sign_block(spec, block_1_state, block_1)
yield from tick_and_run_on_block(spec, store, signed_block_1, test_steps)
yield from run_on_block(spec, store, signed_block_1, test_steps)

# additional block at slot 1
block_2_state = genesis_state.copy()
block_2 = build_empty_block_for_next_slot(spec, block_2_state)
block_2.body.graffiti = b'\x42' * 32
signed_block_2 = state_transition_and_sign_block(spec, block_2_state, block_2)
yield from tick_and_run_on_block(spec, store, signed_block_2, test_steps)
yield from run_on_block(spec, store, signed_block_2, test_steps)

highest_root = max(spec.hash_tree_root(block_1), spec.hash_tree_root(block_2))
assert spec.get_head(store) == highest_root
Expand Down Expand Up @@ -261,3 +265,66 @@ def test_filtered_block_tree(spec, state):
})

yield 'steps', test_steps


@with_all_phases
@spec_state_test
def test_lmd_proposer_scoring_fix(spec, state):
test_steps = []
genesis_state = state.copy()

# Initialization
store, anchor_block = get_genesis_forkchoice_store_and_block(spec, state)
yield 'anchor_state', state
yield 'anchor_block', anchor_block
anchor_root = get_anchor_root(spec, state)
assert spec.get_head(store) == anchor_root
test_steps.append({
'checks': {
'head': get_formatted_head_output(spec, store),
}
})

# Build block that serves as head ONLY on timely arrival, and ONLY in that slot
state_1 = genesis_state.copy()
next_slots(spec, state_1, 3)
block_1 = build_empty_block_for_next_slot(spec, state_1)
signed_block_1 = state_transition_and_sign_block(spec, state_1, block_1)

# Build block that serves as current head, and remains the head after block_1.slot
state_2 = genesis_state.copy()
next_slots(spec, state_2, 2)
block_2 = build_empty_block_for_next_slot(spec, state_2)
block_2.body.graffiti = spec.Bytes32(hex(random.getrandbits(8 * 32))[2:].zfill(64))
signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2)
while spec.hash_tree_root(block_1) > spec.hash_tree_root(block_2):
block_2.body.graffiti = spec.Bytes32(hex(random.getrandbits(8 * 32))[2:].zfill(64))
signed_block_2 = state_transition_and_sign_block(spec, state_2.copy(), block_2)
assert spec.hash_tree_root(block_1) < spec.hash_tree_root(block_2)

spec.on_tick(store, store.genesis_time + block_1.slot * spec.SECONDS_PER_SLOT)

# Process block_2
yield from run_on_block(spec, store, signed_block_2, test_steps)
assert store.proposer_score_boost.root == spec.Root()
assert spec.get_head(store) == spec.hash_tree_root(block_2)

# Process block_1 on timely arrival
# The head should temporarily change to block_1
yield from run_on_block(spec, store, signed_block_1, test_steps)
assert store.proposer_score_boost == spec.LatestMessage(root=spec.hash_tree_root(block_1),
epoch=spec.compute_epoch_at_slot(block_1.slot))
assert spec.get_head(store) == spec.hash_tree_root(block_1)

# After block_1.slot, the head should revert to block_2
spec.on_tick(store, store.genesis_time + (block_1.slot + 1) * spec.SECONDS_PER_SLOT)
assert store.proposer_score_boost.root == spec.Root()
assert spec.get_head(store) == spec.hash_tree_root(block_2)

test_steps.append({
'checks': {
'head': get_formatted_head_output(spec, store),
}
})

yield 'steps', test_steps
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def test_on_attestation_inconsistent_target_and_head(spec, state):
target_state_1 = state.copy()
next_epoch(spec, target_state_1)

# Create chain 2 with different block in chain from chain 1 from chain 1 from chain 1 from chain 1
# Create chain 2 with different block in chain from chain 1
target_state_2 = state.copy()
diff_block = build_empty_block_for_next_slot(spec, target_state_2)
signed_diff_block = state_transition_and_sign_block(spec, target_state_2, diff_block)
Expand Down