diff --git a/CHANGELOG.md b/CHANGELOG.md index daedda885..f78dc8b1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +2023-02-27 v24.2.2 +================== + +Nimbus `v24.2.2` is a hotfix release addressing a consensus violation issue affecting Deneb-transitioned network such as Holešky. Please upgrade as soon as possible if your node is affected. + +### Improvements + +* Added metrics `validator_monitor_block_hit` and `validator_monitor_block_miss` tracking the number of successful and missed block proposals: + https://github.com/status-im/nimbus-eth2/pull/5913 + +### Fixes + +* Nimbus had an incomplete implementation of EIP-7044 (Perpetually Valid Signed Voluntary Exits): + https://github.com/status-im/nimbus-eth2/pull/5953 + https://github.com/status-im/nimbus-eth2/pull/5954 + https://github.com/status-im/nimbus-eth2/pull/5959 + https://github.com/status-im/nimbus-eth2/pull/5966 + +* The Nimbus `v24.2.1` validator client was crashing with a `RangeDefect` error message during block proposal when paired with a `v24.1.x` beacon node: + https://github.com/status-im/nim-stint/pull/148 + + 2023-02-20 v24.2.1 ================== diff --git a/beacon_chain/consensus_object_pools/block_clearance.nim b/beacon_chain/consensus_object_pools/block_clearance.nim index c3c778f27..4b2a5e37f 100644 --- a/beacon_chain/consensus_object_pools/block_clearance.nim +++ b/beacon_chain/consensus_object_pools/block_clearance.nim @@ -285,7 +285,7 @@ proc addHeadBlockWithParent*( var sigs: seq[SignatureSet] if (let e = sigs.collectSignatureSets( signedBlock, dag.db.immutableValidators, - dag.clearanceState, dag.cfg.genesisFork(), dag.cfg.capellaFork(), + dag.clearanceState, dag.cfg.genesisFork(), dag.cfg.CAPELLA_FORK_VERSION, cache); e.isErr()): # A PublicKey or Signature isn't on the BLS12-381 curve info "Unable to load signature sets", diff --git a/beacon_chain/consensus_object_pools/blockchain_dag.nim b/beacon_chain/consensus_object_pools/blockchain_dag.nim index 9c437edd7..b3d044b9a 100644 --- a/beacon_chain/consensus_object_pools/blockchain_dag.nim +++ b/beacon_chain/consensus_object_pools/blockchain_dag.nim @@ -956,9 +956,14 @@ proc advanceSlots*( # which is an acceptable tradeoff for monitoring. withState(state): let postEpoch = forkyState.data.slot.epoch - if preEpoch != postEpoch: + if preEpoch != postEpoch and postEpoch >= 2: + var proposers: array[SLOTS_PER_EPOCH, Opt[ValidatorIndex]] + let epochRef = dag.findEpochRef(stateBid, postEpoch - 2) + if epochRef.isSome(): + proposers = epochRef[][].beacon_proposers + dag.validatorMonitor[].registerEpochInfo( - postEpoch, info, forkyState.data) + forkyState.data, proposers, info) proc applyBlock( dag: ChainDAGRef, state: var ForkedHashedBeaconState, bid: BlockId, diff --git a/beacon_chain/deposits.nim b/beacon_chain/deposits.nim index 9dae6bcb7..2af065cbb 100644 --- a/beacon_chain/deposits.nim +++ b/beacon_chain/deposits.nim @@ -220,33 +220,19 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} = reason = exc.msg quit 1 + # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#voluntary-exits + # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/deneb/beacon-chain.md#modified-process_voluntary_exit let signingFork = try: let response = await client.getSpecVC() if response.status == 200: - let - spec = response.data.data - denebForkEpoch = - block: - let s = spec.getOrDefault("DENEB_FORK_EPOCH", $FAR_FUTURE_EPOCH) - Epoch(Base10.decode(uint64, s).get(uint64(FAR_FUTURE_EPOCH))) - # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/beacon-chain.md#voluntary-exits - # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.0/specs/deneb/beacon-chain.md#modified-process_voluntary_exit - if currentEpoch >= denebForkEpoch: - let capellaForkVersion = - block: - var res: Version - # CAPELLA_FOR_VERSION has specific format - "0x01000000", so - # default empty string is invalid, so `hexToByteArrayStrict` - # will raise exception on empty string. - let s = spec.getOrDefault("CAPELLA_FORK_VERSION", "") - hexToByteArrayStrict(s, distinctBase(res)) - res - Fork( - current_version: capellaForkVersion, - previous_version: capellaForkVersion, - epoch: GENESIS_EPOCH) # irrelevant when current/previous identical - else: - fork + let forkConfig = response.data.data.getConsensusForkConfig() + if forkConfig.isErr: + raise newException(RestError, "Invalid config: " & forkConfig.error) + let capellaForkVersion = forkConfig.get.capellaVersion.valueOr: + raise newException(RestError, + ConsensusFork.Capella.forkVersionConfigKey() & " missing") + voluntary_exit_signature_fork( + fork, capellaForkVersion, currentEpoch, forkConfig.get.denebEpoch) else: raise newException(RestError, "Error response (" & $response.status & ")") except CatchableError as exc: @@ -254,7 +240,7 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} = reason = exc.msg quit 1 - debug "Signing fork obtained", fork = fork + debug "Signing fork obtained", fork, signingFork if not config.printData: case askForExitConfirmation() @@ -292,7 +278,7 @@ proc restValidatorExit(config: BeaconNodeConf) {.async.} = validatorKeyAsStr, exitAtEpoch, validatorIdx, - fork, + signingFork, genesis_validators_root) if config.printData: diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index cefefab7f..8ba704d3c 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -755,6 +755,12 @@ proc init*(T: type BeaconNode, withState(dag.headState): getValidator(forkyState().data.validators.asSeq(), pubkey) + func getCapellaForkVersion(): Opt[Version] = + Opt.some(cfg.CAPELLA_FORK_VERSION) + + func getDenebForkEpoch(): Opt[Epoch] = + Opt.some(cfg.DENEB_FORK_EPOCH) + proc getForkForEpoch(epoch: Epoch): Opt[Fork] = Opt.some(dag.forkAtEpoch(epoch)) @@ -784,6 +790,8 @@ proc init*(T: type BeaconNode, config.getPayloadBuilderAddress, getValidatorAndIdx, getBeaconTime, + getCapellaForkVersion, + getDenebForkEpoch, getForkForEpoch, getGenesisRoot) else: nil diff --git a/beacon_chain/nimbus_validator_client.nim b/beacon_chain/nimbus_validator_client.nim index 5899ef6fb..e4bdfd0a7 100644 --- a/beacon_chain/nimbus_validator_client.nim +++ b/beacon_chain/nimbus_validator_client.nim @@ -4,6 +4,9 @@ # * 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: [].} + import stew/io2, presto, metrics, metrics/chronos_httpserver, ./rpc/rest_key_management_api, @@ -348,6 +351,18 @@ proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} = let keymanagerInitResult = initKeymanagerServer(vc.config, nil) + func getCapellaForkVersion(): Opt[Version] = + if vc.runtimeConfig.forkConfig.isSome(): + vc.runtimeConfig.forkConfig.get().capellaVersion + else: + Opt.none(Version) + + func getDenebForkEpoch(): Opt[Epoch] = + if vc.runtimeConfig.forkConfig.isSome(): + Opt.some(vc.runtimeConfig.forkConfig.get().denebEpoch) + else: + Opt.none(Epoch) + proc getForkForEpoch(epoch: Epoch): Opt[Fork] = if len(vc.forks) > 0: Opt.some(vc.forkAtEpoch(epoch)) @@ -379,6 +394,8 @@ proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} = Opt.none(string), nil, vc.beaconClock.getBeaconTimeFn, + getCapellaForkVersion, + getDenebForkEpoch, getForkForEpoch, getGenesisRoot ) diff --git a/beacon_chain/rpc/rest_constants.nim b/beacon_chain/rpc/rest_constants.nim index 58e327ed3..33372581f 100644 --- a/beacon_chain/rpc/rest_constants.nim +++ b/beacon_chain/rpc/rest_constants.nim @@ -241,6 +241,10 @@ const "The given Merkle proof is invalid" InvalidMerkleProofIndexError* = "The given Merkle proof index is invalid" + FailedToObtainForkVersionError* = + "Failed to obtain fork version" + FailedToObtainConsensusForkError* = + "Failed to obtain consensus fork information" FailedToObtainForkError* = "Failed to obtain fork information" InvalidTimestampValue* = diff --git a/beacon_chain/rpc/rest_key_management_api.nim b/beacon_chain/rpc/rest_key_management_api.nim index 6d697168c..84a8f14bf 100644 --- a/beacon_chain/rpc/rest_key_management_api.nim +++ b/beacon_chain/rpc/rest_key_management_api.nim @@ -1,9 +1,12 @@ +# beacon_chain # Copyright (c) 2021-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: [].} + # NOTE: This module has been used in both `beacon_node` and `validator_client`, # please keep imports clear of `rest_utils` or any other module which imports # beacon node's specific networking code. @@ -561,6 +564,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) = let qpubkey = pubkey.valueOr: return keymanagerApiError(Http400, InvalidValidatorPublicKey) + currentEpoch = host.getBeaconTimeFn().slotOrZero().epoch() qepoch = if epoch.isSome(): let res = epoch.get() @@ -568,7 +572,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) = return keymanagerApiError(Http400, InvalidEpochValueError) res.get() else: - host.getBeaconTimeFn().slotOrZero().epoch() + currentEpoch validator = block: let res = host.validatorPool[].getValidator(qpubkey).valueOr: @@ -581,10 +585,16 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) = validator_index: uint64(validator.index.get())) fork = host.getForkFn(qepoch).valueOr: return keymanagerApiError(Http500, FailedToObtainForkError) + capellaForkVersion = host.getCapellaForkVersionFn().valueOr: + return keymanagerApiError(Http500, FailedToObtainForkVersionError) + denebForkEpoch = host.getDenebForkEpochFn().valueOr: + return keymanagerApiError(Http500, FailedToObtainConsensusForkError) + signingFork = voluntary_exit_signature_fork( + fork, capellaForkVersion, currentEpoch, denebForkEpoch) signature = try: let res = await validator.getValidatorExitSignature( - fork, host.getGenesisFn(), voluntaryExit) + signingFork, host.getGenesisFn(), voluntaryExit) if res.isErr(): return keymanagerApiError(Http500, res.error()) res.get() diff --git a/beacon_chain/spec/eth2_apis/rest_beacon_client.nim b/beacon_chain/spec/eth2_apis/rest_beacon_client.nim index f6b3bc24b..66c0ac341 100644 --- a/beacon_chain/spec/eth2_apis/rest_beacon_client.nim +++ b/beacon_chain/spec/eth2_apis/rest_beacon_client.nim @@ -13,7 +13,8 @@ import rest_beacon_calls, rest_builder_calls, rest_config_calls, rest_debug_calls, rest_keymanager_calls, rest_light_client_calls, rest_node_calls, rest_validator_calls, - rest_nimbus_calls, rest_event_calls, rest_common + rest_nimbus_calls, rest_event_calls, rest_common, + rest_fork_config ] export @@ -21,4 +22,5 @@ export rest_beacon_calls, rest_builder_calls, rest_config_calls, rest_debug_calls, rest_keymanager_calls, rest_light_client_calls, rest_node_calls, rest_validator_calls, - rest_nimbus_calls, rest_event_calls, rest_common + rest_nimbus_calls, rest_event_calls, rest_common, + rest_fork_config diff --git a/beacon_chain/spec/eth2_apis/rest_fork_config.nim b/beacon_chain/spec/eth2_apis/rest_fork_config.nim new file mode 100644 index 000000000..f263cf259 --- /dev/null +++ b/beacon_chain/spec/eth2_apis/rest_fork_config.nim @@ -0,0 +1,97 @@ +# beacon_chain +# 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: [].} + +import + std/strutils, + stew/[base10, byteutils], + ../forks + +from ./rest_types import VCRuntimeConfig + +export forks, rest_types + +type VCForkConfig* = object + altairEpoch*: Epoch + capellaVersion*: Opt[Version] + capellaEpoch*: Epoch + denebEpoch*: Epoch + +func forkVersionConfigKey*(consensusFork: ConsensusFork): string = + if consensusFork > ConsensusFork.Phase0: + ($consensusFork).toUpperAscii() & "_FORK_VERSION" + else: + "GENESIS_FORK_VERSION" + +func forkEpochConfigKey*(consensusFork: ConsensusFork): string = + doAssert consensusFork > ConsensusFork.Phase0 + ($consensusFork).toUpperAscii() & "_FORK_EPOCH" + +proc getOrDefault*(info: VCRuntimeConfig, name: string, + default: uint64): uint64 = + let numstr = info.getOrDefault(name, "missing") + if numstr == "missing": return default + Base10.decode(uint64, numstr).valueOr: + return default + +proc getOrDefault*(info: VCRuntimeConfig, name: string, default: Epoch): Epoch = + Epoch(info.getOrDefault(name, uint64(default))) + +func getForkVersion( + info: VCRuntimeConfig, + consensusFork: Consensusfork): Result[Opt[Version], string] = + let key = consensusFork.forkVersionConfigKey() + let stringValue = info.getOrDefault(key, "missing") + if stringValue == "missing": return ok Opt.none(Version) + var value: Version + try: + hexToByteArrayStrict(stringValue, distinctBase(value)) + except ValueError as exc: + return err(key & " is invalid: " & exc.msg) + ok Opt.some value + +func getForkEpoch(info: VCRuntimeConfig, consensusFork: ConsensusFork): Epoch = + if consensusFork > ConsensusFork.Phase0: + let key = consensusFork.forkEpochConfigKey() + info.getOrDefault(key, FAR_FUTURE_EPOCH) + else: + GENESIS_EPOCH + +func getConsensusForkConfig*( + info: VCRuntimeConfig): Result[VCForkConfig, string] = + ## This extracts all `_FORK_VERSION` and `_FORK_EPOCH` constants + ## that are relevant for Validator Client operation. + ## + ## Note that the fork schedule (`/eth/v1/config/fork_schedule`) cannot be used + ## because it does not indicate whether the forks refer to `ConsensusFork` or + ## to a different fork sequence from an incompatible network (e.g., devnet) + let + res = VCForkConfig( + altairEpoch: info.getForkEpoch(ConsensusFork.Altair), + capellaVersion: ? info.getForkVersion(ConsensusFork.Capella), + capellaEpoch: info.getForkEpoch(ConsensusFork.Capella), + denebEpoch: info.getForkEpoch(ConsensusFork.Deneb)) + + if res.capellaEpoch < res.altairEpoch: + return err( + "Fork epochs are inconsistent, " & $ConsensusFork.Capella & + " is scheduled at epoch " & $res.capellaEpoch & + " which is before prior fork epoch " & $res.altairEpoch) + if res.denebEpoch < res.capellaEpoch: + return err( + "Fork epochs are inconsistent, " & $ConsensusFork.Deneb & + " is scheduled at epoch " & $res.denebEpoch & + " which is before prior fork epoch " & $res.capellaEpoch) + + if res.capellaEpoch != FAR_FUTURE_EPOCH and res.capellaVersion.isNone: + return err( + "Beacon node has scheduled " & + ConsensusFork.Capella.forkEpochConfigKey() & + " but does not report " & + ConsensusFork.Capella.forkVersionConfigKey()) + ok res diff --git a/beacon_chain/spec/signatures.nim b/beacon_chain/spec/signatures.nim index 8c185e1f5..67f57946f 100644 --- a/beacon_chain/spec/signatures.nim +++ b/beacon_chain/spec/signatures.nim @@ -216,6 +216,41 @@ func compute_voluntary_exit_signing_root*( fork, DOMAIN_VOLUNTARY_EXIT, epoch, genesis_validators_root) compute_signing_root(voluntary_exit, domain) +func voluntary_exit_signature_fork( + is_post_deneb: static bool, + state_fork: Fork, + capella_fork_version: Version): Fork = + when is_post_deneb: + # Always use Capella fork version, disregarding `VoluntaryExit` epoch + # [Modified in Deneb:EIP7044] + Fork( + previous_version: capella_fork_version, + current_version: capella_fork_version, + epoch: GENESIS_EPOCH) # irrelevant when current/previous identical + else: + state_fork + +func voluntary_exit_signature_fork*( + consensusFork: static ConsensusFork, + state_fork: Fork, + capella_fork_version: Version): Fork = + const is_post_deneb = (consensusFork >= ConsensusFork.Deneb) + voluntary_exit_signature_fork(is_post_deneb, state_fork, capella_fork_version) + +func voluntary_exit_signature_fork*( + state_fork: Fork, + capella_fork_version: Version, + current_epoch: Epoch, + deneb_fork_epoch: Epoch): Fork = + if current_epoch >= deneb_fork_epoch: + const is_post_deneb = true + voluntary_exit_signature_fork( + is_post_deneb, state_fork, capella_fork_version) + else: + const is_post_deneb = false + voluntary_exit_signature_fork( + is_post_deneb, state_fork, capella_fork_version) + func get_voluntary_exit_signature*( fork: Fork, genesis_validators_root: Eth2Digest, voluntary_exit: VoluntaryExit, diff --git a/beacon_chain/spec/signatures_batch.nim b/beacon_chain/spec/signatures_batch.nim index 3ad13d630..128c69ec4 100644 --- a/beacon_chain/spec/signatures_batch.nim +++ b/beacon_chain/spec/signatures_batch.nim @@ -235,7 +235,7 @@ proc collectSignatureSets*( validatorKeys: openArray[ImmutableValidatorData2], state: ForkedHashedBeaconState, genesis_fork: Fork, - capella_fork: Fork, + capella_fork_version: Version, cache: var StateCache): Result[void, cstring] = ## Collect all signature verifications that process_block would normally do ## except deposits, in one go. @@ -385,25 +385,24 @@ proc collectSignatureSets*( # SSZ deserialization guarantees that blocks received from random sources # including peer or RPC # have at most MAX_VOLUNTARY_EXITS voluntary exits. - for i in 0 ..< signed_block.message.body.voluntary_exits.len: - # don't use "items" for iterating over large type - # due to https://github.com/nim-lang/Nim/issues/14421 - # fixed in 1.4.2 - template volex: untyped = signed_block.message.body.voluntary_exits[i] - let key = validatorKeys.load(volex.message.validator_index).valueOr: - return err("collectSignatureSets: invalid voluntary exit") + if signed_block.message.body.voluntary_exits.len > 0: + let voluntary_exit_fork = withConsensusFork(state.kind): + consensusFork.voluntary_exit_signature_fork(fork, capella_fork_version) + for i in 0 ..< signed_block.message.body.voluntary_exits.len: + # don't use "items" for iterating over large type + # due to https://github.com/nim-lang/Nim/issues/14421 + # fixed in 1.4.2 + template volex: untyped = signed_block.message.body.voluntary_exits[i] + let key = validatorKeys.load(volex.message.validator_index).valueOr: + return err("collectSignatureSets: invalid voluntary exit") - sigs.add voluntary_exit_signature_set( - # https://eips.ethereum.org/EIPS/eip-7044 - # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/deneb/beacon-chain.md#modified-process_voluntary_exit - (if state.kind >= ConsensusFork.Capella: - capella_fork - else: - fork), - genesis_validators_root, volex.message, key, - volex.signature.load.valueOr do: - return err( - "collectSignatureSets: cannot load voluntary exit signature")) + sigs.add voluntary_exit_signature_set( + # https://eips.ethereum.org/EIPS/eip-7044 + # https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.7/specs/deneb/beacon-chain.md#modified-process_voluntary_exit + voluntary_exit_fork, genesis_validators_root, volex.message, key, + volex.signature.load.valueOr do: + return err( + "collectSignatureSets: cannot load voluntary exit signature")) block: when signed_block is phase0.SignedBeaconBlock: diff --git a/beacon_chain/spec/state_transition_block.nim b/beacon_chain/spec/state_transition_block.nim index 5affbf686..03cca1154 100644 --- a/beacon_chain/spec/state_transition_block.nim +++ b/beacon_chain/spec/state_transition_block.nim @@ -269,9 +269,23 @@ func findValidatorIndex*(state: ForkyBeaconState, pubkey: ValidatorPubKey): # given that each block can hold no more than 16 deposits, it's slower to # build the table and use it for lookups than to scan it like this. # Once we have a reusable, long-lived cache, this should be revisited - for vidx in state.validators.vindices: - if state.validators.asSeq[vidx].pubkey == pubkey: - return Opt[ValidatorIndex].ok(vidx) + # + # For deposit processing purposes, two broad cases exist, either + # + # (a) someone has deposited all 32 required ETH as a single transaction, + # in which case the index doesn't yet exist so the search order does + # not matter so long as it's generally in an order memory controller + # prefetching can predict; or + # + # (b) the deposit has been split into multiple parts, typically not far + # apart from each other, such that on average one would expect this + # validator index to be nearer the maximal than minimal index. + # + # countdown() infinite-loops if the lower bound with uint32 is 0, so + # shift indices by 1, which avoids triggering unsigned wraparound. + for vidx in countdown(state.validators.len.uint32, 1): + if state.validators.asSeq[vidx - 1].pubkey == pubkey: + return Opt[ValidatorIndex].ok((vidx - 1).ValidatorIndex) proc process_deposit*(cfg: RuntimeConfig, state: var ForkyBeaconState, @@ -365,16 +379,11 @@ proc check_voluntary_exit*( # Verify signature if skipBlsValidation notin flags: - let exitSignatureFork = - when typeof(state).kind >= ConsensusFork.Deneb: - Fork( - previous_version: cfg.CAPELLA_FORK_VERSION, - current_version: cfg.CAPELLA_FORK_VERSION, - epoch: cfg.CAPELLA_FORK_EPOCH) - else: - state.fork + const consensusFork = typeof(state).kind + let voluntary_exit_fork = consensusFork.voluntary_exit_signature_fork( + state.fork, cfg.CAPELLA_FORK_VERSION) if not verify_voluntary_exit_signature( - exitSignatureFork, state.genesis_validators_root, voluntary_exit, + voluntary_exit_fork, state.genesis_validators_root, voluntary_exit, validator[].pubkey, signed_voluntary_exit.signature): return err("Exit: invalid signature") diff --git a/beacon_chain/validator_client/common.nim b/beacon_chain/validator_client/common.nim index aa610ae58..3fbb396e6 100644 --- a/beacon_chain/validator_client/common.nim +++ b/beacon_chain/validator_client/common.nim @@ -192,7 +192,7 @@ type waiters*: seq[BlockWaiter] ValidatorRuntimeConfig* = object - altairEpoch*: Opt[Epoch] + forkConfig*: Opt[VCForkConfig] ValidatorClient* = object config*: ValidatorClientConf @@ -518,16 +518,6 @@ proc equals*(info: VCRuntimeConfig, name: string, check: DomainType): bool = proc equals*(info: VCRuntimeConfig, name: string, check: Epoch): bool = info.equals(name, uint64(check)) -proc getOrDefault*(info: VCRuntimeConfig, name: string, - default: uint64): uint64 = - let numstr = info.getOrDefault(name, "missing") - if numstr == "missing": return default - Base10.decode(uint64, numstr).valueOr: - return default - -proc getOrDefault*(info: VCRuntimeConfig, name: string, default: Epoch): Epoch = - Epoch(info.getOrDefault(name, uint64(default))) - proc checkConfig*(c: VCRuntimeConfig): bool = c.equals("MAX_VALIDATORS_PER_COMMITTEE", MAX_VALIDATORS_PER_COMMITTEE) and c.equals("SLOTS_PER_EPOCH", SLOTS_PER_EPOCH) and @@ -1436,33 +1426,86 @@ func `==`*(a, b: SyncCommitteeDuty): bool = proc updateRuntimeConfig*(vc: ValidatorClientRef, node: BeaconNodeServerRef, info: VCRuntimeConfig): Result[void, string] = - if not(info.hasKey("ALTAIR_FORK_EPOCH")): - debug "Beacon node's configuration missing ALTAIR_FORK_EPOCH value", - node = node + var forkConfig = ? info.getConsensusForkConfig() - let - res = info.getOrDefault("ALTAIR_FORK_EPOCH", FAR_FUTURE_EPOCH) - wallEpoch = vc.beaconClock.now().slotOrZero().epoch() + if vc.runtimeConfig.forkConfig.isNone(): + vc.runtimeConfig.forkConfig = Opt.some(forkConfig) + else: + template localForkConfig: untyped = vc.runtimeConfig.forkConfig.get() + let wallEpoch = vc.beaconClock.now().slotOrZero().epoch() - return - if vc.runtimeConfig.altairEpoch.get(FAR_FUTURE_EPOCH) == FAR_FUTURE_EPOCH: - vc.runtimeConfig.altairEpoch = Opt.some(res) - ok() - else: - if res == vc.runtimeConfig.altairEpoch.get(): - ok() + proc validateForkVersionCompatibility( + consensusFork: ConsensusFork, + localForkVersion: Opt[Version], + localForkEpoch: Epoch, + forkVersion: Opt[Version]): Result[void, string] = + if localForkVersion.isNone(): + ok() # Potentially discovered new fork, save it at end of function else: - if res == FAR_FUTURE_EPOCH: - if wallEpoch < vc.runtimeConfig.altairEpoch.get(): - debug "Beacon node must be updated before Altair activates", + if forkVersion.isSome(): + if forkVersion.get() == localForkVersion.get(): + ok() # Already known + else: + err("Beacon node has conflicting " & + consensusFork.forkVersionConfigKey() & " value") + else: + if wallEpoch < localForkEpoch: + debug "Beacon node must be updated before fork activates", node = node, - altairForkEpoch = vc.runtimeConfig.altairEpoch.get() + consensusFork, + forkEpoch = localForkEpoch ok() else: err("Beacon node must be updated and report correct " & - "ALTAIR_FORK_EPOCH value") + $consensusFork & " config value") + + ? ConsensusFork.Capella.validateForkVersionCompatibility( + localForkConfig.capellaVersion, + localForkConfig.capellaEpoch, + forkConfig.capellaVersion) + + proc validateForkEpochCompatibility( + consensusFork: ConsensusFork, + localForkEpoch: Epoch, + forkEpoch: Epoch): Result[void, string] = + if localForkEpoch == FAR_FUTURE_EPOCH: + ok() # Potentially discovered new fork, save it at end of function + else: + if forkEpoch != FAR_FUTURE_EPOCH: + if forkEpoch == localForkEpoch: + ok() # Already known + else: + err("Beacon node has conflicting " & + consensusFork.forkEpochConfigKey() & " value") else: - err("Beacon node has conflicting ALTAIR_FORK_EPOCH value") + if wallEpoch < localForkEpoch: + debug "Beacon node must be updated before fork activates", + node = node, + consensusFork, + forkEpoch = localForkEpoch + ok() + else: + err("Beacon node must be updated and report correct " & + $consensusFork & " config value") + + ? ConsensusFork.Altair.validateForkEpochCompatibility( + localForkConfig.altairEpoch, forkConfig.altairEpoch) + ? ConsensusFork.Capella.validateForkEpochCompatibility( + localForkConfig.capellaEpoch, forkConfig.capellaEpoch) + ? ConsensusFork.Deneb.validateForkEpochCompatibility( + localForkConfig.denebEpoch, forkConfig.denebEpoch) + + # Save newly discovered forks. + if localForkConfig.altairEpoch == FAR_FUTURE_EPOCH: + localForkConfig.altairEpoch = forkConfig.altairEpoch + if localForkConfig.capellaVersion.isNone(): + localForkConfig.capellaVersion = forkConfig.capellaVersion + if localForkConfig.capellaEpoch == FAR_FUTURE_EPOCH: + localForkConfig.capellaEpoch = forkConfig.capellaEpoch + if localForkConfig.denebEpoch == FAR_FUTURE_EPOCH: + localForkConfig.denebEpoch = forkConfig.denebEpoch + + ok() proc `+`*(slot: Slot, epochs: Epoch): Slot = slot + uint64(epochs) * SLOTS_PER_EPOCH diff --git a/beacon_chain/validator_client/duties_service.nim b/beacon_chain/validator_client/duties_service.nim index 340ca49d7..fa6b84b36 100644 --- a/beacon_chain/validator_client/duties_service.nim +++ b/beacon_chain/validator_client/duties_service.nim @@ -5,6 +5,8 @@ # * 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: [].} + import std/[sets, sequtils] import chronicles, metrics import "."/[common, api, block_service, selection_proofs] @@ -210,7 +212,8 @@ proc pollForSyncCommitteeDuties*( let vc = service.client indices = toSeq(vc.attachedValidators[].indices()) - epoch = max(period.start_epoch(), vc.runtimeConfig.altairEpoch.get()) + altairEpoch = vc.runtimeConfig.forkConfig.get().altairEpoch + epoch = max(period.start_epoch(), altairEpoch) relevantDuties = block: var duties: seq[RestSyncCommitteeDuty] @@ -369,8 +372,11 @@ proc pollForSyncCommitteeDuties*(service: DutiesServiceRef) {.async.} = let currentSlot = vc.getCurrentSlot().get(Slot(0)) currentEpoch = currentSlot.epoch() - altairEpoch = vc.runtimeConfig.altairEpoch.valueOr: - return + altairEpoch = + if vc.runtimeConfig.forkConfig.isSome(): + vc.runtimeConfig.forkConfig.get().altairEpoch + else: + return if currentEpoch < altairEpoch: # We are not going to poll for sync committee duties until `altairEpoch`. diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index c090419e8..20d85060c 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -73,6 +73,10 @@ type proc (pubkey: ValidatorPubKey): Opt[ValidatorAndIndex] {.raises: [], gcsafe.} + GetCapellaForkVersionFn* = + proc (): Opt[Version] {.raises: [], gcsafe.} + GetDenebForkEpochFn* = + proc (): Opt[Epoch] {.raises: [], gcsafe.} GetForkFn* = proc (epoch: Epoch): Opt[Fork] {.raises: [], gcsafe.} GetGenesisFn* = @@ -90,6 +94,8 @@ type defaultBuilderAddress*: Opt[string] getValidatorAndIdxFn*: ValidatorPubKeyToDataFn getBeaconTimeFn*: GetBeaconTimeFn + getCapellaForkVersionFn*: GetCapellaForkVersionFn + getDenebForkEpochFn*: GetDenebForkEpochFn getForkFn*: GetForkFn getGenesisFn*: GetGenesisFn @@ -122,6 +128,8 @@ func init*(T: type KeymanagerHost, defaultBuilderAddress: Opt[string], getValidatorAndIdxFn: ValidatorPubKeyToDataFn, getBeaconTimeFn: GetBeaconTimeFn, + getCapellaForkVersionFn: GetCapellaForkVersionFn, + getDenebForkEpochFn: GetDenebForkEpochFn, getForkFn: GetForkFn, getGenesisFn: GetGenesisFn): T = T(validatorPool: validatorPool, @@ -135,6 +143,8 @@ func init*(T: type KeymanagerHost, defaultBuilderAddress: defaultBuilderAddress, getValidatorAndIdxFn: getValidatorAndIdxFn, getBeaconTimeFn: getBeaconTimeFn, + getCapellaForkVersionFn: getCapellaForkVersionFn, + getDenebForkEpochFn: getDenebForkEpochFn, getForkFn: getForkFn, getGenesisFn: getGenesisFn) diff --git a/beacon_chain/validators/validator_monitor.nim b/beacon_chain/validators/validator_monitor.nim index 059b10f9f..58672a197 100644 --- a/beacon_chain/validators/validator_monitor.nim +++ b/beacon_chain/validators/validator_monitor.nim @@ -149,6 +149,11 @@ declareCounter validator_monitor_proposer_slashing, declareCounter validator_monitor_attester_slashing, "Number of attester slashings seen", labels = ["src", "validator"] +declareCounter validator_monitor_block_hit, + "Number of times a block proposed by the validator was included an epoch later", labels = ["validator"] +declareCounter validator_monitor_block_miss, + "Number of times the validator was expected to propose a block but no block was included", labels = ["validator"] + const total = "total" # what we use for label when using "totals" mode @@ -405,12 +410,15 @@ func is_active_unslashed_in_previous_epoch(status: ParticipationInfo): bool = ParticipationFlag.eligible in status.flags proc registerEpochInfo*( - self: var ValidatorMonitor, epoch: Epoch, info: ForkedEpochInfo, - state: ForkyBeaconState) = + self: var ValidatorMonitor, state: ForkyBeaconState, + proposers: array[SLOTS_PER_EPOCH, Opt[ValidatorIndex]], + info: ForkedEpochInfo) = # Register rewards, as computed during the epoch transition that lands in # `epoch` - the rewards will be from attestations that were created at # `epoch - 2`. + let epoch = state.slot.epoch + if epoch < 2 or self.monitors.len == 0: return @@ -442,6 +450,24 @@ proc registerEpochInfo*( # attestations. continue + # Check that block proposals are sticky an epoch later + for i in 0..