# Copyright (c) 2018-2023 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: [].} import ./datatypes/[phase0, altair, bellatrix], ./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.4.0-beta.0/specs/phase0/beacon-chain.md#compute_shuffled_index # https://github.com/ethereum/consensus-specs/blob/v1.4.0-alpha.3/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..= MAX_EFFECTIVE_BALANCE * random_byte: return Opt.some(candidate_index) i += 1 # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/phase0/beacon-chain.md#get_beacon_proposer_index func get_beacon_proposer_index*( state: ForkyBeaconState, cache: var StateCache, slot: Slot): Opt[ValidatorIndex] = let epoch = get_current_epoch(state) if slot.epoch() != epoch: # compute_proposer_index depends on `effective_balance`, therefore the # beacon proposer index can only be computed for the "current" epoch: # https://github.com/ethereum/consensus-specs/pull/772#issuecomment-475574357 return Opt.none(ValidatorIndex) cache.beacon_proposer_indices.withValue(slot, proposer) do: return proposer[] do: # Return the beacon proposer index at the current slot. 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 - the same validator may # however propose several times in the same epoch (however unlikely) let # active validator indices are kept in cache but sorting them takes # quite a while indices = get_active_validator_indices(state, epoch) var res: Opt[ValidatorIndex] for epoch_slot in epoch.slots(): buffer[32..39] = uint_to_bytes(epoch_slot.asUInt64) let seed = eth2digest(buffer) let pi = compute_proposer_index(state, indices, seed) if epoch_slot == slot: res = pi cache.beacon_proposer_indices[epoch_slot] = pi return res # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/phase0/beacon-chain.md#get_beacon_proposer_index func get_beacon_proposer_index*(state: ForkyBeaconState, cache: var StateCache): Opt[ValidatorIndex] = ## Return the beacon proposer index at the current slot. get_beacon_proposer_index(state, cache, state.slot) func get_beacon_proposer_index*(state: ForkedHashedBeaconState, cache: var StateCache, slot: Slot): Opt[ValidatorIndex] = withState(state): get_beacon_proposer_index(forkyState.data, cache, slot) # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/phase0/validator.md#aggregation-selection func is_aggregator*(committee_len: uint64, slot_signature: ValidatorSig): bool = let modulo = max(1'u64, committee_len div TARGET_AGGREGATORS_PER_COMMITTEE) bytes_to_uint64(eth2digest( slot_signature.toRaw()).data.toOpenArray(0, 7)) mod modulo == 0 # https://github.com/ethereum/builder-specs/pull/47 func livenessFailsafeInEffect*( block_roots: array[Limit SLOTS_PER_HISTORICAL_ROOT, Eth2Digest], slot: Slot): bool = const MAX_MISSING_CONTIGUOUS = 3 MAX_MISSING_WINDOW = 5 static: doAssert MAX_MISSING_WINDOW > MAX_MISSING_CONTIGUOUS if slot <= MAX_MISSING_CONTIGUOUS: # Cannot ever trigger and allows a bit of safe arithmetic. Furthermore # there's notionally always a genesis block, which pushes the earliest # possible failure out an additional slot. return false # Using this slightly convoluted construction to handle wraparound better; # baseIndex + faultInspectionWindow can overflow array but only exactly by # the required amount. Furthermore, go back one more slot to address using # that it looks ahead rather than looks back and whether a block's missing # requires seeing the previous block_root. let faultInspectionWindow = min(distinctBase(slot) - 1, SLOTS_PER_EPOCH) baseIndex = (slot + SLOTS_PER_HISTORICAL_ROOT - faultInspectionWindow) mod SLOTS_PER_HISTORICAL_ROOT endIndex = baseIndex + faultInspectionWindow - 1 doAssert endIndex mod SLOTS_PER_HISTORICAL_ROOT == (slot - 1) mod SLOTS_PER_HISTORICAL_ROOT var totalMissing = 0 streakLen = 0 maxStreakLen = 0 for i in baseIndex .. endIndex: # This look-forward means checking slot i for being missing uses i - 1 if block_roots[(i mod SLOTS_PER_HISTORICAL_ROOT).int] == block_roots[((i + 1) mod SLOTS_PER_HISTORICAL_ROOT).int]: totalMissing += 1 if totalMissing > MAX_MISSING_WINDOW: return true streakLen += 1 if streakLen > maxStreakLen: maxStreakLen = streakLen if maxStreakLen > MAX_MISSING_CONTIGUOUS: return true else: streakLen = 0 false # https://github.com/ethereum/consensus-specs/blob/v1.4.0-alpha.3/specs/phase0/p2p-interface.md#attestation-subnet-subcription func compute_subscribed_subnet(node_id: UInt256, epoch: Epoch, index: uint64): SubnetId = # Ensure neither `truncate` loses information static: doAssert EPOCHS_PER_SUBNET_SUBSCRIPTION <= high(uint64) doAssert sizeof(UInt256) * 8 == NODE_ID_BITS doAssert ATTESTATION_SUBNET_PREFIX_BITS < sizeof(SubnetId) * 8 let node_id_prefix = truncate( node_id shr (NODE_ID_BITS - ATTESTATION_SUBNET_PREFIX_BITS), uint64) node_offset = truncate(node_id mod EPOCHS_PER_SUBNET_SUBSCRIPTION, uint64) permutation_seed = eth2digest(uint_to_bytes( uint64((epoch + node_offset) div EPOCHS_PER_SUBNET_SUBSCRIPTION))) permutated_prefix = compute_shuffled_index( node_id_prefix, 1 shl ATTESTATION_SUBNET_PREFIX_BITS, permutation_seed, ) SubnetId((permutated_prefix + index) mod ATTESTATION_SUBNET_COUNT) # https://github.com/ethereum/consensus-specs/blob/v1.4.0-alpha.3/specs/phase0/p2p-interface.md#attestation-subnet-subcription iterator compute_subscribed_subnets*(node_id: UInt256, epoch: Epoch): SubnetId = for index in 0'u64 ..< SUBNETS_PER_NODE: yield compute_subscribed_subnet(node_id, epoch, index)