# Copyright (c) 2018-2021 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. # Helpers and functions pertaining to managing the validator set {.push raises: [Defect].} import std/[options, math, tables], ./datatypes/[phase0, altair, merge], ./helpers export helpers const SEED_SIZE = sizeof(Eth2Digest) ROUND_SIZE = 1 POSITION_WINDOW_SIZE = 4 PIVOT_VIEW_SIZE = SEED_SIZE + ROUND_SIZE TOTAL_SIZE = PIVOT_VIEW_SIZE + POSITION_WINDOW_SIZE # https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/phase0/beacon-chain.md#compute_shuffled_index # https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/phase0/beacon-chain.md#compute_committee # Port of https://github.com/protolambda/zrnt/blob/master/eth2/beacon/shuffle.go # Shuffles or unshuffles, depending on the `dir` (true for shuffling, false for unshuffling func shuffle_list*(input: var seq[ValidatorIndex], seed: Eth2Digest) = let list_size = input.lenu64 if list_size <= 1: return var buf {.noinit.}: array[TOTAL_SIZE, byte] # Seed is always the first 32 bytes of the hash input, we never have to change # this part of the buffer. buf[0..<32] = seed.data # The original code includes a direction flag, but only the reverse direction # is used in eth2, so we simplify it here for r in 0'u8..= uint64(result) func get_committee_count_per_slot*(state: ForkedHashedBeaconState, epoch: Epoch, cache: var StateCache): uint64 = withState(state): get_committee_count_per_slot(state.data, epoch, cache) iterator committee_indices_per_slot*(state: ForkyBeaconState, epoch: Epoch, cache: var StateCache): CommitteeIndex = for idx in 0'u64 ..< get_committee_count_per_slot(state, epoch, cache): yield CommitteeIndex.verifiedValue(idx) func get_committee_count_per_slot*(state: ForkyBeaconState, slot: Slot, cache: var StateCache): uint64 = get_committee_count_per_slot(state, slot.compute_epoch_at_slot, cache) # https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/phase0/beacon-chain.md#get_previous_epoch func get_previous_epoch*(current_epoch: Epoch): Epoch = ## Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``). if current_epoch == GENESIS_EPOCH: current_epoch else: current_epoch - 1 func get_previous_epoch*(state: ForkyBeaconState): Epoch = ## Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``). # Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``). get_previous_epoch(get_current_epoch(state)) # https://github.com/ethereum/consensus-specs/blob/v1.0.1/specs/phase0/beacon-chain.md#get_previous_epoch func get_previous_epoch*(state: ForkedHashedBeaconState): Epoch = ## Return the previous epoch (unless the current epoch is ``GENESIS_EPOCH``). get_previous_epoch(get_current_epoch(state)) # https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/phase0/beacon-chain.md#compute_committee func compute_committee_slice*( active_validators, index, count: uint64): Slice[int] = doAssert active_validators <= ValidatorIndex.high.uint64 if index < count: let start = (active_validators * index) div count endIdx = (active_validators * (index + 1)) div count start.int..(endIdx.int - 1) else: 0 .. -1 iterator compute_committee*(shuffled_indices: seq[ValidatorIndex], index: uint64, count: uint64): ValidatorIndex = let slice = compute_committee_slice(shuffled_indices.lenu64, index, count) for i in slice: yield shuffled_indices[i] func compute_committee*(shuffled_indices: seq[ValidatorIndex], index: uint64, count: uint64): seq[ValidatorIndex] = ## Return the committee corresponding to ``indices``, ``seed``, ``index``, ## and committee ``count``. ## In this version, we pass in the shuffled indices meaning we no longer need ## the seed. let slice = compute_committee_slice(shuffled_indices.lenu64, index, count) # In spec, this calls get_shuffled_index() every time, but that's wasteful # Here, get_beacon_committee() gets the shuffled version. shuffled_indices[slice] func compute_committee_len*( active_validators, index, count: uint64): uint64 = ## Return the committee corresponding to ``indices``, ``seed``, ``index``, ## and committee ``count``. let slice = compute_committee_slice(active_validators, index, count) (slice.b - slice.a + 1).uint64 # https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/phase0/beacon-chain.md#get_beacon_committee iterator get_beacon_committee*( state: ForkyBeaconState, slot: Slot, index: CommitteeIndex, cache: var StateCache): ValidatorIndex = ## Return the beacon committee at ``slot`` for ``index``. let epoch = compute_epoch_at_slot(slot) committees_per_slot = get_committee_count_per_slot(state, epoch, cache) for idx in compute_committee( cache.get_shuffled_active_validator_indices(state, epoch), (slot mod SLOTS_PER_EPOCH) * committees_per_slot + index.uint64, committees_per_slot * SLOTS_PER_EPOCH ): yield idx func get_beacon_committee*( state: ForkyBeaconState, slot: Slot, index: CommitteeIndex, cache: var StateCache): seq[ValidatorIndex] = ## Return the beacon committee at ``slot`` for ``index``. let epoch = compute_epoch_at_slot(slot) committees_per_slot = get_committee_count_per_slot(state, epoch, cache) compute_committee( cache.get_shuffled_active_validator_indices(state, epoch), (slot mod SLOTS_PER_EPOCH) * committees_per_slot + index.uint64, committees_per_slot * SLOTS_PER_EPOCH ) func get_beacon_committee*( state: ForkedHashedBeaconState, slot: Slot, index: CommitteeIndex, cache: var StateCache): seq[ValidatorIndex] = # This one is used by tests/, ncli/, and a couple of places in RPC # TODO use the iterator version alone, to remove the risk of using # diverging get_beacon_committee() in tests and beacon_chain/ by a # wrapper approach (e.g., toSeq). This is a perf tradeoff for test # correctness/consistency. withState(state): get_beacon_committee(state.data, slot, index, cache) # https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/phase0/beacon-chain.md#get_beacon_committee func get_beacon_committee_len*( state: ForkyBeaconState, slot: Slot, index: CommitteeIndex, cache: var StateCache): uint64 = # Return the number of members in the beacon committee at ``slot`` for ``index``. let epoch = compute_epoch_at_slot(slot) committees_per_slot = get_committee_count_per_slot(state, epoch, cache) compute_committee_len( count_active_validators(state, epoch, cache), (slot mod SLOTS_PER_EPOCH) * committees_per_slot + index.uint64, committees_per_slot * SLOTS_PER_EPOCH ) func get_beacon_committee_len*( state: ForkedHashedBeaconState, slot: Slot, index: CommitteeIndex, cache: var StateCache): uint64 = # This one is used by tests withState(state): get_beacon_committee_len(state.data, slot, index, cache) # https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/phase0/beacon-chain.md#compute_shuffled_index func compute_shuffled_index*( index: uint64, index_count: uint64, seed: Eth2Digest): uint64 = ## Return the shuffled index corresponding to ``seed`` (and ``index_count``). doAssert index < index_count var source_buffer {.noinit.}: array[(32+1+4), byte] cur_idx_permuted = index source_buffer[0..31] = seed.data # Swap or not (https://link.springer.com/content/pdf/10.1007%2F978-3-642-32009-5_1.pdf) # See the 'generalized domain' algorithm on page 3 for current_round in 0'u8 ..< SHUFFLE_ROUND_COUNT.uint8: source_buffer[32] = current_round let # If using multiple indices, can amortize this pivot = bytes_to_uint64(eth2digest(source_buffer.toOpenArray(0, 32)).data.toOpenArray(0, 7)) mod index_count flip = ((index_count + pivot) - cur_idx_permuted) mod index_count position = max(cur_idx_permuted, flip) source_buffer[33..36] = uint_to_bytes4((position shr 8)) let source = eth2digest(source_buffer).data byte_value = source[(position mod 256) shr 3] bit = (byte_value shr (position mod 8)) mod 2 cur_idx_permuted = if bit != 0: flip else: cur_idx_permuted cur_idx_permuted # https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/phase0/beacon-chain.md#compute_proposer_index func compute_proposer_index(state: ForkyBeaconState, indices: seq[ValidatorIndex], seed: Eth2Digest): Option[ValidatorIndex] = ## Return from ``indices`` a random index sampled by effective balance. const MAX_RANDOM_BYTE = 255 if len(indices) == 0: return none(ValidatorIndex) let seq_len = indices.lenu64 var i = 0'u64 buffer: array[32+8, byte] buffer[0..31] = seed.data while true: buffer[32..39] = uint_to_bytes8(i div 32) let candidate_index = indices[compute_shuffled_index(i mod seq_len, seq_len, seed)] random_byte = (eth2digest(buffer).data)[i mod 32] effective_balance = state.validators[candidate_index].effective_balance if effective_balance * MAX_RANDOM_BYTE >= MAX_EFFECTIVE_BALANCE * random_byte: return some(candidate_index) i += 1 # https://github.com/ethereum/consensus-specs/blob/v1.1.5/specs/phase0/beacon-chain.md#get_beacon_proposer_index func get_beacon_proposer_index*( state: ForkyBeaconState, cache: var StateCache, slot: Slot): Option[ValidatorIndex] = cache.beacon_proposer_indices.withValue(slot, proposer) do: return proposer[] do: # Return the beacon proposer index at the current slot. let epoch = get_current_epoch(state) var buffer: array[32 + 8, byte] buffer[0..31] = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER).data # There's exactly one beacon proposer per slot. let # active validator indices are kept in cache but sorting them takes # quite a while indices = get_active_validator_indices(state, epoch) start = epoch.compute_start_slot_at_epoch() var res: Option[ValidatorIndex] for i in 0..