Skip to content

Commit

Permalink
add EIP-7044 support to keymanager API (#5959)
Browse files Browse the repository at this point in the history
* add EIP-7044 support to keymanager API

When trying to sign `VoluntaryExit` via keymanager API, the logic is not
yet aware of EIP-7044 (part of Deneb). This patch adds missing EIP-7044
support to the keymanager API as well.

As part of this, the VC needs to become aware about:

- `CAPELLA_FORK_VERSION`: To correctly form the EIP-7044 signing domain.
  The fork schedule does not indicate which of the results, if any,
  corresponds to Capella.
- `CAPELLA_FORK_EPOCH`: To detect whether Capella was scheduled.
  If a BN does not have it in its config while other BNs have it,
  this leads to a log if Capella has not activated yet, or marks the BN
  as incompatible if Capella already activated.
- `DENEB_FORK_EPOCH`: To check whether EIP-7044 logic should be used.

Related PRs:

- #5120 added support for processing EIP-7044 `VoluntaryExit` messages
  as part of the state transition functions (tested by EF spec tests).
- #5953 synced the support from #5120 to gossip validation.
- #5954 added support to the `nimbus_beacon_node deposits exit` command.
- #5956 contains an alternative generic version of `VCForkConfig`.

* address reviewer feedback: letter case, module location, double lookup

---------

Co-authored-by: cheatfate <eugene.kabanov@status.im>

* Update beacon_chain/rpc/rest_constants.nim

* move `VCRuntimeConfig` back to `rest_types`

---------

Co-authored-by: cheatfate <eugene.kabanov@status.im>

* fix `getForkVersion` helper

---------

Co-authored-by: cheatfate <eugene.kabanov@status.im>
  • Loading branch information
etan-status and cheatfate committed Feb 26, 2024
1 parent 00510a9 commit 4e9bc7f
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 67 deletions.
34 changes: 10 additions & 24 deletions beacon_chain/deposits.nim
Expand Up @@ -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:
Expand Down
8 changes: 8 additions & 0 deletions beacon_chain/nimbus_beacon_node.nim
Expand Up @@ -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))

Expand Down Expand Up @@ -784,6 +790,8 @@ proc init*(T: type BeaconNode,
config.getPayloadBuilderAddress,
getValidatorAndIdx,
getBeaconTime,
getCapellaForkVersion,
getDenebForkEpoch,
getForkForEpoch,
getGenesisRoot)
else: nil
Expand Down
17 changes: 17 additions & 0 deletions beacon_chain/nimbus_validator_client.nim
Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -379,6 +394,8 @@ proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} =
Opt.none(string),
nil,
vc.beaconClock.getBeaconTimeFn,
getCapellaForkVersion,
getDenebForkEpoch,
getForkForEpoch,
getGenesisRoot
)
Expand Down
4 changes: 4 additions & 0 deletions beacon_chain/rpc/rest_constants.nim
Expand Up @@ -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* =
Expand Down
14 changes: 12 additions & 2 deletions 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.
Expand Down Expand Up @@ -561,14 +564,15 @@ 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()
if res.isErr():
return keymanagerApiError(Http400, InvalidEpochValueError)
res.get()
else:
host.getBeaconTimeFn().slotOrZero().epoch()
currentEpoch
validator =
block:
let res = host.validatorPool[].getValidator(qpubkey).valueOr:
Expand All @@ -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()
Expand Down
6 changes: 4 additions & 2 deletions beacon_chain/spec/eth2_apis/rest_beacon_client.nim
Expand Up @@ -13,12 +13,14 @@ 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
chronos, client,
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
97 changes: 97 additions & 0 deletions 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
27 changes: 24 additions & 3 deletions beacon_chain/spec/signatures.nim
Expand Up @@ -216,11 +216,11 @@ 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*(
consensusFork: static ConsensusFork,
func voluntary_exit_signature_fork(
is_post_deneb: static bool,
state_fork: Fork,
capella_fork_version: Version): Fork =
when consensusFork >= ConsensusFork.Deneb:
when is_post_deneb:
# Always use Capella fork version, disregarding `VoluntaryExit` epoch
# [Modified in Deneb:EIP7044]
Fork(
Expand All @@ -230,6 +230,27 @@ func voluntary_exit_signature_fork*(
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,
Expand Down

0 comments on commit 4e9bc7f

Please sign in to comment.