diff --git a/beacon_chain/spec/beaconstate.nim b/beacon_chain/spec/beaconstate.nim index 2e55a04da..742c1b970 100644 --- a/beacon_chain/spec/beaconstate.nim +++ b/beacon_chain/spec/beaconstate.nim @@ -144,9 +144,10 @@ func update_validator_status*(state: var BeaconState, if new_status in [EXITED_WITH_PENALTY, EXITED_WITHOUT_PENALTY]: exit_validator(state, index, new_status) -func on_startup*(initial_validator_deposits: openArray[Deposit], - genesis_time: uint64, - processed_pow_receipt_root: Eth2Digest): BeaconState = +func get_initial_beacon_state*( + initial_validator_deposits: openArray[Deposit], + genesis_time: uint64, + processed_pow_receipt_root: Eth2Digest): BeaconState = ## BeaconState constructor ## ## Before the beacon chain starts, validators will register in the Eth1 chain diff --git a/beacon_chain/spec/datatypes.nim b/beacon_chain/spec/datatypes.nim index a0a0e61d4..97d8c6375 100644 --- a/beacon_chain/spec/datatypes.nim +++ b/beacon_chain/spec/datatypes.nim @@ -66,7 +66,7 @@ const BLS_WITHDRAWAL_PREFIX_BYTE* = 0'u8 MAX_CASPER_VOTES* = 2^10 - LATEST_BLOCK_ROOTS_COUNT* = 2'u64^13 + LATEST_BLOCK_ROOTS_LENGTH* = 2'u64^13 MIN_BALANCE* = 2'u64^4 ##\ ## Minimum balance in ETH before a validator is removed from the validator @@ -142,8 +142,8 @@ type proposal_signature_2*: ValidatorSig CasperSlashing* = object - votes_1*: SlashableVoteData - votes_2*: SlashableVoteData + slashable_vote_data_1*: SlashableVoteData + slashable_vote_data_2*: SlashableVoteData SlashableVoteData* = object aggregate_signature_poc_0_indices*: seq[Uint24] ##\ @@ -157,9 +157,14 @@ type Attestation* = object data*: AttestationData - participation_bitfield*: seq[byte] # Attester participation bitfield - custody_bitfield*: seq[byte] # Proof of custody bitfield - aggregate_signature*: ValidatorSig # BLS aggregate signature + participation_bitfield*: seq[byte] ##\ + ## The attesters that are represented in the aggregate signature - each + ## bit represents an index in `ShardCommittee.committee` + + custody_bitfield*: seq[byte] ##\ + ## Proof of custody - Phase 1 + aggregate_signature*: ValidatorSig ##\ + ## Aggregate signature of the validators in `custody_bitfield` AttestationData* = object slot*: uint64 @@ -220,9 +225,11 @@ type ## is formed. slot*: uint64 - parent_root*: Eth2Digest + parent_root*: Eth2Digest ##\ + ##\ Root hash of the previous block - state_root*: Eth2Digest + state_root*: Eth2Digest ##\ + ##\ The state root, _after_ this block has been processed randao_reveal*: Eth2Digest ##\ ## Proposer RANDAO reveal @@ -261,12 +268,15 @@ type ## For light clients to easily track delta # Randomness and committees - randao_mix*: Eth2Digest # RANDAO state - next_seed*: Eth2Digest # Randao seed used for next shuffling + randao_mix*: Eth2Digest + next_seed*: Eth2Digest ##\ + ## Randao seed used for next shuffling + shard_committees_at_slots*: array[2 * EPOCH_LENGTH, seq[ShardCommittee]] ## \ ## Committee members and their assigned shard, per slot, covers 2 cycles ## worth of assignments - persistent_committees*: seq[seq[Uint24]] # Persistent shard committees + + persistent_committees*: seq[seq[Uint24]] persistent_committee_reassignments*: seq[ShardReassignmentRecord] # Finality @@ -277,10 +287,12 @@ type latest_crosslinks*: array[SHARD_COUNT, CrosslinkRecord] latest_state_recalculation_slot*: uint64 - latest_block_roots*: array[LATEST_BLOCK_ROOTS_COUNT.int, Eth2Digest] ##\ + latest_block_roots*: array[LATEST_BLOCK_ROOTS_LENGTH.int, Eth2Digest] ##\ ## Needed to process attestations, older to newer + latest_penalized_exit_balances*: seq[uint64] ##\ ## Balances penalized in the current withdrawal period + latest_attestations*: seq[PendingAttestationRecord] batched_block_roots*: seq[Eth2Digest] @@ -299,7 +311,8 @@ type ## * processRandaoReveal randao_layers*: uint64 ##\ - ## Slot the proposer has skipped (ie. layers of RANDAO expected) + ## Number of proposals the proposer missed, and thus the number of times to + ## apply hash function to randao reveal balance*: uint64 # Balance in Gwei status*: ValidatorStatusCodes @@ -310,18 +323,28 @@ type ## Exit counter when validator exited (or 0) CrosslinkRecord* = object - slot*: uint64 # Slot number - shard_block_root*: Eth2Digest # Shard chain block hash + slot*: uint64 + shard_block_root*: Eth2Digest ##\ + ## Shard chain block root ShardCommittee* = object - shard*: uint64 # Shard number - committee*: seq[Uint24] # Validator indices - total_validator_count*: uint64 # # Total validator count (for proofs of custody) + shard*: uint64 + committee*: seq[Uint24] ##\ + ## Committe participants that get to attest to blocks on this shard - + ## indices into BeaconState.validator_registry + + total_validator_count*: uint64 ##\ + ## Total validator count (for proofs of custody) ShardReassignmentRecord* = object - validator_index*: Uint24 # Which validator to reassign - shard*: uint64 # To which shard - slot*: uint64 # When + validator_index*: Uint24 ##\ + ## Which validator to reassign + + shard*: uint64 ##\ + ## To which shard + + slot*: uint64 ##\ + ## When CandidatePoWReceiptRootRecord* = object candidate_pow_receipt_root*: Eth2Digest # Candidate PoW receipt root @@ -338,6 +361,12 @@ type post_fork_version*: uint64 # Post fork version fork_slot*: uint64 # Fork slot number + ValidatorRegistryDeltaBlock* = object + latest_registry_delta_root*: Eth2Digest + validator_index*: Uint24 + pubkey*: ValidatorPubKey + flag*: ValidatorSetDeltaFlags + ValidatorStatusCodes* {.pure.} = enum PENDING_ACTIVATION = 0 ACTIVE = 1 diff --git a/beacon_chain/spec/helpers.nim b/beacon_chain/spec/helpers.nim index 32cb053bd..ad6079bd0 100644 --- a/beacon_chain/spec/helpers.nim +++ b/beacon_chain/spec/helpers.nim @@ -111,11 +111,14 @@ func get_beacon_proposer_index*(state: BeaconState, slot: uint64): Uint24 = ## that have not yet been included into that chain. ## ## idx in Vidx == p(i mod N), pi being a random permutation of validators indices (i.e. a committee) - + # TODO this index is invalid outside of the block state transition function + # because presently, `state.slot += 1` happens before this function + # is called - see also testutil.getNextBeaconProposerIndex let idx = get_shard_committees_index(state, slot) state.shard_committees_at_slots[idx][0].committee.mod_get(slot) -func int_sqrt*(n: SomeInteger): SomeInteger = +func integer_squareroot*(n: SomeInteger): SomeInteger = + ## The largest integer ``x`` such that ``x**2`` is less than ``n``. var x = n y = (x + 1) div 2 @@ -141,4 +144,24 @@ func merkle_root*(values: openArray[Eth2Digest]): Eth2Digest = # o[i] = hash(o[i*2] + o[i*2+1]) # return o[1] # TODO - discard \ No newline at end of file + discard + +proc is_double_vote*(attestation_data_1: AttestationData, + attestation_data_2: AttestationData): bool = + ## Assumes ``attestation_data_1`` is distinct from ``attestation_data_2``. + ## Returns True if the provided ``AttestationData`` are slashable + ## due to a 'double vote'. + attestation_data_1.slot == attestation_data_2.slot + +proc is_surround_vote*(attestation_data_1: AttestationData, + attestation_data_2: AttestationData): bool = + ## Assumes ``attestation_data_1`` is distinct from ``attestation_data_2``. + ## Returns True if the provided ``AttestationData`` are slashable + ## due to a 'surround vote'. + ## Note: parameter order matters as this function only checks + ## that ``attestation_data_1`` surrounds ``attestation_data_2``. + ( + (attestation_data_1.justified_slot < attestation_data_2.justified_slot) and + (attestation_data_1.justified_slot + 1 == attestation_data_2.slot) and + (attestation_data_2.slot < attestation_data_1.slot) + ) diff --git a/beacon_chain/spec/validator.nim b/beacon_chain/spec/validator.nim index 2ffda661f..1fa2d24db 100644 --- a/beacon_chain/spec/validator.nim +++ b/beacon_chain/spec/validator.nim @@ -71,11 +71,12 @@ func get_new_validator_registry_delta_chain_tip*( flag: ValidatorSetDeltaFlags): Eth2Digest = ## Compute the next hash in the validator registry delta hash chain. - withEth2Hash: - h.update hash_tree_root(current_validator_registry_delta_chain_tip) - h.update hash_tree_root(flag.uint8) - h.update hash_tree_root(index) - h.update hash_tree_root(pubkey) + Eth2Digest(data: hash_tree_root(ValidatorRegistryDeltaBlock( + latest_registry_delta_root: current_validator_registry_delta_chain_tip, + validator_index: index, + pubkey: pubkey, + flag: flag + ))) func get_effective_balance*(validator: ValidatorRecord): uint64 = min(validator.balance, MAX_DEPOSIT * GWEI_PER_ETH) diff --git a/beacon_chain/state_transition.nim b/beacon_chain/state_transition.nim index b4b9051c4..62a0cd8af 100644 --- a/beacon_chain/state_transition.nim +++ b/beacon_chain/state_transition.nim @@ -28,7 +28,6 @@ # signed bigint semantics - under- and overflows ensue # * Sane error handling is missing in most cases (yay, we'll get the chance to # debate exceptions again!) -# # When updating the code, add TODO sections to mark where there are clear # improvements to be made - other than that, keep things similar to spec for # now. @@ -44,6 +43,10 @@ func flatten[T](v: openArray[seq[T]]): seq[T] = for x in v: result.add x func verifyProposerSignature(state: BeaconState, blck: BeaconBlock): bool = + ## When creating the block, the proposer will sign a version of the block that + ## doesn't contain the data (chicken and egg), then add the signature to that + ## block. + ## https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#proposer-signature var blck_without_sig = blck @@ -56,15 +59,14 @@ func verifyProposerSignature(state: BeaconState, blck: BeaconBlock): bool = block_root: Eth2Digest(data: hash_tree_root(blck_without_sig)) ) proposal_hash = hash_tree_root(signed_data) - - let proposer_index = get_beacon_proposer_index(state, state.slot) + proposer_index = get_beacon_proposer_index(state, state.slot) bls_verify( state.validator_registry[proposer_index].pubkey, proposal_hash, blck.signature, get_domain(state.fork_data, state.slot, DOMAIN_PROPOSAL)) -func processRandao(state: var BeaconState, blck: BeaconBlock): bool = +func processRandao(state: var BeaconState, blck: BeaconBlock, force: bool): bool = ## When a validator signs up, they will commit an hash to the block, ## the randao_commitment - this hash is the result of a secret value ## hashed n times. @@ -82,10 +84,11 @@ func processRandao(state: var BeaconState, blck: BeaconBlock): bool = proposer_index = get_beacon_proposer_index(state, state.slot) proposer = addr state.validator_registry[proposer_index] - # Check that proposer commit and reveal match - if repeat_hash(blck.randao_reveal, proposer.randao_layers) != - proposer.randao_commitment: - return false + if not force: + # Check that proposer commit and reveal match + if repeat_hash(blck.randao_reveal, proposer.randao_layers) != + proposer.randao_commitment: + return false # Update state and proposer now that we're alright for i, b in state.randao_mix.data: @@ -189,29 +192,32 @@ proc processCasperSlashings(state: var BeaconState, blck: BeaconBlock): bool = return false for casper_slashing in blck.body.casper_slashings: - if not verify_slashable_vote_data(state, casper_slashing.votes_1): - warn("CaspSlash: invalid votes 1") - return false - if not verify_slashable_vote_data(state, casper_slashing.votes_2): - warn("CaspSlash: invalid votes 2") - return false - if not (casper_slashing.votes_1.data != casper_slashing.votes_2.data): + let + slashable_vote_data_1 = casper_slashing.slashable_vote_data_1 + slashable_vote_data_2 = casper_slashing.slashable_vote_data_2 + intersection = filterIt( + indices(slashable_vote_data_1), it in indices(slashable_vote_data_2)) + + if not (slashable_vote_data_1.data != slashable_vote_data_2.data): warn("CaspSlash: invalid data") return false - let intersection = filterIt( - indices(casper_slashing.votes_1), it in indices(casper_slashing.votes_2)) - - if len(intersection) < 1: + if not (len(intersection) >= 1): warn("CaspSlash: no intersection") return false if not ( - ((casper_slashing.votes_1.data.justified_slot + 1 < - casper_slashing.votes_2.data.justified_slot + 1) == - (casper_slashing.votes_2.data.slot < casper_slashing.votes_1.data.slot)) or - (casper_slashing.votes_1.data.slot == casper_slashing.votes_2.data.slot)): - warn("CaspSlash: some weird long condition failed") + is_double_vote(slashable_vote_data_1.data, slashable_vote_data_2.data) or + is_surround_vote(slashable_vote_data_1.data, slashable_vote_data_2.data)): + warn("CaspSlash: surround or double vote check failed") + return false + + if not verify_slashable_vote_data(state, slashable_vote_data_1): + warn("CaspSlash: invalid votes 1") + return false + + if not verify_slashable_vote_data(state, slashable_vote_data_2): + warn("CaspSlash: invalid votes 2") return false for i in intersection: @@ -295,22 +301,56 @@ proc process_ejections(state: var BeaconState) = if is_active_validator(validator) and validator.balance < EJECTION_BALANCE: update_validator_status(state, index.Uint24, EXITED_WITHOUT_PENALTY) -proc processBlock(state: var BeaconState, latest_block, blck: BeaconBlock): bool = +func processSlot(state: var BeaconState, latest_block: BeaconBlock) = + ## Time on the beacon chain moves in slots. Every time we make it to a new + ## slot, a proposer cleates a block to represent the state of the beacon + ## chain at that time. In case the proposer is missing, it may happen that + ## the no block is produced during the slot. + ## + ## https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#per-slot-processing + + state.slot += 1 + state.validator_registry[ + get_beacon_proposer_index(state, state.slot)].randao_layers += 1 + + let + previous_block_root = Eth2Digest(data: hash_tree_root(latest_block)) + for i in 0 ..< state.latest_block_roots.len - 1: + state.latest_block_roots[i] = state.latest_block_roots[i + 1] + state.latest_block_roots[state.latest_block_roots.len - 1] = + previous_block_root + + if state.slot mod LATEST_BLOCK_ROOTS_LENGTH == 0: + state.batched_block_roots.add(merkle_root(state.latest_block_roots)) + + +proc processBlock( + state: var BeaconState, blck: BeaconBlock, force: bool): bool = ## When there's a new block, we need to verify that the block is sane and ## update the state accordingly # TODO when there's a failure, we should reset the state! # TODO probably better to do all verification first, then apply state changes - if blck.slot != state.slot: + if not (blck.slot == state.slot): warn("Unexpected block slot number") return false - if not verifyProposerSignature(state, blck): - warn("Proposer signature not valid") + # TODO does this need checking? not in the spec! + if not (blck.parent_root == state.latest_block_roots[^1]): + warn("Unexpected parent root") return false - if not processRandao(state, blck): + if not force: + # TODO Technically, we could make processBlock take a generic type instead + # of BeaconBlock - we would then have an intermediate `ProposedBlock` + # type that omits some fields - this way, the compiler would guarantee + # that we don't try to access fields that don't have a value yet + if not verifyProposerSignature(state, blck): + warn("Proposer signature not valid") + return false + + if not processRandao(state, blck, force): warn("Randao reveal failed") return false @@ -335,34 +375,10 @@ proc processBlock(state: var BeaconState, latest_block, blck: BeaconBlock): bool return true -func processSlot(state: var BeaconState, latest_block: BeaconBlock) = - ## Time on the beacon chain moves in slots. Every time we make it to a new - ## slot, a proposer cleates a block to represent the state of the beacon - ## chain at that time. In case the proposer is missing, it may happen that - ## the no block is produced during the slot. - ## - ## https://github.com/ethereum/eth2.0-specs/blob/master/specs/core/0_beacon-chain.md#per-slot-processing - - let - latest_hash = Eth2Digest(data: hash_tree_root(latest_block)) - - state.slot += 1 - state.validator_registry[ - get_beacon_proposer_index(state, state.slot)].randao_layers += 1 - - let - previous_block_root = Eth2Digest(data: hash_tree_root(latest_block)) - for i in 0 ..< state.latest_block_roots.len - 1: - state.latest_block_roots[i] = state.latest_block_roots[i + 1] - state.latest_block_roots[state.latest_block_roots.len - 1] = - previous_block_root - - if state.slot mod LATEST_BLOCK_ROOTS_COUNT == 0: - state.batched_block_roots.add(merkle_root(state.latest_block_roots)) - -func get_epoch_boundary_attesters( +func get_attesters( state: BeaconState, attestations: openArray[PendingAttestationRecord]): seq[Uint24] = + # Union of attesters that participated in some attestations # TODO spec - add as helper? deduplicate(flatten(mapIt(attestations, get_attestation_participants(state, it.data, it.participation_bitfield)))) @@ -381,7 +397,7 @@ func boundary_attestations( func sum_effective_balances( state: BeaconState, validator_indices: openArray[Uint24]): uint64 = - # TODO spec - add as helper? + # TODO spec - add as helper? sum(mapIt( validator_indices, get_effective_balance(state.validator_registry[it])) ) @@ -408,7 +424,8 @@ func processEpoch(state: var BeaconState) = total_balance_in_eth = total_balance div GWEI_PER_ETH # The per-slot maximum interest rate is `2/reward_quotient`.) - base_reward_quotient = BASE_REWARD_QUOTIENT * int_sqrt(total_balance_in_eth) + base_reward_quotient = + BASE_REWARD_QUOTIENT * integer_squareroot(total_balance_in_eth) func base_reward(v: ValidatorRecord): uint64 = get_effective_balance(v) div base_reward_quotient.uint64 div 4 @@ -433,7 +450,7 @@ func processEpoch(state: var BeaconState) = this_epoch_attestations) this_epoch_boundary_attesters = - get_epoch_boundary_attesters(state, this_epoch_attestations) + get_attesters(state, this_epoch_attestations) this_epoch_boundary_attesting_balance = sum_effective_balances(state, this_epoch_boundary_attesters) @@ -444,10 +461,8 @@ func processEpoch(state: var BeaconState) = state.slot <= it.data.slot + 2 * EPOCH_LENGTH and it.data.slot + EPOCH_LENGTH < state.slot) - previous_epoch_attesters = flatten(mapIt( - previous_epoch_attestations, - get_attestation_participants(state, it.data, it.participation_bitfield) - )) + previous_epoch_attesters = + get_attesters(state, previous_epoch_attestations) let # Previous epoch justified previous_epoch_justified_attestations = filterIt( @@ -455,10 +470,8 @@ func processEpoch(state: var BeaconState) = it.data.justified_slot == state.previous_justified_slot ) - previous_epoch_justified_attesters = flatten(mapIt( - previous_epoch_justified_attestations, - get_attestation_participants(state, it.data, it.participation_bitfield) - )) + previous_epoch_justified_attesters = + get_attesters(state, previous_epoch_justified_attestations) previous_epoch_justified_attesting_balance = sum_effective_balances(state, previous_epoch_justified_attesters) @@ -472,10 +485,8 @@ func processEpoch(state: var BeaconState) = state, get_block_root(state, negative_uint_hack), previous_epoch_attestations) - previous_epoch_boundary_attesters = flatten(mapIt( - previous_epoch_boundary_attestations, - get_attestation_participants(state, it.data, it.participation_bitfield) - )) + previous_epoch_boundary_attesters = + get_attesters(state, previous_epoch_boundary_attestations) previous_epoch_boundary_attesting_balance = sum_effective_balances(state, previous_epoch_boundary_attesters) @@ -486,10 +497,8 @@ func processEpoch(state: var BeaconState) = previous_epoch_attestations, it.data.beacon_block_root == get_block_root(state, it.data.slot)) - previous_epoch_head_attesters = flatten(mapIt( - previous_epoch_head_attestations, - get_attestation_participants(state, it.data, it.participation_bitfield) - )) + previous_epoch_head_attesters = + get_attesters(state, previous_epoch_head_attestations) previous_epoch_head_attesting_balance = sum_effective_balances(state, previous_epoch_head_attesters) @@ -500,12 +509,11 @@ func processEpoch(state: var BeaconState) = let statePtr = state.addr func attesting_validators( shard_committee: ShardCommittee, shard_block_root: Eth2Digest): seq[Uint24] = - flatten( - mapIt( - filterIt(concat(this_epoch_attestations, previous_epoch_attestations), - it.data.shard == shard_committee.shard and - it.data.shard_block_root == shard_block_root), - get_attestation_participants(statePtr[], it.data, it.participation_bitfield))) + let shard_block_attestations = + filterIt(concat(this_epoch_attestations, previous_epoch_attestations), + it.data.shard == shard_committee.shard and + it.data.shard_block_root == shard_block_root) + get_attesters(statePtr[], shard_block_attestations) func winning_hash(obj: ShardCommittee): Eth2Digest = # * Let `winning_hash(obj)` be the winning `shard_block_root` value. @@ -544,13 +552,13 @@ func processEpoch(state: var BeaconState) = for a in statePtr[].latest_attestations: if v in get_attestation_participants(statePtr[], a.data, a.participation_bitfield): return a.slot_included - assert false # shouldn't happen.. + doAssert false # shouldn't happen.. func inclusion_distance(v: Uint24): uint64 = for a in statePtr[].latest_attestations: if v in get_attestation_participants(statePtr[], a.data, a.participation_bitfield): return a.slot_included - a.data.slot - assert false # shouldn't happen.. + doAssert false # shouldn't happen.. block: # Receipt roots if state.slot mod POW_RECEIPT_ROOT_VOTING_PERIOD == 0: @@ -567,23 +575,25 @@ func processEpoch(state: var BeaconState) = # TODO why are all bits kept? state.justification_bitfield = state.justification_bitfield shl 1 - if 3'u64 * previous_epoch_boundary_attesting_balance >= - 2'u64 * total_balance: - state.justification_bitfield = state.justification_bitfield or 2 - state.justified_slot = state.slot - 2 * EPOCH_LENGTH + # TODO Spec - underflow + if state.slot >= 2'u64 * EPOCH_LENGTH: + if 3'u64 * previous_epoch_boundary_attesting_balance >= + 2'u64 * total_balance: + state.justification_bitfield = state.justification_bitfield or 2 + state.justified_slot = state.slot - 2 * EPOCH_LENGTH - if 3'u64 * this_epoch_boundary_attesting_balance >= - 2'u64 * total_balance: - state.justification_bitfield = state.justification_bitfield or 1 - state.justified_slot = state.slot - 1 * EPOCH_LENGTH + if 3'u64 * this_epoch_boundary_attesting_balance >= + 2'u64 * total_balance: + state.justification_bitfield = state.justification_bitfield or 1 + state.justified_slot = state.slot - 1 * EPOCH_LENGTH block: # Finalization if - (state.previous_justified_slot == state.slot - 2 * EPOCH_LENGTH and + (state.previous_justified_slot + 2 * EPOCH_LENGTH == state.slot and state.justification_bitfield mod 4 == 3) or - (state.previous_justified_slot == state.slot - 3 * EPOCH_LENGTH and + (state.previous_justified_slot + 3 * EPOCH_LENGTH == state.slot and state.justification_bitfield mod 8 == 7) or - (state.previous_justified_slot == state.slot - 4 * EPOCH_LENGTH and + (state.previous_justified_slot + 4 * EPOCH_LENGTH == state.slot and state.justification_bitfield mod 16 in [15'u64, 14]): state.finalized_slot = state.justified_slot @@ -600,42 +610,33 @@ func processEpoch(state: var BeaconState) = let slots_since_finality = state.slot - state.finalized_slot + proc update_balance(attesters: openArray[Uint24], attesting_balance: uint64) = + # TODO Spec - add helper? + for v in attesters: + statePtr.validator_registry[v].balance += adjust_for_inclusion_distance( + base_reward(statePtr.validator_registry[v]) * + attesting_balance div total_balance, inclusion_distance(v)) + + for v in active_validator_indices: + if v notin attesters: + statePtr.validator_registry[v].balance -= + base_reward(statePtr.validator_registry[v]) + if slots_since_finality <= 4'u64 * EPOCH_LENGTH: # Expected FFG source - for v in previous_epoch_justified_attesters: - state.validator_registry[v].balance += adjust_for_inclusion_distance( - base_reward(state.validator_registry[v]) * - previous_epoch_justified_attesting_balance div total_balance, - inclusion_distance(v)) - - for v in active_validator_indices: - if v notin previous_epoch_justified_attesters: - state.validator_registry[v].balance -= - base_reward(state.validator_registry[v]) + update_balance( + previous_epoch_justified_attesters, + previous_epoch_justified_attesting_balance) # Expected FFG target: - for v in previous_epoch_boundary_attesters: - state.validator_registry[v].balance += adjust_for_inclusion_distance( - base_reward(state.validator_registry[v]) * - previous_epoch_boundary_attesting_balance div total_balance, - inclusion_distance(v)) - - for v in active_validator_indices: - if v notin previous_epoch_boundary_attesters: - state.validator_registry[v].balance -= - base_reward(state.validator_registry[v]) + update_balance( + previous_epoch_boundary_attesters, + previous_epoch_boundary_attesting_balance) # Expected beacon chain head: - for v in previous_epoch_head_attesters: - state.validator_registry[v].balance += adjust_for_inclusion_distance( - base_reward(state.validator_registry[v]) * - previous_epoch_head_attesting_balance div total_balance, - inclusion_distance(v)) - - for v in active_validator_indices: - if v notin previous_epoch_head_attesters: - state.validator_registry[v].balance -= - base_reward(state.validator_registry[v]) + update_balance( + previous_epoch_head_attesters, + previous_epoch_head_attesting_balance) else: for v in active_validator_indices: @@ -655,8 +656,7 @@ func processEpoch(state: var BeaconState) = block: # Attestation inclusion for v in previous_epoch_attesters: - let proposer_index = - get_beacon_proposer_index(state, inclusion_slot(v)) + let proposer_index = get_beacon_proposer_index(state, inclusion_slot(v)) state.validator_registry[proposer_index].balance += base_reward(state.validator_registry[v]) div INCLUDER_REWARD_QUOTIENT @@ -745,7 +745,7 @@ func processEpoch(state: var BeaconState) = ) proc updateState*(state: BeaconState, latest_block: BeaconBlock, - new_block: Option[BeaconBlock]): + new_block: Option[BeaconBlock], force: bool): tuple[state: BeaconState, block_ok: bool] = ## Time in the beacon chain moves by slots. Every time (haha.) that happens, ## we will update the beacon state. Normally, the state updates will be driven @@ -753,6 +753,10 @@ proc updateState*(state: BeaconState, latest_block: BeaconBlock, ## missing - the state updates happen regardless. ## Each call to this function will advance the state by one slot - new_block, ## if present, must match that slot. + ## + ## The `force` flag is used to skip a few checks, and apply `new_block` + ## regardless. We do this because some fields in the block depend on the state + ## being updated with the information in the new block. # # TODO this function can be written with a loop inside to handle all empty # slots up to the slot of the new_block - but then again, why not eagerly @@ -779,20 +783,34 @@ proc updateState*(state: BeaconState, latest_block: BeaconBlock, # TODO what should happen if block processing fails? # https://github.com/ethereum/eth2.0-specs/issues/293 var block_state = new_state - if processBlock(block_state, latest_block, new_block.get()): + if processBlock(block_state, new_block.get(), force): # processBlock will mutate the state! only apply if it worked.. # TODO yeah, this too is inefficient new_state = block_state - true + processEpoch(block_state) + + # This is a bit awkward - at the end of processing we verify that the + # state we arrive at is what the block producer thought it would be - + # meaning that potentially, it could fail verification + let expected_state_root = Eth2Digest(data: hash_tree_root(block_state)) + if force or + (new_block.get().state_root == expected_state_root): + new_state = block_state + true + else: + warn("State: root verification failed", + state_root = new_block.get().state_root, expected_state_root) + processEpoch(new_state) + false else: false else: # Skip all per-block processing. Move directly to epoch processing # prison. Do not do any block updates when passing go. - true - # Heavy updates that happen for every epoch - these never fail (or so we hope) - processEpoch(new_state) + # Heavy updates that happen for every epoch - these never fail (or so we hope) + processEpoch(new_state) + true # State update never fails, but block validation might... (new_state, block_ok) diff --git a/research/serialized_sizes.nim b/research/serialized_sizes.nim index ba17f03bc..048fe9fb8 100644 --- a/research/serialized_sizes.nim +++ b/research/serialized_sizes.nim @@ -4,7 +4,8 @@ import ../tests/testutil proc stateSize(deposits: int) = - let state = on_startup(makeInitialDeposits(deposits), 0, Eth2Digest()) + let state = get_initial_beacon_state( + makeInitialDeposits(deposits), 0, Eth2Digest()) echo "Validators: ", deposits, ", total: ", state.serialize().len diff --git a/tests/test_beaconstate.nim b/tests/test_beaconstate.nim index b0af0f5b5..3fadbe468 100644 --- a/tests/test_beaconstate.nim +++ b/tests/test_beaconstate.nim @@ -12,6 +12,7 @@ import ../beacon_chain/spec/[beaconstate, datatypes, digest] suite "Beacon state": - test "Smoke test on_startup": - let state = on_startup(makeInitialDeposits(EPOCH_LENGTH), 0, Eth2Digest()) + test "Smoke test get_initial_beacon_state": + let state = get_initial_beacon_state( + makeInitialDeposits(EPOCH_LENGTH), 0, Eth2Digest()) check: state.validator_registry.len == EPOCH_LENGTH diff --git a/tests/test_state_transition.nim b/tests/test_state_transition.nim index 9dec9035f..75f6533ed 100644 --- a/tests/test_state_transition.nim +++ b/tests/test_state_transition.nim @@ -15,21 +15,28 @@ suite "Block processing": ## For now just test that we can compile and execute block processing with ## mock data. + let + # Genesis state with minimal number of deposits + # TODO bls verification is a bit of a bottleneck here + genesisState = get_initial_beacon_state( + makeInitialDeposits(), 0, Eth2Digest()) + genesisBlock = makeGenesisBlock(genesisState) + test "Passes from genesis state, no block": let - state = on_startup(makeInitialDeposits(), 0, Eth2Digest()) - latest_block = makeGenesisBlock(state) - new_state = updateState(state, latest_block, none(BeaconBlock)) + state = genesisState + latest_block = genesisBlock + new_state = updateState(state, latest_block, none(BeaconBlock), false) check: new_state.state.slot == latest_block.slot + 1 new_state.block_ok test "Passes from genesis state, empty block": let - state = on_startup(makeInitialDeposits(), 0, Eth2Digest()) - latest_block = makeGenesisBlock(state) + state = genesisState + latest_block = genesisBlock new_block = makeBlock(state, latest_block) - new_state = updateState(state, latest_block, some(new_block)) + new_state = updateState(state, latest_block, some(new_block), false) check: new_state.state.slot == latest_block.slot + 1 @@ -37,11 +44,11 @@ suite "Block processing": test "Passes through epoch update, no block": var - state = on_startup(makeInitialDeposits(), 0, Eth2Digest()) - latest_block = makeGenesisBlock(state) + state = genesisState + latest_block = genesisBlock for i in 1..EPOCH_LENGTH.int: - let new_state = updateState(state, latest_block, none(BeaconBlock)) + let new_state = updateState(state, latest_block, none(BeaconBlock), false) check: new_state.block_ok state = new_state.state @@ -51,13 +58,13 @@ suite "Block processing": test "Passes through epoch update, empty block": var - state = on_startup(makeInitialDeposits(), 0, Eth2Digest()) - latest_block = makeGenesisBlock(state) + state = genesisState + latest_block = genesisBlock for i in 1..EPOCH_LENGTH.int: var new_block = makeBlock(state, latest_block) - let new_state = updateState(state, latest_block, some(new_block)) + let new_state = updateState(state, latest_block, some(new_block), false) check: new_state.block_ok @@ -69,11 +76,12 @@ suite "Block processing": test "Increments proposer randao_layers, no block": let - state = on_startup(makeInitialDeposits(), 0, Eth2Digest()) - latest_block = makeGenesisBlock(state) - proposer_index = get_beacon_proposer_index(state, state.slot + 1) - previous_randao_layers = state.validator_registry[proposer_index].randao_layers - new_state = updateState(state, latest_block, none(BeaconBlock)) + state = genesisState + latest_block = genesisBlock + proposer_index = getNextBeaconProposerIndex(state) + previous_randao_layers = + state.validator_registry[proposer_index].randao_layers + new_state = updateState(state, latest_block, none(BeaconBlock), false) updated_proposer = new_state.state.validator_registry[proposer_index] check: @@ -81,12 +89,13 @@ suite "Block processing": test "Proposer randao layers unchanged, empty block": let - state = on_startup(makeInitialDeposits(), 0, Eth2Digest()) - latest_block = makeGenesisBlock(state) - proposer_index = get_beacon_proposer_index(state, state.slot + 1) - previous_randao_layers = state.validator_registry[proposer_index].randao_layers + state = genesisState + latest_block = genesisBlock + proposer_index = getNextBeaconProposerIndex(state) + previous_randao_layers = + state.validator_registry[proposer_index].randao_layers new_block = makeBlock(state, latest_block) - new_state = updateState(state, latest_block, some(new_block)) + new_state = updateState(state, latest_block, some(new_block), false) updated_proposer = new_state.state.validator_registry[proposer_index] check: diff --git a/tests/testutil.nim b/tests/testutil.nim index c9d83267b..68dfee088 100644 --- a/tests/testutil.nim +++ b/tests/testutil.nim @@ -6,8 +6,8 @@ # at your option. This file may not be copied, modified, or distributed except according to those terms. import - milagro_crypto, - ../beacon_chain/[extras, ssz], + options, milagro_crypto, + ../beacon_chain/[extras, ssz, state_transition], ../beacon_chain/spec/[crypto, datatypes, digest, helpers] const @@ -71,25 +71,55 @@ func makeGenesisBlock*(state: BeaconState): BeaconBlock = state_root: Eth2Digest(data: hash_tree_root(state)) ) -func makeBlock*( - state: BeaconState, latest_block: BeaconBlock): BeaconBlock = +func getNextBeaconProposerIndex*(state: BeaconState): Uint24 = # TODO: this works but looks wrong - we update the slot in the state without # updating corresponding data - this works because the state update # code does the same - updates slot, then uses that slot when calling # beacon_proposer_index, then finally updates the shuffling at the end! var next_state = state next_state.slot += 1 - let - proposer = state.validator_registry[ - get_beacon_proposer_index(next_state, next_state.slot)] + get_beacon_proposer_index(next_state, next_state.slot) - var new_block = BeaconBlock( - slot: next_state.slot, - state_root: Eth2Digest(data: hash_tree_root(state)), - randao_reveal: hackReveal(proposer) +proc makeBlock*(state: BeaconState, previous_block: BeaconBlock): BeaconBlock = + # Create a block for `state.slot + 1` - like a block proposer would do! + # It's a bit awkward - in order to produce a block for N+1, we need to + # calculate what the state will look like after that block has been applied, + # because the block includes the state root. + + let + # Index from the new state, but registry from the old state.. hmm... + proposer = state.validator_registry[getNextBeaconProposerIndex(state)] + + var + # In order to reuse the state transition function, we first create a dummy + # block that has some fields set, and use that to generate the state as it + # would look with the new block applied. + new_block = BeaconBlock( + slot: state.slot + 1, + + # TODO is this checked anywhere? + # https://github.com/ethereum/eth2.0-specs/issues/336 + parent_root: Eth2Digest(data: hash_tree_root(previous_block)), + state_root: Eth2Digest(), # we need the new state first + randao_reveal: hackReveal(proposer), + candidate_pow_receipt_root: Eth2Digest(), # TODO + signature: ValidatorSig(), # we need the rest of the block first! + body: BeaconBlockBody() # TODO throw in stuff here... ) let + next_state = updateState(state, previous_block, some(new_block), true) + assert next_state.block_ok + + # Ok, we have the new state as it would look with the block applied - now we + # can set the state root in order to be able to create a valid signature + new_block.state_root = Eth2Digest(data: hash_tree_root(next_state.state)) + + let + proposerPrivkey = hackPrivKey(proposer) + + # Once we've collected all the state data, we sign the block data along with + # some book-keeping values signed_data = ProposalSignedData( slot: new_block.slot, shard: BEACON_CHAIN_SHARD_NUMBER, @@ -97,14 +127,18 @@ func makeBlock*( ) proposal_hash = hash_tree_root(signed_data) - proposerPrivkey = hackPrivKey(proposer) - - assert proposerPrivkey.fromSigKey() == proposer.pubkey + assert proposerPrivkey.fromSigKey() == proposer.pubkey, + "signature key should be derived from private key! - wrong privkey?" + # We have a signature - put it in the block and we should be done! new_block.signature = + # TODO domain missing! signMessage(proposerPrivkey, proposal_hash) - assert verifyMessage( - new_block.signature, proposal_hash, proposerPrivkey.fromSigKey()) + assert bls_verify( + proposer.pubkey, + proposal_hash, new_block.signature, + get_domain(state.fork_data, state.slot, DOMAIN_PROPOSAL)), + "we just signed this message - it should pass verification!" new_block