import time
from typing import Set, Optional, Sequence, Tuple, Dict, Text
from dataclasses import dataclass, field
from .specs import (
Slot, Root, Epoch, CommitteeIndex, ValidatorIndex, Store,
BeaconState, BeaconBlock, BeaconBlockBody, SignedBeaconBlock,
Attestation, AttestationData, Checkpoint, BLSSignature,
MAX_VALIDATORS_PER_COMMITTEE, VALIDATOR_REGISTRY_LIMIT,
SLOTS_PER_EPOCH, DOMAIN_RANDAO, DOMAIN_BEACON_PROPOSER,
DOMAIN_BEACON_ATTESTER,
get_forkchoice_store, get_current_slot, compute_epoch_at_slot,
get_head, process_slots, on_tick, get_current_epoch,
get_committee_assignment, compute_start_slot_at_epoch,
get_block_root, process_block, process_attestation,
get_block_root_at_slot, get_beacon_proposer_index,
get_domain, compute_signing_root, state_transition,
on_block, on_attestation,
)
import milagro_bls_binding as bls
from eth2spec.utils.ssz.ssz_impl import hash_tree_root
from eth2spec.utils.ssz.ssz_typing import Container, List, uint64, Bitlist, Bytes32
from eth2spec.test.helpers.keys import pubkeys, pubkey_to_privkey
frequency = 1
assert frequency in [1, 10, 100, 1000]
class ValidatorMove(object):
"""
Internal class recording validator moves: messages sent over the wire by the validator.
Useful e.g. to avoid double-sending an attestation.
"""
time: uint64
"""Simulation time (in ms) when move was made."""
slot: Slot
"""Slot where move was made."""
move: str
"""Type of move. Currently either 'attest' or 'propose'"""
def __init__(self, time, slot, move):
"""Initialise ValidatorMove"""
self.time = time
self.slot = slot
self.move = move
class ValidatorData:
"""
Holds current validator data, to be consumed by BRValidator subclasses.
"""
slot: Slot
"""Current slot"""
time_ms: uint64
"""Current simulation time in milliseconds"""
head_root: Root
"""Current head root, returned by `get_head` on validator's `Store`"""
current_epoch: Epoch
"""Current epoch"""
current_attest_slot: Slot
"""Last computed slot to attest in the current epoch"""
current_committee_index: CommitteeIndex
"""Last computed committee index to attest in the current epoch"""
current_committee: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE]
"""Last computed committee to attest in the current epoch"""
next_attest_slot: Slot
"""Last computed slot to attest in the next epoch"""
next_committee_index: CommitteeIndex
"""Last computed committee index to attest in the next epoch"""
next_committee: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE]
"""Last computed committee to attest in the next epoch"""
last_slot_attested: Optional[Slot]
"""
Last slot where validator attested. Possibly we have
`self.slot == self.last_slot_attested`
"""
current_proposer_duties: Sequence[bool]
"""
For the next SLOTS_PER_EPOCH, up to the beginning of a new epoch,
is the validator a block proposer?
"""
last_slot_proposed: Optional[Slot]
"""
Last slot where validator proposed a block. Possibly we have
`self.slot == self.last_slot_proposed`
"""
recorded_attestations: List[Root, VALIDATOR_REGISTRY_LIMIT]
"""
Hash roots of `Store` recorded attestations. Used for internal cache.
"""
received_block: bool
"""
Has the validator received a block for `self.slot`?
"""
class HashableSpecStore(Container):
""" We cache a map from current state of the `Store` to `head`, since `get_head`
is computationally intensive. But `Store` is not hashable right off the bat.
`get_head` only depends on stored blocks and latest messages, so we use that here.
"""
recorded_attestations: List[Root, VALIDATOR_REGISTRY_LIMIT]
"""Recorded attestations in the `Store`"""
recorded_blocks: List[Root, VALIDATOR_REGISTRY_LIMIT]
"""Recorded blocks in the `Store`"""
[docs]class BRValidator:
"""
Abstract superclass from which validator behaviours inherit.
Defines and maintains environment accessor functions (is the validator an attester? proposer?)
Performs caching to avoid recomputing expensive operations.
In general, you are not expected to use any of the methods or attributes defined here, _except_
for `validator.data`, which exposes current simulation environment properties, up-to-date with
respect to the validator (e.g., proposer and attester duties).
Subclasses of `BRValidator` must define at least two methods:
- `attest(self, known_items) -> Optional[Attestation]`
- `propose(self, known_items) -> Optional[Attestation]`
"""
validator_index: ValidatorIndex
"""Validator index in the simulation."""
pubkey: int
"""Validator public key."""
privkey: int
"""Validator private key."""
store: Store
"""`Store` objects are defined in the specs."""
history: List[ValidatorMove, VALIDATOR_REGISTRY_LIMIT]
"""History of `ValidatorMove` by the validator."""
data: ValidatorData
"""Current validator data. Maintained by the `BRValidator` methods."""
head_store: Dict[Root, Root] = {}
"""
Static cache for expensive operations.
`head_store` stores a map from store hash to head root.
"""
state_store: Dict[Tuple[Root, Slot], BeaconState] = {}
"""
Static cache for expensive operations.
`state_store` stores a map from `(current_state_hash, to_slot)` calling
`process_slots(current_state, to_slot)`.
"""
def __init__(self, validator_index: ValidatorIndex):
"""
Validator constructor
We preload a bunch of things, to be updated later on as needed
The validator is initialised from some base state, and given a `validator_index`
"""
self.validator_index = validator_index
self.pubkey = pubkeys[validator_index]
self.privkey = pubkey_to_privkey[self.pubkey].to_bytes(32, 'big')
self.history = []
self.data = ValidatorData()
[docs] def load_state(self, state: BeaconState) -> None:
"""
"""
self.store = get_forkchoice_store(state.copy())
self.data.time_ms = self.store.time * 1000
self.data.recorded_attestations = []
self.data.slot = get_current_slot(self.store)
self.data.current_epoch = compute_epoch_at_slot(self.data.slot)
self.data.head_root = self.get_head()
current_state = state.copy()
if current_state.slot < self.data.slot:
process_slots(current_state, self.data.slot)
self.update_attester(current_state, self.data.current_epoch)
self.update_proposer(current_state)
self.update_data()
[docs] def get_hashable_store(self) -> HashableSpecStore:
"""
Returns a hash of the current store state.
Args:
self (BRValidator): Validator
Returns:
HashableSpecStore: A hashable representation of the current `self.store`
"""
return HashableSpecStore(
recorded_attestations = self.data.recorded_attestations,
recorded_blocks = list(self.store.blocks.keys())
)
[docs] def get_head(self) -> Root:
"""
Our cached reimplementation of specs-defined `get_head`.
Args:
self (BRValidator): Validator
Returns:
Root: Current head according to the validator `self.store`
"""
store_root = hash_tree_root(self.get_hashable_store())
# If we can get the head from the cache, great!
if store_root in BRValidator.head_store:
return BRValidator.head_store[store_root]
# Otherwise we must compute it again :(
else:
head_root = get_head(self.store)
BRValidator.head_store[store_root] = head_root
return head_root
[docs] def process_to_slot(self, current_head_root: Root, slot: Slot) -> BeaconState:
"""
Our cached `process_slots` operation.
Args:
self (BRValidator): Validator
current_head_root (Root): Process to slot from this state root
slot (Slot): Slot to process to
Returns:
BeaconState: Post-state after transition to `slot`
"""
# If we want to fast-forward a state root to some slot, we check if we have already recorded the
# resulting state.
if (current_head_root, slot) in BRValidator.state_store:
return BRValidator.state_store[(current_head_root, slot)].copy()
# If we haven't, we need to process it.
else:
current_state = self.store.block_states[current_head_root].copy()
if current_state.slot < slot:
process_slots(current_state, slot)
BRValidator.state_store[(current_head_root, slot)] = current_state
return current_state.copy()
[docs] def update_time(self, frequency: uint64 = frequency) -> None:
"""
Moving validators' clocks by one step.
To keep it simple, we assume frequency is a power of ten.
Args:
self (BRValidator): Validator
frequency (uint64): Simulation update rate
Returns:
None
"""
self.data.time_ms = self.data.time_ms + int(1000 / frequency)
if self.data.time_ms % 1000 == 0:
# The store is updated each second in the specs
on_tick(self.store, self.store.time + 1)
# If a new slot starts, we update
if get_current_slot(self.store) != self.data.slot:
self.update_data()
[docs] def forward_by(self, seconds: uint64, frequency: uint64 = frequency) -> None:
"""
A utility method to forward the clock by a given number of seconds.
Useful for exposition!
Args:
self (BRValidator): Validator
seconds (uint64): Number of seconds to fast-forward by
frequency (uint64): Simulation update rate
Returns:
None
"""
number_ticks = seconds * frequency
for i in range(number_ticks):
self.update_time(frequency)
[docs] def update_attester(self, current_state: BeaconState, epoch: Epoch) -> None:
"""
This is a fairly expensive operation, so we try not to call it when we don't have to.
Update attester duties for the `epoch`.
This can be queried no earlier than two epochs before
(e.g., learn about epoch e + 2 duties at epoch t).
Args:
self (BRValidator): Validator
current_state (BeaconState): The state from which proposer duties are computed
epoch (Epoch): Either `current_epoch` or `current_epoch + 1`
Returns:
None
"""
current_epoch = get_current_epoch(current_state)
# When is the validator scheduled to attest in `epoch`?
(committee, committee_index, attest_slot) = get_committee_assignment(
current_state,
epoch,
self.validator_index)
if epoch == current_epoch:
self.data.current_attest_slot = attest_slot
self.data.current_committee_index = committee_index
self.data.current_committee = committee
elif epoch == current_epoch + 1:
self.data.next_attest_slot = attest_slot
self.data.next_committee_index = committee_index
self.data.next_committee = committee
[docs] def update_proposer(self, current_state: BeaconState) -> None:
"""
This is a fairly expensive operation, so we try not to call it when we don't have to.
Update proposer duties for the current epoch.
We need to check for each slot of the epoch whether the validator is a proposer or not.
Args:
self (BRValidator): Validator
current_state (BeaconState): The state from which proposer duties are computed
Returns:
None
"""
current_epoch = get_current_epoch(current_state)
start_slot = compute_start_slot_at_epoch(current_epoch)
start_state = current_state.copy() if start_slot == current_state.slot else \
self.store.block_states[get_block_root(current_state, current_epoch)].copy()
current_proposer_duties = []
for slot in range(start_slot, start_slot + SLOTS_PER_EPOCH):
if slot < start_state.slot:
current_proposer_duties += [False]
continue
if start_state.slot < slot:
process_slots(start_state, slot)
current_proposer_duties += [get_beacon_proposer_index(start_state) == self.validator_index]
self.data.current_proposer_duties = current_proposer_duties
[docs] def update_attest_move(self) -> None:
"""
When was the last attestation by the validator?
Updates `self.data.last_slot_attested`.
Args:
self (BRValidator): Validator
Returns:
None
"""
slots_attested = sorted([log.slot for log in self.history if log.move == "attest"], reverse = True)
self.data.last_slot_attested = None if len(slots_attested) == 0 else slots_attested[0]
[docs] def update_propose_move(self) -> None:
"""
When was the last block proposal by the validator?
Updates `self.data.last_slot_proposed`.
Args:
self (BRValidator): Validator
Returns:
None
"""
slots_proposed = sorted([log.slot for log in self.history if log.move == "propose"], reverse = True)
self.data.last_slot_proposed = None if len(slots_proposed) == 0 else slots_proposed[0]
[docs] def update_data(self) -> None:
"""
The head may change if we recorded a new block/new attestation in the `store`.
Attester/proposer responsibilities may change if head changes *and*
canonical chain changes to further back from start current epoch.
.. code-block:: txt
---x------
\ x is fork point
-----
In the following
attester = attester responsibilities for current epoch
proposer = proposer responsibilities for current epoch
- If x after current epoch change
(---|--x , | = start current epoch),
proposer and attester don't change
- If x between start of previous epoch and
start of current epoch
(--||--x---|-- , || = start previous epoch)
proposer changes but not attester
- If x before start of previous epoch
(--x--||-----|----) both proposer and attester change
Args:
self (BRValidator): Validator
Returns:
None
"""
slot = get_current_slot(self.store)
new_slot = self.data.slot != slot
# Current epoch in validator view
current_epoch = compute_epoch_at_slot(slot)
self.update_attest_move()
self.update_propose_move()
# Did the validator record a block for this slot?
received_block = len([block for block_root, block in self.store.blocks.items() if block.slot == slot]) > 0
if not new_slot:
# It's not a new slot, we are here because a new block/attestation was received
# Getting the current state, fast-forwarding from the head
head_root = self.get_head()
if self.data.head_root != head_root:
# New head!
lca = lowest_common_ancestor(
self.store, self.data.head_root, head_root)
lca_epoch = compute_epoch_at_slot(lca.slot)
if lca_epoch == current_epoch:
# do nothing
pass
else:
current_state = self.process_to_slot(head_root, slot)
if lca_epoch == current_epoch - 1:
self.update_proposer(current_state)
else:
self.update_proposer(current_state)
self.update_attester(current_state, current_epoch)
self.data.head_root = head_root
else:
# It's a new slot. We should update our proposer/attester duties
# if it's also a new epoch. If not we do nothing.
if self.data.current_epoch != current_epoch:
current_state = self.process_to_slot(self.data.head_root, slot)
# We need to check our proposer role for this new epoch
self.update_proposer(current_state)
# We need to check our attester role for this new epoch
self.update_attester(current_state, current_epoch)
self.data.slot = slot
self.data.current_epoch = current_epoch
self.data.received_block = received_block
[docs] def log_block(self, item: SignedBeaconBlock) -> None:
"""
Recording 'block proposal' move by the validator in its history.
"""
self.history.append(ValidatorMove(
time = self.data.time_ms,
slot = item.message.slot,
move = "propose"
))
self.update_propose_move()
[docs] def log_attestation(self, item: Attestation) -> None:
"""
Recording 'attestation proposal' move by the validator in its history.
"""
self.history.append(ValidatorMove(
time = self.data.time_ms,
slot = item.data.slot,
move = "attest"
))
self.update_attest_move()
[docs] def record_block(self, item: SignedBeaconBlock) -> bool:
"""
When a validator receives a block from the network, they call `record_block` to see
whether they should record it.
"""
# If we already know about the block, do nothing
if hash_tree_root(item.message) in self.store.blocks:
return False
# Sometimes recording the block fails. Examples include:
# - The block slot is not the current slot (we keep it in memory for later, when we check backlog)
# - The block parent is not known
try:
state = self.process_to_slot(item.message.parent_root, item.message.slot)
on_block(self.store, item, state = state)
except AssertionError as e:
return False
# If attestations are included in the block, we want to record them
for attestation in item.message.body.attestations:
self.record_attestation(attestation)
return True
[docs] def record_attestation(self, item: Attestation) -> bool:
"""
When a validator receives an attestation from the network,
they call `record_attestation` to see whether they should record it.
"""
att_hash = hash_tree_root(item)
# If we have already seen this attestation, no need to go further
if att_hash in self.data.recorded_attestations:
return False
# Sometimes recording the attestation fails. Examples include:
# - The attestation is not for the current slot *PLUS ONE*
# (we keep it in memory for later, when we check backlog)
# - The block root it is attesting for is not known
try:
on_attestation(self.store, item)
self.data.recorded_attestations += [att_hash]
return True
except:
return False
[docs] def check_backlog(self, known_items: Dict[str, Sequence[Container]]) -> None:
"""
Called whenever a new event happens on the network that might make a validator update
their internals.
We loop over known blocks and attestations to check whether we should record any
that we might have discarded before, or just received.
"""
recorded_blocks = 0
for block in known_items["blocks"]:
recorded = self.record_block(block.item)
if recorded:
recorded_blocks += 1
recorded_attestations = 0
for attestation in known_items["attestations"]:
recorded = self.record_attestation(attestation.item)
if recorded:
recorded_attestations += 1
# If we do record anything, update the internals.
if recorded_blocks > 0 or recorded_attestations > 0:
self.update_data()
def lowest_common_ancestor(store, old_head, new_head) -> Optional[BeaconBlock]:
"""
Find the lowest common ancestor to `old_head` and `new_head` in `store`.
In most cases, `old_head` is an ancestor to `new_head`.
We sort of (loosely) optimise for this.
"""
new_head_ancestors = [new_head]
current_block = store.blocks[new_head]
keep_searching = True
while keep_searching:
parent_root = current_block.parent_root
parent_block = store.blocks[parent_root]
if parent_root == old_head:
return store.blocks[old_head]
elif parent_block.slot == 0:
keep_searching = False
else:
new_head_ancestors += [parent_root]
current_block = parent_block
# At this point, old_head wasn't an ancestor to new_head
# We need to find old_head's ancestors
current_block = store.blocks[old_head]
keep_searching = True
while keep_searching:
parent_root = current_block.parent_root
parent_block = store.blocks[parent_root]
if parent_root in new_head_ancestors:
return parent_block
elif parent_block.slot == 0:
print("return none")
return None
else:
current_block = parent_block
### Attestation strategies
def get_attestation_signature(state: BeaconState, attestation_data: AttestationData, privkey: int) -> BLSSignature:
domain = get_domain(state, DOMAIN_BEACON_ATTESTER, attestation_data.target.epoch)
signing_root = compute_signing_root(attestation_data, domain)
return bls.Sign(privkey, signing_root)
def honest_attest(validator, known_items):
"""
Returns an honest attestation from `validator`.
Args:
validator (BRValidator): The attesting validator
known_items (Dict): Known blocks and attestations received over-the-wire (but perhaps not included yet in `validator.store`)
Returns:
Attestation: The honest attestation
"""
# Unpacking
validator_index = validator.validator_index
store = validator.store
committee_slot = validator.data.current_attest_slot
committee_index = validator.data.current_committee_index
committee = validator.data.current_committee
# What am I attesting for?
block_root = validator.get_head()
head_state = store.block_states[block_root].copy()
if head_state.slot < committee_slot:
process_slots(head_state, committee_slot)
start_slot = compute_start_slot_at_epoch(get_current_epoch(head_state))
epoch_boundary_block_root = block_root if start_slot == head_state.slot else get_block_root_at_slot(head_state, start_slot)
tgt_checkpoint = Checkpoint(epoch=get_current_epoch(head_state), root=epoch_boundary_block_root)
att_data = AttestationData(
index = committee_index,
slot = committee_slot,
beacon_block_root = block_root,
source = head_state.current_justified_checkpoint,
target = tgt_checkpoint
)
# Set aggregation bits to myself only
committee_size = len(committee)
index_in_committee = committee.index(validator_index)
aggregation_bits = Bitlist[MAX_VALIDATORS_PER_COMMITTEE](*([0] * committee_size))
aggregation_bits[index_in_committee] = True # set the aggregation bit of the validator to True
attestation = Attestation(
aggregation_bits=aggregation_bits,
data=att_data
)
attestation_signature = get_attestation_signature(head_state, att_data, validator.privkey)
attestation.signature = attestation_signature
return attestation
### Aggregation helpers
def get_aggregate_signature(attestations: Sequence[Attestation]) -> BLSSignature:
signatures = [attestation.signature for attestation in attestations]
return bls.Aggregate(signatures)
def build_aggregate(attestations):
"""
Given a set of attestations from the same slot, committee index and vote for
same source, target and beacon block, return an aggregated attestation.
"""
if len(attestations) == 0:
return []
aggregation_bits = Bitlist[MAX_VALIDATORS_PER_COMMITTEE](*([0] * len(attestations[0].aggregation_bits)))
for attestation in attestations:
validator_index_in_committee = attestation.aggregation_bits.index(1)
aggregation_bits[validator_index_in_committee] = True
aggregate_attestation = Attestation(
aggregation_bits=aggregation_bits,
data=attestations[0].data
)
aggregate_signature = get_aggregate_signature(attestations)
aggregate_attestation.signature = aggregate_signature
return aggregate_attestation
def aggregate_attestations(attestations):
"""
Take in a set of attestations. Output aggregated attestations.
"""
hashes = set([hash_tree_root(att.data) for att in attestations])
return [build_aggregate(
[att for att in attestations if att_hash == hash_tree_root(att.data)]
) for att_hash in hashes]
### Proposal strategies
def get_block_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> BLSSignature:
domain = get_domain(state, DOMAIN_BEACON_PROPOSER, compute_epoch_at_slot(block.slot))
signing_root = compute_signing_root(block, domain)
return bls.Sign(privkey, signing_root)
def get_epoch_signature(state: BeaconState, block: BeaconBlock, privkey: int) -> BLSSignature:
domain = get_domain(state, DOMAIN_RANDAO, compute_epoch_at_slot(block.slot))
signing_root = compute_signing_root(compute_epoch_at_slot(block.slot), domain)
return bls.Sign(privkey, signing_root)
def should_process_attestation(state: BeaconState, attestation: Attestation) -> bool:
try:
process_attestation(state.copy(), attestation)
return True
except:
return False
def honest_propose(validator, known_items):
"""
Returns an honest block, using the current LMD-GHOST head and all known, aggregated, attestations.
Args:
validator (BRValidator): The proposing validator
known_items (Dict): Known blocks and attestations received over-the-wire (but perhaps not included yet in `validator.store`)
Returns:
SignedBeaconBlock: The honest proposed block.
"""
print(validator.validator_index, "proposing block for slot", validator.data.slot)
slot = validator.data.slot
head = validator.data.head_root
processed_state = validator.process_to_slot(head, slot)
attestations = [att for att in known_items["attestations"] if should_process_attestation(processed_state, att.item)]
attestations = aggregate_attestations([att.item for att in attestations if slot <= att.item.data.slot + SLOTS_PER_EPOCH])
beacon_block = BeaconBlock(
slot=slot,
parent_root=head,
proposer_index = validator.validator_index,
)
beacon_block_body = BeaconBlockBody(
attestations=attestations
)
epoch_signature = get_epoch_signature(processed_state, beacon_block, validator.privkey)
beacon_block_body.randao_reveal = epoch_signature
beacon_block.body = beacon_block_body
process_block(processed_state, beacon_block)
state_root = hash_tree_root(processed_state)
beacon_block.state_root = state_root
block_signature = get_block_signature(processed_state, beacon_block, validator.privkey)
signed_block = SignedBeaconBlock(message=beacon_block, signature=block_signature)
return signed_block