# Copyright (c) 2018-2024 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. {.push raises: [].} # Helpers and functions pertaining to managing the validator set import std/algorithm, "."/[crypto, 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.6/specs/phase0/beacon-chain.md#compute_shuffled_index # https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.8/specs/phase0/beacon-chain.md#compute_committee # Port of https://github.com/protolambda/zrnt/blob/v0.14.0/eth2/beacon/shuffle.go 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..= ConsensusFork.Electra: MAX_EFFECTIVE_BALANCE_ELECTRA.Gwei # [Modified in Electra:EIP7251] else: MAX_EFFECTIVE_BALANCE.Gwei if effective_balance * MAX_RANDOM_BYTE >= max_effective_balance * random_byte: res = Opt.some(candidate_index) break i += 1 doAssert res.isSome res func compute_proposer_index(state: ForkyBeaconState, indices: openArray[ValidatorIndex], seed: Eth2Digest): Opt[ValidatorIndex] = ## Return from ``indices`` a random index sampled by effective balance. compute_proposer_index(state, indices, seed, shuffled_index) # https://github.com/ethereum/consensus-specs/blob/v1.5.0-alpha.8/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 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.6/specs/phase0/beacon-chain.md#get_beacon_proposer_index func get_beacon_proposer_indices*( state: ForkyBeaconState, shuffled_indices: openArray[ValidatorIndex], epoch: Epoch): seq[Opt[ValidatorIndex]] = ## Return the beacon proposer indices at the current epoch, using shuffled ## rather than sorted active validator indices. var buffer {.noinit.}: array[32 + 8, byte] res: seq[Opt[ValidatorIndex]] buffer[0..31] = get_seed(state, epoch, DOMAIN_BEACON_PROPOSER).data let epoch_shuffle_seed = get_seed(state, epoch, DOMAIN_BEACON_ATTESTER) for epoch_slot in epoch.slots(): buffer[32..39] = uint_to_bytes(epoch_slot.asUInt64) res.add ( compute_proposer_index(state, shuffled_indices, eth2digest(buffer)) do: compute_inverted_shuffled_index( shuffled_index, seq_len, epoch_shuffle_seed)) res # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/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/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/blob/v0.4.0/specs/bellatrix/validator.md#liveness-failsafe 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-beta.5/specs/phase0/p2p-interface.md#attestation-subnet-subscription 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 - static(ATTESTATION_SUBNET_PREFIX_BITS.int)), uint64) node_offset = truncate( node_id mod static(EPOCHS_PER_SUBNET_SUBSCRIPTION.u256), 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-beta.4/specs/phase0/p2p-interface.md#attestation-subnet-subscription 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) iterator get_committee_indices*(bits: AttestationCommitteeBits): CommitteeIndex = for index, b in bits: if b: yield CommitteeIndex.init(uint64(index)).valueOr: break # Too many bits! Shouldn't happen func get_committee_index_one*(bits: AttestationCommitteeBits): Opt[CommitteeIndex] = var res = Opt.none(CommitteeIndex) for committee_index in get_committee_indices(bits): if res.isSome(): return Opt.none(CommitteeIndex) res = Opt.some(committee_index) res proc compute_on_chain_aggregate*( network_aggregates: openArray[electra.Attestation]): Opt[electra.Attestation] = # aggregates = sorted(network_aggregates, key=lambda a: get_committee_indices(a.committee_bits)[0]) let aggregates = network_aggregates.sortedByIt(it.committee_bits.get_committee_index_one().expect("just one")) let data = aggregates[0].data var agg: AggregateSignature var committee_bits: AttestationCommitteeBits var totalLen = 0 for i, a in aggregates: totalLen += a.aggregation_bits.len # TODO doesn't work if a committee is skipped var aggregation_bits = ElectraCommitteeValidatorsBits.init(totalLen) var pos = 0 var prev_committee_index: Opt[CommitteeIndex] for i, a in aggregates: let committee_index = ? get_committee_index_one(a.committee_bits) first = pos == 0 when false: if prev_committee_index.isNone: prev_committee_index = Opt.some committee_index elif committee_index.distinctBase <= prev_committee_index.get.distinctBase: continue prev_committee_index = Opt.some committee_index for b in a.aggregation_bits: aggregation_bits[pos] = b pos += 1 let sig = ? a.signature.load() # Expensive if first: agg = AggregateSignature.init(sig) else: agg.aggregate(sig) committee_bits[int(committee_index)] = true let signature = agg.finish() ok electra.Attestation( aggregation_bits: aggregation_bits, data: data, committee_bits: committee_bits, signature: signature.toValidatorSig(), )