diff --git a/eth2_node.py b/eth2_node.py index 9d00bc4..423c19c 100644 --- a/eth2_node.py +++ b/eth2_node.py @@ -1,32 +1,94 @@ +from typing import List from eth2spec.phase0.mainnet import ( - Container, - Slot, - AttestationData, - Attestation, - ValidatorIndex, + Container, SignedBeaconBlock, uint64, Bytes32, + BLSPubkey, BLSSignature, + Slot, Epoch, + ValidatorIndex, CommitteeIndex, + AttestationData, Attestation, + BeaconBlock, SignedBeaconBlock, ) class AttestationDuty(Container): - # TODO: Update schema to include committee_index etc. as defined - # in https://ethereum.github.io/beacon-APIs/#/ + pubkey: BLSPubkey + validator_index: ValidatorIndex + committee_index: CommitteeIndex + committee_length: uint64 + committees_at_slot: uint64 + validator_committee_index: ValidatorIndex # TODO: Is this the correct datatype? slot: Slot validator_index: ValidatorIndex +class ProposerDuty(Container): + pubkey: BLSPubkey + validator_index: ValidatorIndex + slot: Slot + + +# Beacon Node Interface -# Beacon Node -def bn_get_next_attestation_duty() -> AttestationDuty: +def bn_get_attestation_duties_for_epoch(validator_indices: List[ValidatorIndex], epoch: Epoch) -> List[AttestationDuty]: + # TODO: Define typing here: + # What's the size of validator_indices & the returned attestation_duties? + """Fetch attestation duties for the validator indices in the epoch. + Uses https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getAttesterDuties + """ pass -def bn_broadcast_attestation(attestation: Attestation) -> None: +def bn_get_attestation_data(slot: Slot, committee_index: CommitteeIndex) -> AttestationData: + """Produces attestation data for the given slot & committee index. + Uses https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/produceAttestationData + """ pass - -# Validator Client -def vc_is_slashable(attestation_data: AttestationData, validator_index: ValidatorIndex) -> bool: - # TODO: Should we use validator index or pubkey? + +def bn_submit_attestation(attestation: Attestation) -> None: + """Submit attestation to BN for p2p gossip. + Uses https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolAttestations + """ + pass + +def bn_get_proposer_duties_for_epoch(epoch: Epoch) -> List[ProposerDuty]: + """Fetch proposer duties for all proposers in the epoch. + Uses https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/getProposerDuties + """ + pass + +def bn_produce_block(slot: Slot, randao_reveal: BLSSignature, graffiti: Bytes32) -> BeaconBlock: + """Produces valid block for given slot using provided data + Uses https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/produceBlockV2 + """ pass -# TODO: What object does the VC sign? -# Is it the same object that the BN accepts for broadcast? -def vc_sign_attestation(attestation_data: AttestationData, validator_index: ValidatorIndex) -> AttestationData: +def bn_submit_block(block: SignedBeaconBlock) -> None: + """Submit block to BN for p2p gossip. + Uses https://ethereum.github.io/beacon-APIs/#/ValidatorRequiredApi/publishBlock + """ pass + +# Validator Client Interface + +def vc_is_slashable_attestation_data(attestation_data: AttestationData, validator_pubkey: BLSPubkey) -> bool: + """Checks whether the attestation data is slashable according to the anti-slashing database. + This endpoint does not exist in beacon-APIs. + """ + pass + +def vc_sign_attestation(attestation_data: AttestationData, attestation_duty: AttestationDuty) -> Attestation: + """Returns a signed attestations that is constructed using the given attestation data & attestation duty. + This endpoint does not exist in beacon-APIs. + """ + # See note about attestation construction here: + # https://github.com/ethereum/beacon-APIs/blame/05c1bc142e1a3fb2a63c79098743776241341d08/validator-flow.md#L35-L37 + pass + +def vc_is_slashable_block(block: BeaconBlock, validator_pubkey: BLSPubkey) -> bool: + """Checks whether the block is slashable according to the anti-slashing database. + This endpoint does not exist in beacon-APIs. + """ + pass + +def vc_sign_block(block: BeaconBlock, proposer_duty: ProposerDuty) -> SignedBeaconBlock: + """Returns a signed beacon block using the validator index given in the proposer duty. + This endpoint does not exist in beacon-APIs. + """ + pass diff --git a/spec.py b/spec.py index d065901..b1da1f2 100644 --- a/spec.py +++ b/spec.py @@ -1,39 +1,105 @@ -from test import ( - get_current_time, - bn_get_next_attestation_duty, - bn_broadcast_attestation, +from eth2_node import ( + AttestationDuty, ProposerDuty, + AttestationData, BeaconBlock, + bn_get_attestation_duties_for_epoch, + bn_get_attestation_data, + bn_submit_attestation, + bn_get_proposer_duties_for_epoch, + bn_produce_block, + bn_submit_block, + vc_is_slashable_attestation_data, vc_sign_attestation, - calculate_attestation_time, - consensus, -) -from eth2spec.phase0.mainnet import ( - Attestation, + vc_is_slashable_block, + vc_sign_block, ) -def attestation_duty_loop(): - # run in a loop forever - attestation_duty = bn_get_next_attestation_duty() - while get_current_time() < calculate_attestation_time(attestation_duty.slot): - pass - # Obtain lock on consensus process here - only a single consensus instance - # should be running at any given time - attestation_data = consensus(attestation_duty.slot) +""" +Consensus Specification +""" + +def consensus_is_valid_attestation_data(attestation_data: AttestationData, attestation_duty: AttestationDuty) -> bool: + """Determines if the given attestation is valid for the attestation duty. + """ + assert attestation_data.slot == attestation_duty.slot + assert attestation_data.committee_index == attestation_duty.committee_index + assert not vc_is_slashable_attestation_data(attestation_data, attestation_duty.pubkey) + +def consensus_on_attestation(attestation_duty: AttestationDuty) -> AttestationData: + """Consensus protocol between distributed validator nodes for attestation values. + Returns the decided value. + The consensus protocol must use `consensus_is_valid_attestation_data` to determine + validity of the proposed attestation value. + """ + pass + +def consensus_is_valid_block(block: BeaconBlock, proposer_duty: ProposerDuty) -> bool: + """Determines if the given block is valid for the proposer duty. + """ + assert block.slot == proposer_duty.slot + # TODO: Assert correct block.proposer_index + assert not vc_is_slashable_block(block, proposer_duty.pubkey) + +def consensus_on_block(proposer_duty: ProposerDuty) -> AttestationData: + """Consensus protocol between distributed validator nodes for block values. + Returns the decided value. + The consensus protocol must use `consensus_is_valid_block` to determine + validity of the proposed block value. + """ + pass + + +""" +Attestation Production Process: +1. At the start of every epoch, get attestation duties for epoch+1 by running + bn_get_attestation_duties_for_epoch(validator_indices, epoch+1) +2. For each attestation_duty recevied in Step 1, schedule + serve_attestation_duty(attestation_duty) at 1/3rd way through the slot + attestation_duty.slot + +See notes here: +https://github.com/ethereum/beacon-APIs/blob/05c1bc142e1a3fb2a63c79098743776241341d08/validator-flow.md#attestation +""" + +def serve_attestation_duty(attestation_duty): + # Obtain lock on consensus_on_attestation here. + # Only a single consensus_on_attestation instance should be + # running at any given time + attestation_data = consensus_on_attestation(attestation_duty) # 1. Threshold sign attestation from local VC - threshold_signed_attestation_data = vc_sign_attestation(attestation_data, attestation_duty.validator_index) + threshold_signed_attestation = vc_sign_attestation(attestation_data, attestation_duty) # 2. Broadcast threshold signed attestation - # TODO # 3. Reconstruct complete signed attestation by combining threshold signed attestations - complete_signed_attestation_data = threshold_signed_attestation_data - complete_signed_attestation = Attestation(data=complete_signed_attestation_data) + complete_signed_attestation = threshold_signed_attestation # 4. Send complete signed attestation to BN for broadcast - bn_broadcast_attestation(complete_signed_attestation) + bn_submit_attestation(complete_signed_attestation) + + # Release lock on consensus_on_attestation here. + +""" +Block Production Process: +1. At the start of every epoch, get proposer duties for epoch+1 by running + bn_get_proposer_duties_for_epoch(epoch+1) +2. For each proposer_duty recevied in Step 1 for our validators, schedule + serve_proposer_duty(proposer_duty) at beginning of slot proposer_duty.slot + +See notes here: +https://github.com/ethereum/beacon-APIs/blob/05c1bc142e1a3fb2a63c79098743776241341d08/validator-flow.md#block-proposing +""" - # Release lock on consensus process here +def serve_proposer_duty(proposer_duty): + # Obtain lock on consensus_on_block here. + # Only a single consensus_on_block instance should be + # running at any given time + block = consensus_on_block(proposer_duty) - print( - f"Duty Slot: {attestation_duty.slot}, Attestation Slot: {attestation_data.slot}") + # 1. Threshold sign block from local VC + threshold_signed_block = vc_sign_block(block, proposer_duty) + # 2. Broadcast threshold signed block + # 3. Reconstruct complete signed block by combining threshold signed blocks + complete_signed_block = threshold_signed_block + # 4. Send complete signed block to BN for broadcast + bn_submit_block(complete_signed_block) -while True: - attestation_duty_loop() + # Release lock on consensus_on_block here. diff --git a/test.py b/test.py index 2cd8c35..cbc9bb5 100644 --- a/test.py +++ b/test.py @@ -1,75 +1,122 @@ -from eth2_node import ( - AttestationData, - AttestationDuty, - Attestation, - ValidatorIndex, -) +import random from eth2spec.phase0.mainnet import ( + SLOTS_PER_EPOCH, TARGET_COMMITTEE_SIZE, + Checkpoint, is_slashable_attestation_data, + compute_start_slot_at_epoch, + compute_epoch_at_slot, + config, ) +from eth2_node import ( + List, + AttestationDuty, ProposerDuty, + Container, SignedBeaconBlock, uint64, Bytes32, + BLSPubkey, BLSSignature, + Slot, Epoch, + ValidatorIndex, CommitteeIndex, + AttestationData, Attestation, + BeaconBlock, SignedBeaconBlock, +) + +VALIDATOR_SET_SIZE = SLOTS_PER_EPOCH +VALIDATOR_INDICES = [1, 2, 3] +GENESIS_TIME = 0 def time_generator(): - time = 0 + time = GENESIS_TIME while True: yield time time += 1 - timer = time_generator() - def get_current_time(): # Returns current time return next(timer) -# Beacon Node methods -def attestation_duty_generator(): - val_index = 1 - slot = 0 - while True: - yield slot, val_index - slot += 1 - -attestation_duty_source = attestation_duty_generator() - -def bn_get_next_attestation_duty() -> AttestationDuty: - slot, val_index = next(attestation_duty_source) - return AttestationDuty(slot=slot, validator_index=val_index) +# Beacon Node Methods + +def bn_get_attestation_duties_for_epoch(validator_indices: List[ValidatorIndex], epoch: Epoch) -> List[AttestationDuty]: + attestation_duties = [] + for validator_index in validator_indices: + start_slot_at_epoch = compute_start_slot_at_epoch(epoch) + attestation_slot = start_slot_at_epoch + random.randrange(SLOTS_PER_EPOCH) + attestation_duty = AttestationDuty(validator_index=validator_index, + committee_length=TARGET_COMMITTEE_SIZE, + validator_committee_index=random.randrange(TARGET_COMMITTEE_SIZE), + slot=attestation_slot) + attestation_duties.append(attestation_duty) + return attestation_duties + +def bn_get_attestation_data(slot: Slot, committee_index: CommitteeIndex) -> AttestationData: + attestation_data = AttestationData( slot=slot, + index=committee_index, + source=Checkpoint(epoch=min(compute_epoch_at_slot(slot) - 1, 0)), + target=Checkpoint(epoch=compute_epoch_at_slot(slot))) + return attestation_data -def bn_broadcast_attestation(attestation: Attestation) -> None: +def bn_submit_attestation(attestation: Attestation) -> None: pass -# Validator Client methods -vc_slashing_db = {} - -def vc_is_slashable(attestation_data: AttestationData, validator_index: ValidatorIndex) -> bool: - if validator_index not in vc_slashing_db: - vc_slashing_db[validator_index] = set() - for past_attestation_data in vc_slashing_db[validator_index]: - if is_slashable_attestation_data(past_attestation_data, attestation_data): - return True +def bn_get_proposer_duties_for_epoch(epoch: Epoch) -> List[ProposerDuty]: + proposer_duties = [] + validator_indices = [x for x in range(VALIDATOR_SET_SIZE)] + random.shuffle(validator_indices) + for i in range(SLOTS_PER_EPOCH): + proposer_duties.append(ProposerDuty(validator_index=validator_indices[i], slot=i)) + return proposer_duties + +def bn_produce_block(slot: Slot, randao_reveal: BLSSignature, graffiti: Bytes32) -> BeaconBlock: + block = BeaconBlock() + block.slot = slot + block.body.randao_reveal = randao_reveal + block.body.graffiti = graffiti + return block + +# Validator Client Methods + +attestation_slashing_db = {} + +def update_attestation_slashing_db(attestation_data, validator_pubkey): + """Check that the attestation data is not slashable for the validator and + add attestation to slashing DB. + """ + if validator_pubkey not in attestation_slashing_db: + attestation_slashing_db[validator_pubkey] = set() + assert not vc_is_slashable_attestation_data(attestation_data, validator_pubkey) + attestation_slashing_db[validator_pubkey].add(attestation_data) + +def vc_is_slashable_attestation_data(attestation_data: AttestationData, validator_pubkey: BLSPubkey) -> bool: + if validator_pubkey in attestation_slashing_db: + for past_attestation_data in attestation_slashing_db[validator_pubkey]: + if is_slashable_attestation_data(past_attestation_data, attestation_data): + return True return False -def vc_sign_attestation(attestation_data: AttestationData, validator_index: ValidatorIndex) -> AttestationData: - assert not vc_is_slashable(attestation_data, validator_index) - vc_slashing_db[validator_index].add(attestation_data) - return attestation_data - - -# Other methods - -def calculate_attestation_time(slot): - return 12 * slot + 4 - -def is_valid_attestation_data(slot, attestation_data): - # Determines if `attestation_data` is valid for `slot` - return attestation_data.slot == slot +def vc_sign_attestation(attestation_data: AttestationData, attestation_duty) -> Attestation: + update_attestation_slashing_db(attestation_data, attestation_duty.pubkey) + attestation = Attestation(data=attestation_data) + attestation.aggregation_bits[attestation_duty.validator_committee_index] = 1 + return attestation + +block_slashing_db = {} + +def update_block_slashing_db(block, validator_pubkey): + """Check that the block is not slashable for the validator and + add block to slashing DB. + """ + if validator_pubkey not in block_slashing_db: + block_slashing_db[validator_pubkey] = set() + assert not vc_is_slashable_block(block, validator_pubkey) + block_slashing_db[validator_pubkey].add(block) + +def vc_is_slashable_block(block: BeaconBlock, validator_pubkey: BLSPubkey) -> bool: + if validator_pubkey in block_slashing_db: + for past_block in block_slashing_db[validator_pubkey]: + if past_block.slot == block.slot: + return True + return False -def consensus(slot): - # Returns decided value for consensus instance at `slot` - attestation_data = AttestationData() - attestation_data.slot = slot - attestation_data.target.epoch = slot - is_valid_attestation_data(slot, attestation_data) - return attestation_data +def vc_sign_block(block: BeaconBlock, proposer_duty: ProposerDuty) -> SignedBeaconBlock: + return SignedBeaconBlock(message=block) \ No newline at end of file