nimbus-eth2/beacon_chain/el/el_conf.nim

199 lines
6.6 KiB
Nim

# beacon_chain
# Copyright (c) 2023 Status Research & Development GmbH
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
std/[options, strutils, uri],
stew/results, chronicles, confutils,
confutils/toml/defs as confTomlDefs,
confutils/toml/std/net as confTomlNet,
confutils/toml/std/uri as confTomlUri,
json_serialization, # for logging
toml_serialization, toml_serialization/lexer,
../spec/engine_authentication
export
toml_serialization, confTomlDefs, confTomlNet, confTomlUri
type
EngineApiRole* = enum
DepositSyncing = "sync-deposits"
BlockValidation = "validate-blocks"
BlockProduction = "produce-blocks"
EngineApiRoles* = set[EngineApiRole]
EngineApiUrl* = object
url: string
jwtSecret: Option[seq[byte]]
roles: EngineApiRoles
EngineApiUrlConfigValue* = object
url*: string # TODO: Use the URI type here
jwtSecret* {.serializedFieldName: "jwt-secret".}: Option[string]
jwtSecretFile* {.serializedFieldName: "jwt-secret-file".}: Option[InputFile]
roles*: Option[EngineApiRoles]
const
defaultEngineApiRoles* = { DepositSyncing, BlockValidation, BlockProduction }
# https://github.com/ethereum/execution-apis/pull/302
defaultJwtSecret* = "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
defaultEngineApiUrl* = EngineApiUrlConfigValue(
url: "http://127.0.0.1:8551",
jwtSecret: some defaultJwtSecret)
chronicles.formatIt EngineApiUrl:
it.url
proc init*(T: type EngineApiUrl,
url: string,
jwtSecret = none seq[byte],
roles = defaultEngineApiRoles): T =
T(url: url, jwtSecret: jwtSecret, roles: roles)
func url*(engineUrl: EngineApiUrl): string =
engineUrl.url
func jwtSecret*(engineUrl: EngineApiUrl): Option[seq[byte]] =
engineUrl.jwtSecret
func roles*(engineUrl: EngineApiUrl): EngineApiRoles =
engineUrl.roles
func unknownRoleMsg(role: string): string =
"'" & role & "' is not a valid EL function"
template raiseError(reader: var TomlReader, msg: string) =
raiseTomlErr(reader.lex, msg)
proc readValue*(reader: var TomlReader, value: var EngineApiRoles)
{.raises: [SerializationError, IOError].} =
let roles = reader.readValue seq[string]
if roles.len == 0:
reader.raiseError "At least one role should be provided"
for role in roles:
case role.toLowerAscii
of $DepositSyncing:
value.incl DepositSyncing
of $BlockValidation:
value.incl BlockValidation
of $BlockProduction:
value.incl BlockProduction
else:
reader.raiseError(unknownRoleMsg role)
proc writeValue*(
writer: var JsonWriter, roles: EngineApiRoles) {.raises: [IOError].} =
var strRoles: seq[string]
for role in EngineApiRole:
if role in roles: strRoles.add $role
writer.writeValue strRoles
proc parseCmdArg*(T: type EngineApiUrlConfigValue, input: string): T
{.raises: [ValueError].} =
var
uri = parseUri(input)
jwtSecret: Option[string]
jwtSecretFile: Option[InputFile]
roles: Option[EngineApiRoles]
if uri.anchor != "":
for key, value in decodeQuery(uri.anchor):
case key
of "jwtSecret", "jwt-secret":
jwtSecret = some value
of "jwtSecretFile", "jwt-secret-file":
jwtSecretFile = some InputFile.parseCmdArg(value)
of "roles":
var uriRoles: EngineApiRoles = {}
for role in split(value, ","):
case role.toLowerAscii
of $DepositSyncing:
uriRoles.incl DepositSyncing
of $BlockValidation:
uriRoles.incl BlockValidation
of $BlockProduction:
uriRoles.incl BlockProduction
else:
raise newException(ValueError, unknownRoleMsg role)
if uriRoles == {}:
raise newException(ValueError, "The list of roles should not be empty")
roles = some uriRoles
else:
raise newException(ValueError, "'" & key & "' is not a recognized Engine URL property")
uri.anchor = ""
EngineApiUrlConfigValue(
url: $uri,
jwtSecret: jwtSecret,
jwtSecretFile: jwtSecretFile,
roles: roles)
proc readValue*(reader: var TomlReader, value: var EngineApiUrlConfigValue)
{.raises: [SerializationError, IOError].} =
if reader.lex.readable and reader.lex.peekChar in ['\'', '"']:
# If the input is a string, we'll reuse the command-line parsing logic
value = try: parseCmdArg(EngineApiUrlConfigValue, reader.readValue(string))
except ValueError as err:
reader.lex.raiseUnexpectedValue("Valid Engine API URL expected: " & err.msg)
else:
# Else, we'll use the standard object-serializer in TOML
toml_serialization.readValue(reader, value)
proc fixupWeb3Urls*(web3Url: var string) =
var normalizedUrl = toLowerAscii(web3Url)
if not (normalizedUrl.startsWith("https://") or
normalizedUrl.startsWith("http://") or
normalizedUrl.startsWith("wss://") or
normalizedUrl.startsWith("ws://")):
warn "The Web3 URL does not specify a protocol. Assuming a WebSocket server", web3Url
web3Url = "ws://" & web3Url
proc toFinalUrl*(confValue: EngineApiUrlConfigValue,
confJwtSecret: Option[seq[byte]]): Result[EngineApiUrl, cstring] =
if confValue.jwtSecret.isSome and confValue.jwtSecretFile.isSome:
return err "The options `jwtSecret` and `jwtSecretFile` should not be specified together"
let jwtSecret = if confValue.jwtSecret.isSome:
some(? parseJwtTokenValue(confValue.jwtSecret.get))
elif confValue.jwtSecretFile.isSome:
some(? loadJwtSecretFile(confValue.jwtSecretFile.get))
else:
confJwtSecret
var url = confValue.url
fixupWeb3Urls(url)
ok EngineApiUrl.init(
url = url,
jwtSecret = jwtSecret,
roles = confValue.roles.get(defaultEngineApiRoles))
proc loadJwtSecret*(jwtSecret: Option[InputFile]): Option[seq[byte]] =
if jwtSecret.isSome:
let res = loadJwtSecretFile(jwtSecret.get)
if res.isOk:
some res.value
else:
fatal "Failed to load JWT secret file", err = res.error
quit 1
else:
none seq[byte]
proc toFinalEngineApiUrls*(elUrls: seq[EngineApiUrlConfigValue],
confJwtSecret: Option[InputFile]): seq[EngineApiUrl] =
let jwtSecret = loadJwtSecret confJwtSecret
for elUrl in elUrls:
let engineApiUrl = elUrl.toFinalUrl(jwtSecret).valueOr:
fatal "Invalid EL configuration", err = error
quit 1
result.add engineApiUrl