detect mismatch of config and binary (#4132)

* detect mismatch of config and binary

When loading configuration that sets keys that Nimbus bakes into the
binary at compile-time, raise an error if the config is incompatible
instead of ignoring the conflicting value.
This commit is contained in:
Etan Kissling 2022-09-19 11:07:46 +02:00 committed by GitHub
parent 4b3768c3a1
commit 9999362b11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 161 additions and 123 deletions

View File

@ -98,7 +98,7 @@ type
indices*: Table[Eth2Digest, Index]
currentEpochTips*: Table[Index, FinalityCheckpoints]
previousProposerBoostRoot*: Eth2Digest
previousProposerBoostScore*: int64
previousProposerBoostScore*: uint64
ProtoNode* = object
root*: Eth2Digest

View File

@ -125,13 +125,11 @@ iterator realizePendingCheckpoints*(
if resetTipTracking:
self.currentEpochTips.clear()
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/fork-choice.md#configuration
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/specs/phase0/fork-choice.md#get_latest_attesting_balance
const PROPOSER_SCORE_BOOST* = 40
func calculateProposerBoost(validatorBalances: openArray[Gwei]): int64 =
func calculateProposerBoost(validatorBalances: openArray[Gwei]): uint64 =
var
total_balance: uint64
num_validators: int64
num_validators: uint64
for balance in validatorBalances:
# We need to filter zero balances here to get an accurate active validator
# count. This is because we default inactive validator balances to zero
@ -142,8 +140,8 @@ func calculateProposerBoost(validatorBalances: openArray[Gwei]): int64 =
if num_validators == 0:
return 0
let
average_balance = int64(total_balance div num_validators.uint64)
committee_size = num_validators div SLOTS_PER_EPOCH.int64
average_balance = total_balance div num_validators.uint64
committee_size = num_validators div SLOTS_PER_EPOCH
committee_weight = committee_size * average_balance
(committee_weight * PROPOSER_SCORE_BOOST) div 100
@ -188,7 +186,7 @@ func applyScoreChanges*(self: var ProtoArray,
self.nodes.buf[nodePhysicalIdx]
# Default value, if not otherwise set in first node loop
var proposerBoostScore: int64
var proposerBoostScore: uint64
# Iterate backwards through all the indices in `self.nodes`
for nodePhysicalIdx in countdown(self.nodes.len - 1, 0):
@ -202,11 +200,11 @@ func applyScoreChanges*(self: var ProtoArray,
if (not self.previousProposerBoostRoot.isZero) and
self.previousProposerBoostRoot == node.root:
if nodeDelta < 0 and
nodeDelta - low(Delta) < self.previousProposerBoostScore:
nodeDelta - low(Delta) < self.previousProposerBoostScore.int64:
return err ForkChoiceError(
kind: fcDeltaUnderflow,
index: nodePhysicalIdx)
nodeDelta -= self.previousProposerBoostScore
nodeDelta -= self.previousProposerBoostScore.int64
# If we find the node matching the current proposer boost root, increase
# the delta by the new score amount.
@ -215,7 +213,7 @@ func applyScoreChanges*(self: var ProtoArray,
if (not proposerBoostRoot.isZero) and proposerBoostRoot == node.root:
proposerBoostScore = calculateProposerBoost(newBalances)
if nodeDelta >= 0 and
high(Delta) - nodeDelta < self.previousProposerBoostScore:
high(Delta) - nodeDelta < proposerBoostScore.int64:
return err ForkChoiceError(
kind: fcDeltaOverflow,
index: nodePhysicalIdx)

View File

@ -1431,7 +1431,7 @@ proc getLowSubnets(node: Eth2Node, epoch: Epoch): (AttnetBits, SyncnetBits) =
notHighOutgoingSubnets
return (
findLowSubnets(getAttestationTopic, SubnetId, ATTESTATION_SUBNET_COUNT),
findLowSubnets(getAttestationTopic, SubnetId, ATTESTATION_SUBNET_COUNT.int),
# We start looking one epoch before the transition in order to allow
# some time for the gossip meshes to get healthy:
if epoch + 1 >= node.cfg.ALTAIR_FORK_EPOCH:

View File

@ -1,3 +1,4 @@
# beacon_chain
# Copyright (c) 2018-2022 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).
@ -8,7 +9,6 @@ import stew/[byteutils, base10], chronicles
import ".."/beacon_node,
".."/spec/forks,
"."/rest_utils
from ../fork_choice/proto_array import PROPOSER_SCORE_BOOST
export rest_utils
@ -178,7 +178,7 @@ proc installConfigApiHandlers*(router: var RestRouter, node: BeaconNode) =
CHURN_LIMIT_QUOTIENT:
Base10.toString(cfg.CHURN_LIMIT_QUOTIENT),
PROPOSER_SCORE_BOOST:
Base10.toString(uint64(PROPOSER_SCORE_BOOST)),
Base10.toString(PROPOSER_SCORE_BOOST),
DEPOSIT_CHAIN_ID:
Base10.toString(cfg.DEPOSIT_CHAIN_ID),
DEPOSIT_NETWORK_ID:
@ -248,7 +248,7 @@ proc installConfigApiHandlers*(router: var RestRouter, node: BeaconNode) =
EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION:
Base10.toString(EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION),
ATTESTATION_SUBNET_COUNT:
Base10.toString(uint64(ATTESTATION_SUBNET_COUNT)),
Base10.toString(ATTESTATION_SUBNET_COUNT),
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/altair/validator.md#constants
TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE:

View File

@ -50,9 +50,6 @@ const
INTERVALS_PER_SLOT* = 3
FAR_FUTURE_BEACON_TIME* = BeaconTime(ns_since_genesis: int64.high())
FAR_FUTURE_SLOT* = Slot(not 0'u64)
# FAR_FUTURE_EPOCH* = Epoch(not 0'u64) # in presets
FAR_FUTURE_PERIOD* = SyncCommitteePeriod(not 0'u64)
NANOSECONDS_PER_SLOT = SECONDS_PER_SLOT * 1_000_000_000'u64

View File

@ -94,9 +94,6 @@ const
DEPOSIT_CONTRACT_TREE_DEPTH* = 32
BASE_REWARDS_PER_EPOCH* = 4
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/validator.md#misc
ATTESTATION_SUBNET_COUNT* = 64
DEPOSIT_CONTRACT_LIMIT* = Limit(1'u64 shl DEPOSIT_CONTRACT_TREE_DEPTH)
template maxSize*(n: int) {.pragma.}
@ -132,10 +129,6 @@ template maxSize*(n: int) {.pragma.}
# - broke the compiler in SSZ and nim-serialization
type
# Domains
# ---------------------------------------------------------------
DomainType* = distinct array[4, byte]
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/beacon-chain.md#custom-types
Eth2Domain* = array[32, byte]
@ -461,7 +454,7 @@ type
attester_slashings*: List[AttesterSlashing, Limit MAX_ATTESTER_SLASHINGS]
voluntary_exits*: List[SignedVoluntaryExit, Limit MAX_VOLUNTARY_EXITS]
AttnetBits* = BitArray[ATTESTATION_SUBNET_COUNT]
AttnetBits* = BitArray[int ATTESTATION_SUBNET_COUNT]
type
# Caches for computing justificiation, rewards and penalties - based on
@ -513,22 +506,6 @@ type
flags*: set[RewardFlags]
const
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/beacon-chain.md#domain-types
DOMAIN_BEACON_PROPOSER* = DomainType([byte 0x00, 0x00, 0x00, 0x00])
DOMAIN_BEACON_ATTESTER* = DomainType([byte 0x01, 0x00, 0x00, 0x00])
DOMAIN_RANDAO* = DomainType([byte 0x02, 0x00, 0x00, 0x00])
DOMAIN_DEPOSIT* = DomainType([byte 0x03, 0x00, 0x00, 0x00])
DOMAIN_VOLUNTARY_EXIT* = DomainType([byte 0x04, 0x00, 0x00, 0x00])
DOMAIN_SELECTION_PROOF* = DomainType([byte 0x05, 0x00, 0x00, 0x00])
DOMAIN_AGGREGATE_AND_PROOF* = DomainType([byte 0x06, 0x00, 0x00, 0x00])
DOMAIN_APPLICATION_MASK* = DomainType([byte 0x00, 0x00, 0x00, 0x01])
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/altair/beacon-chain.md#domain-types
DOMAIN_SYNC_COMMITTEE* = DomainType([byte 0x07, 0x00, 0x00, 0x00])
DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF* = DomainType([byte 0x08, 0x00, 0x00, 0x00])
DOMAIN_CONTRIBUTION_AND_PROOF* = DomainType([byte 0x09, 0x00, 0x00, 0x00])
func getImmutableValidatorData*(validator: Validator): ImmutableValidatorData2 =
let cookedKey = validator.pubkey.loadValid() # `Validator` has valid key
ImmutableValidatorData2(
@ -902,7 +879,7 @@ static:
# Sanity checks - these types should be trivial enough to copy with memcpy
doAssert supportsCopyMem(Validator)
doAssert supportsCopyMem(Eth2Digest)
doAssert ATTESTATION_SUBNET_COUNT <= high(distinctBase SubnetId).int
doAssert ATTESTATION_SUBNET_COUNT <= high(distinctBase SubnetId)
func getSizeofSig(x: auto, n: int = 0): seq[(string, int, int)] =
for name, value in x.fieldPairs:

View File

@ -28,9 +28,6 @@ import
export json_serialization, base
const
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.1/specs/bellatrix/beacon-chain.md#transition-settings
TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH* = FAR_FUTURE_EPOCH
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.1/src/engine/specification.md#request-1
FORKCHOICEUPDATED_TIMEOUT* = 8.seconds

View File

@ -0,0 +1,43 @@
# beacon_chain
# Copyright (c) 2022 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.
type
Slot* = distinct uint64
Epoch* = distinct uint64
SyncCommitteePeriod* = distinct uint64
DomainType* = distinct array[4, byte]
const
# 2^64 - 1 in spec
FAR_FUTURE_SLOT* = Slot(not 0'u64)
FAR_FUTURE_EPOCH* = Epoch(not 0'u64)
FAR_FUTURE_PERIOD* = SyncCommitteePeriod(not 0'u64)
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/beacon-chain.md#domain-types
DOMAIN_BEACON_PROPOSER* = DomainType([byte 0x00, 0x00, 0x00, 0x00])
DOMAIN_BEACON_ATTESTER* = DomainType([byte 0x01, 0x00, 0x00, 0x00])
DOMAIN_RANDAO* = DomainType([byte 0x02, 0x00, 0x00, 0x00])
DOMAIN_DEPOSIT* = DomainType([byte 0x03, 0x00, 0x00, 0x00])
DOMAIN_VOLUNTARY_EXIT* = DomainType([byte 0x04, 0x00, 0x00, 0x00])
DOMAIN_SELECTION_PROOF* = DomainType([byte 0x05, 0x00, 0x00, 0x00])
DOMAIN_AGGREGATE_AND_PROOF* = DomainType([byte 0x06, 0x00, 0x00, 0x00])
DOMAIN_APPLICATION_MASK* = DomainType([byte 0x00, 0x00, 0x00, 0x01])
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/altair/beacon-chain.md#domain-types
DOMAIN_SYNC_COMMITTEE* = DomainType([byte 0x07, 0x00, 0x00, 0x00])
DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF* = DomainType([byte 0x08, 0x00, 0x00, 0x00])
DOMAIN_CONTRIBUTION_AND_PROOF* = DomainType([byte 0x09, 0x00, 0x00, 0x00])
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/bellatrix/beacon-chain.md#transition-settings
TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH* = FAR_FUTURE_EPOCH
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/fork-choice.md#configuration
PROPOSER_SCORE_BOOST*: uint64 = 40
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/validator.md#misc
ATTESTATION_SUBNET_COUNT*: uint64 = 64

View File

@ -11,8 +11,11 @@ else:
{.push raises: [].}
import
std/[macros, strutils, parseutils, tables],
stew/[byteutils], stint, web3/[ethtypes]
std/[strutils, parseutils, tables, typetraits],
stew/[byteutils], stint, web3/[ethtypes],
./datatypes/constants
export constants
export stint, ethtypes.toHex
@ -27,9 +30,6 @@ const
EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION*: uint64 = 256
type
Slot* = distinct uint64
Epoch* = distinct uint64
SyncCommitteePeriod* = distinct uint64
Version* = distinct array[4, byte]
Eth1Address* = ethtypes.Address
@ -88,75 +88,12 @@ type
PresetIncompatibleError* = object of CatchableError
const
FAR_FUTURE_EPOCH* = (not 0'u64).Epoch # 2^64 - 1 in spec
const_preset* {.strdefine.} = "mainnet"
# These constants cannot really be overriden in a preset.
# If we encounter them, we'll just ignore the preset value.
# TODO verify the value against the constant instead
ignoredValues* = [
# TODO Once implemented as part of the RuntimeConfig,
# this should be removed from here
"SECONDS_PER_SLOT",
"BLS_WITHDRAWAL_PREFIX",
"MAX_COMMITTEES_PER_SLOT",
"TARGET_COMMITTEE_SIZE",
"MAX_VALIDATORS_PER_COMMITTEE",
"SHUFFLE_ROUND_COUNT",
"HYSTERESIS_QUOTIENT",
"HYSTERESIS_DOWNWARD_MULTIPLIER",
"HYSTERESIS_UPWARD_MULTIPLIER",
"SAFE_SLOTS_TO_UPDATE_JUSTIFIED",
"MIN_DEPOSIT_AMOUNT",
"MAX_EFFECTIVE_BALANCE",
"EFFECTIVE_BALANCE_INCREMENT",
"MIN_ATTESTATION_INCLUSION_DELAY",
"SLOTS_PER_EPOCH",
"MIN_SEED_LOOKAHEAD",
"MAX_SEED_LOOKAHEAD",
"EPOCHS_PER_ETH1_VOTING_PERIOD",
"SLOTS_PER_HISTORICAL_ROOT",
"MIN_EPOCHS_TO_INACTIVITY_PENALTY",
"EPOCHS_PER_HISTORICAL_VECTOR",
"EPOCHS_PER_SLASHINGS_VECTOR",
"HISTORICAL_ROOTS_LIMIT",
"VALIDATOR_REGISTRY_LIMIT",
"BASE_REWARD_FACTOR",
"WHISTLEBLOWER_REWARD_QUOTIENT",
"PROPOSER_REWARD_QUOTIENT",
"INACTIVITY_PENALTY_QUOTIENT",
"MIN_SLASHING_PENALTY_QUOTIENT",
"PROPORTIONAL_SLASHING_MULTIPLIER",
"MAX_PROPOSER_SLASHINGS",
"MAX_ATTESTER_SLASHINGS",
"MAX_ATTESTATIONS",
"MAX_DEPOSITS",
"MAX_VOLUNTARY_EXITS",
"TARGET_AGGREGATORS_PER_COMMITTEE",
"RANDOM_SUBNETS_PER_VALIDATOR",
"EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION",
"ATTESTATION_SUBNET_COUNT",
"DOMAIN_BEACON_PROPOSER",
"DOMAIN_BEACON_ATTESTER",
"DOMAIN_RANDAO",
"DOMAIN_DEPOSIT",
"DOMAIN_VOLUNTARY_EXIT",
"DOMAIN_SELECTION_PROOF",
"DOMAIN_AGGREGATE_AND_PROOF",
"DOMAIN_SYNC_COMMITTEE",
"DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF",
"DOMAIN_CONTRIBUTION_AND_PROOF",
# No-longer used values from legacy config files
ignoredValues = [
"TRANSITION_TOTAL_DIFFICULTY", # Name that appears in some altair alphas, obsolete, remove when no more testnets
"MIN_ANCHOR_POW_BLOCK_DIFFICULTY", # Name that appears in some altair alphas, obsolete, remove when no more testnets
"TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH", # Never pervasively implemented, still under discussion
"PROPOSER_SCORE_BOOST", # Isn't being used as a preset in the usual way: at any time, there's one correct value
]
when const_preset == "mainnet":
@ -429,13 +366,16 @@ template parse(T: type BlockHash, input: string): T =
template parse(T: type UInt256, input: string): T =
parse(input, UInt256, 10)
func parse(T: type DomainType, input: string): T
{.raises: [ValueError, Defect].} =
DomainType hexToByteArray(input, 4)
proc readRuntimeConfig*(
path: string): (RuntimeConfig, seq[string]) {.
raises: [IOError, PresetFileError, PresetIncompatibleError, Defect].} =
var
lineNum = 0
cfg = defaultRuntimeConfig
unknowns: seq[string]
template lineinfo: string =
try: "$1($2) " % [path, $lineNum]
@ -460,15 +400,97 @@ proc readRuntimeConfig*(
if lineParts[0] in ignoredValues: continue
if lineParts[0] notin names:
unknowns.add(lineParts[0])
values[lineParts[0]] = lineParts[1].strip
# Certain config keys are baked into the binary at compile-time
# and cannot be overridden via config.
template checkCompatibility(constValue: untyped): untyped =
block:
const name = astToStr(constValue)
if values.hasKey(name):
try:
let value = parse(typeof(constValue), values[name])
when constValue is distinct:
if distinctBase(value) != distinctBase(constValue):
raise (ref PresetFileError)(msg:
"Cannot override config" &
" (compiled: " & name & "=" & $distinctBase(constValue) &
" - config: " & name & "=" & values[name] & ")")
else:
if value != constValue:
raise (ref PresetFileError)(msg:
"Cannot override config" &
" (compiled: " & name & "=" & $constValue &
" - config: " & name & "=" & values[name] & ")")
values.del name
except ValueError:
raise (ref PresetFileError)(msg: "Unable to parse " & name)
checkCompatibility SECONDS_PER_SLOT
checkCompatibility BLS_WITHDRAWAL_PREFIX
checkCompatibility MAX_COMMITTEES_PER_SLOT
checkCompatibility TARGET_COMMITTEE_SIZE
checkCompatibility MAX_VALIDATORS_PER_COMMITTEE
checkCompatibility SHUFFLE_ROUND_COUNT
checkCompatibility HYSTERESIS_QUOTIENT
checkCompatibility HYSTERESIS_DOWNWARD_MULTIPLIER
checkCompatibility HYSTERESIS_UPWARD_MULTIPLIER
checkCompatibility SAFE_SLOTS_TO_UPDATE_JUSTIFIED
checkCompatibility MIN_DEPOSIT_AMOUNT
checkCompatibility MAX_EFFECTIVE_BALANCE
checkCompatibility EFFECTIVE_BALANCE_INCREMENT
checkCompatibility MIN_ATTESTATION_INCLUSION_DELAY
checkCompatibility SLOTS_PER_EPOCH
checkCompatibility MIN_SEED_LOOKAHEAD
checkCompatibility MAX_SEED_LOOKAHEAD
checkCompatibility EPOCHS_PER_ETH1_VOTING_PERIOD
checkCompatibility SLOTS_PER_HISTORICAL_ROOT
checkCompatibility MIN_EPOCHS_TO_INACTIVITY_PENALTY
checkCompatibility EPOCHS_PER_HISTORICAL_VECTOR
checkCompatibility EPOCHS_PER_SLASHINGS_VECTOR
checkCompatibility HISTORICAL_ROOTS_LIMIT
checkCompatibility VALIDATOR_REGISTRY_LIMIT
checkCompatibility BASE_REWARD_FACTOR
checkCompatibility WHISTLEBLOWER_REWARD_QUOTIENT
checkCompatibility PROPOSER_REWARD_QUOTIENT
checkCompatibility INACTIVITY_PENALTY_QUOTIENT
checkCompatibility MIN_SLASHING_PENALTY_QUOTIENT
checkCompatibility PROPORTIONAL_SLASHING_MULTIPLIER
checkCompatibility MAX_PROPOSER_SLASHINGS
checkCompatibility MAX_ATTESTER_SLASHINGS
checkCompatibility MAX_ATTESTATIONS
checkCompatibility MAX_DEPOSITS
checkCompatibility MAX_VOLUNTARY_EXITS
checkCompatibility TARGET_AGGREGATORS_PER_COMMITTEE
checkCompatibility RANDOM_SUBNETS_PER_VALIDATOR
checkCompatibility EPOCHS_PER_RANDOM_SUBNET_SUBSCRIPTION
checkCompatibility ATTESTATION_SUBNET_COUNT
checkCompatibility DOMAIN_BEACON_PROPOSER
checkCompatibility DOMAIN_BEACON_ATTESTER
checkCompatibility DOMAIN_RANDAO
checkCompatibility DOMAIN_DEPOSIT
checkCompatibility DOMAIN_VOLUNTARY_EXIT
checkCompatibility DOMAIN_SELECTION_PROOF
checkCompatibility DOMAIN_AGGREGATE_AND_PROOF
checkCompatibility DOMAIN_SYNC_COMMITTEE
checkCompatibility DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF
checkCompatibility DOMAIN_CONTRIBUTION_AND_PROOF
# Never pervasively implemented, still under discussion
checkCompatibility TERMINAL_BLOCK_HASH_ACTIVATION_EPOCH
# Isn't being used as a preset in the usual way: at any time, there's one correct value
checkCompatibility PROPOSER_SCORE_BOOST
for name, field in cfg.fieldPairs():
if name in values:
try:
field = parse(typeof(field), values[name])
values.del name
except ValueError:
raise (ref PresetFileError)(msg: "Unable to parse " & name)
@ -476,6 +498,10 @@ proc readRuntimeConfig*(
raise (ref PresetIncompatibleError)(
msg: "Config not compatible with binary, compile with -d:const_preset=" & cfg.PRESET_BASE)
var unknowns: seq[string]
for name in values.keys:
unknowns.add name
(cfg, unknowns)
template name*(cfg: RuntimeConfig): string =

View File

@ -149,7 +149,7 @@ proc updateSlot*(tracker: var ActionTracker, wallSlot: Slot) =
# https://github.com/ethereum/consensus-specs/blob/v1.2.0-rc.3/specs/phase0/validator.md#phase-0-attestation-subnet-stability
let expectedSubnets =
min(ATTESTATION_SUBNET_COUNT, tracker.knownValidators.len)
min(ATTESTATION_SUBNET_COUNT.int, tracker.knownValidators.len)
let epoch = wallSlot.epoch
block:

View File

@ -71,10 +71,10 @@ procSuite "Eth2 specific discovery tests":
await node2.closeWait()
asyncTest "Invalid attnets field":
var invalidAttnets: BitArray[ATTESTATION_SUBNET_COUNT div 2]
var invalidAttnets: BitArray[ATTESTATION_SUBNET_COUNT.int div 2]
invalidAttnets.setBit(15)
# TODO: This doesn't fail actually.
# var invalidAttnets2: BitArray[ATTESTATION_SUBNET_COUNT * 2]
# var invalidAttnets2: BitArray[ATTESTATION_SUBNET_COUNT.int * 2]
# invalidAttnets2.setBit(15)
var attnets: AttnetBits
attnets.setBit(15)