allow trusted node sync based on LC trusted block root (#4736)
* allow trusted node sync based on LC trusted block root Extends `trustedNodeSync` with a new `--trusted-block-root` option that allows initializing a light client. No `--state-id` must be provided. The beacon node will then use this light client to obtain the latest finalized state from the remote server in a trust-minimized fashion. Note that the provided `--trusted-block-root` should be somewhat recent, and that security precautions such as comparing the state root against block explorers is still recommended. * fix * workaround for `valueOr` limitations * reduce magic numbers * digest len > context len for readability * move `cstring` conversion to caller * avoid abbreviations * `return` codestyle
This commit is contained in:
parent
57623af36a
commit
cb9e0eed49
|
@ -779,15 +779,19 @@ type
|
|||
|
||||
stateId* {.
|
||||
desc: "State id to sync to - this can be \"finalized\", a slot number or state hash or \"head\""
|
||||
defaultValue: "finalized",
|
||||
name: "state-id"
|
||||
.}: string
|
||||
.}: Option[string]
|
||||
|
||||
blockId* {.
|
||||
hidden
|
||||
desc: "Block id to sync to - this can be a block root, slot number, \"finalized\" or \"head\" (deprecated)"
|
||||
.}: Option[string]
|
||||
|
||||
lcTrustedBlockRoot* {.
|
||||
hidden
|
||||
desc: "Recent trusted finalized block root to initialize light client from"
|
||||
name: "trusted-block-root" .}: Option[Eth2Digest]
|
||||
|
||||
backfillBlocks* {.
|
||||
desc: "Backfill blocks directly from REST server instead of fetching via API"
|
||||
defaultValue: true
|
||||
|
|
|
@ -1991,6 +1991,23 @@ proc handleStartUpCmd(config: var BeaconNodeConf) {.raises: [Defect, CatchableEr
|
|||
let
|
||||
network = loadEth2Network(config)
|
||||
cfg = network.cfg
|
||||
syncTarget =
|
||||
if config.stateId.isSome:
|
||||
if config.lcTrustedBlockRoot.isSome:
|
||||
warn "Ignoring `trustedBlockRoot`, `stateId` is set",
|
||||
stateId = config.stateId,
|
||||
trustedBlockRoot = config.lcTrustedBlockRoot
|
||||
TrustedNodeSyncTarget(
|
||||
kind: TrustedNodeSyncKind.StateId,
|
||||
stateId: config.stateId.get)
|
||||
elif config.lcTrustedBlockRoot.isSome:
|
||||
TrustedNodeSyncTarget(
|
||||
kind: TrustedNodeSyncKind.TrustedBlockRoot,
|
||||
trustedBlockRoot: config.lcTrustedBlockRoot.get)
|
||||
else:
|
||||
TrustedNodeSyncTarget(
|
||||
kind: TrustedNodeSyncKind.StateId,
|
||||
stateId: "finalized")
|
||||
genesis =
|
||||
if network.genesisData.len > 0:
|
||||
newClone(readSszForkedHashedBeaconState(
|
||||
|
@ -2007,7 +2024,7 @@ proc handleStartUpCmd(config: var BeaconNodeConf) {.raises: [Defect, CatchableEr
|
|||
config.databaseDir,
|
||||
config.eraDir,
|
||||
config.trustedNodeUrl,
|
||||
config.stateId,
|
||||
syncTarget,
|
||||
config.backfillBlocks,
|
||||
config.reindex,
|
||||
config.downloadDepositSnapshot,
|
||||
|
|
|
@ -28,6 +28,20 @@ export
|
|||
from web3/ethtypes import BlockHash
|
||||
export ethtypes.BlockHash
|
||||
|
||||
func decodeMediaType*(
|
||||
contentType: Opt[ContentTypeData]): Result[MediaType, string] =
|
||||
if contentType.isNone or isWildCard(contentType.get.mediaType):
|
||||
return err("Missing or incorrect Content-Type")
|
||||
ok contentType.get.mediaType
|
||||
|
||||
func decodeEthConsensusVersion*(
|
||||
value: string): Result[ConsensusFork, string] =
|
||||
let normalizedValue = value.toLowerAscii()
|
||||
for consensusFork in ConsensusFork:
|
||||
if normalizedValue == ($consensusFork).toLowerAscii():
|
||||
return ok consensusFork
|
||||
err("Unsupported Eth-Consensus-Version: " & value)
|
||||
|
||||
Json.createFlavor RestJson
|
||||
|
||||
## The RestJson format implements JSON serialization in the way specified
|
||||
|
@ -136,7 +150,9 @@ type
|
|||
Web3SignerSignatureResponse |
|
||||
Web3SignerStatusResponse |
|
||||
GetStateRootResponse |
|
||||
GetBlockRootResponse
|
||||
GetBlockRootResponse |
|
||||
SomeForkedLightClientObject |
|
||||
seq[SomeForkedLightClientObject]
|
||||
|
||||
RestVersioned*[T] = object
|
||||
data*: T
|
||||
|
@ -1852,6 +1868,49 @@ proc writeValue*(writer: var JsonWriter[RestJson], value: ForkedHashedBeaconStat
|
|||
writer.writeField("data", value.denebData.data)
|
||||
writer.endRecord()
|
||||
|
||||
## SomeForkedLightClientObject
|
||||
proc readValue*[T: SomeForkedLightClientObject](
|
||||
reader: var JsonReader[RestJson], value: var T) {.
|
||||
raises: [IOError, SerializationError, Defect].} =
|
||||
var
|
||||
version: Opt[ConsensusFork]
|
||||
data: Opt[JsonString]
|
||||
|
||||
for fieldName in readObjectFields(reader):
|
||||
case fieldName
|
||||
of "version":
|
||||
if version.isSome:
|
||||
reader.raiseUnexpectedField("Multiple version fields found", T.name)
|
||||
let consensusFork =
|
||||
decodeEthConsensusVersion(reader.readValue(string)).valueOr:
|
||||
reader.raiseUnexpectedValue("Incorrect version field value")
|
||||
version.ok consensusFork
|
||||
of "data":
|
||||
if data.isSome:
|
||||
reader.raiseUnexpectedField("Multiple data fields found", T.name)
|
||||
data.ok reader.readValue(JsonString)
|
||||
else:
|
||||
unrecognizedFieldWarning()
|
||||
|
||||
if version.isNone:
|
||||
reader.raiseUnexpectedValue("Field version is missing")
|
||||
if data.isNone:
|
||||
reader.raiseUnexpectedValue("Field data is missing")
|
||||
|
||||
withLcDataFork(lcDataForkAtConsensusFork(version.get)):
|
||||
when lcDataFork > LightClientDataFork.None:
|
||||
value = T(kind: lcDataFork)
|
||||
try:
|
||||
value.forky(lcDataFork) = RestJson.decode(
|
||||
string(data.get()),
|
||||
T.Forky(lcDataFork),
|
||||
requireAllFields = true,
|
||||
allowUnknownFields = true)
|
||||
except SerializationError:
|
||||
reader.raiseUnexpectedValue("Incorrect format (" & $lcDataFork & ")")
|
||||
else:
|
||||
reader.raiseUnexpectedValue("Unsupported fork " & $version.get)
|
||||
|
||||
## Web3SignerRequest
|
||||
proc writeValue*(writer: var JsonWriter[RestJson],
|
||||
value: Web3SignerRequest) {.
|
||||
|
@ -2959,7 +3018,14 @@ proc decodeBytes*[T: DecodeTypes](
|
|||
proc encodeString*(value: string): RestResult[string] =
|
||||
ok(value)
|
||||
|
||||
proc encodeString*(value: Epoch|Slot|CommitteeIndex|SyncSubcommitteeIndex): RestResult[string] =
|
||||
proc encodeString*(
|
||||
value:
|
||||
uint64 |
|
||||
SyncCommitteePeriod |
|
||||
Epoch |
|
||||
Slot |
|
||||
CommitteeIndex |
|
||||
SyncSubcommitteeIndex): RestResult[string] =
|
||||
ok(Base10.toString(uint64(value)))
|
||||
|
||||
proc encodeString*(value: ValidatorSig): RestResult[string] =
|
||||
|
|
|
@ -10,12 +10,14 @@ import
|
|||
chronos, presto/client,
|
||||
"."/[
|
||||
rest_beacon_calls, rest_config_calls, rest_debug_calls,
|
||||
rest_node_calls, rest_validator_calls, rest_keymanager_calls,
|
||||
rest_keymanager_calls, rest_light_client_calls,
|
||||
rest_node_calls, rest_validator_calls,
|
||||
rest_nimbus_calls, rest_common
|
||||
]
|
||||
|
||||
export
|
||||
chronos, client,
|
||||
rest_beacon_calls, rest_config_calls, rest_debug_calls,
|
||||
rest_node_calls, rest_validator_calls, rest_keymanager_calls,
|
||||
rest_keymanager_calls, rest_light_client_calls,
|
||||
rest_node_calls, rest_validator_calls,
|
||||
rest_nimbus_calls, rest_common
|
||||
|
|
|
@ -29,3 +29,26 @@ proc raiseUnknownStatusError*(resp: RestPlainResponse) {.
|
|||
noreturn, raises: [RestError, Defect].} =
|
||||
let msg = "Unknown response status error (" & $resp.status & ")"
|
||||
raise newException(RestError, msg)
|
||||
|
||||
proc getBodyBytesWithCap*(
|
||||
response: HttpClientResponseRef,
|
||||
maxBytes: int): Future[Opt[seq[byte]]] {.async.} =
|
||||
var reader = response.getBodyReader()
|
||||
try:
|
||||
let
|
||||
data = await reader.read(maxBytes)
|
||||
isComplete = reader.atEof()
|
||||
await reader.closeWait()
|
||||
reader = nil
|
||||
await response.finish()
|
||||
if not isComplete:
|
||||
return err()
|
||||
return ok data
|
||||
except CancelledError as exc:
|
||||
if not(isNil(reader)):
|
||||
await reader.closeWait()
|
||||
raise exc
|
||||
except AsyncStreamError:
|
||||
if not(isNil(reader)):
|
||||
await reader.closeWait()
|
||||
raise newHttpReadError("Could not read response")
|
||||
|
|
|
@ -0,0 +1,320 @@
|
|||
# Copyright (c) 2023 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
|
||||
chronos,
|
||||
stew/results,
|
||||
presto/client,
|
||||
../helpers,
|
||||
"."/[rest_common, eth2_rest_serialization]
|
||||
|
||||
func checkForkConsistency(
|
||||
obj: SomeForkedLightClientObject,
|
||||
cfg: RuntimeConfig,
|
||||
consensusFork = err(Opt[ConsensusFork])) {.raises: [RestError].} =
|
||||
let objectFork = withForkyObject(obj):
|
||||
when lcDataFork > LightClientDataFork.None:
|
||||
cfg.consensusForkAtEpoch(forkyObject.contextEpoch)
|
||||
else:
|
||||
raiseRestDecodingBytesError("Invalid data")
|
||||
|
||||
if lcDataForkAtConsensusFork(objectFork) != obj.kind:
|
||||
raiseRestDecodingBytesError(cstring("Inconsistent forks" &
|
||||
" (kind: " & $(obj.kind) & ", data: " & $objectFork & ")"))
|
||||
|
||||
if consensusFork.isSome:
|
||||
if objectFork != consensusFork.get:
|
||||
raiseRestDecodingBytesError(cstring("Inconsistent forks" &
|
||||
" (header: " & $(consensusFork.get) & ", data: " & $objectFork & ")"))
|
||||
|
||||
func checkForkConsistency(
|
||||
obj: SomeForkedLightClientObject,
|
||||
cfg: RuntimeConfig,
|
||||
consensusFork: ConsensusFork) {.raises: [RestError].} =
|
||||
obj.checkForkConsistency(cfg, Opt[ConsensusFork].ok(consensusFork))
|
||||
|
||||
proc decodeHttpLightClientObject[T: SomeForkedLightClientObject](
|
||||
x: typedesc[T],
|
||||
data: seq[byte],
|
||||
contentType: Opt[ContentTypeData],
|
||||
consensusFork: ConsensusFork,
|
||||
cfg: RuntimeConfig): T {.raises: [RestError].} =
|
||||
let mediaTypeRes = decodeMediaType(contentType)
|
||||
if mediaTypeRes.isErr:
|
||||
raise newException(RestError, mediaTypeRes.error)
|
||||
template mediaType: auto = mediaTypeRes.get
|
||||
|
||||
return
|
||||
if mediaType == OctetStreamMediaType:
|
||||
try:
|
||||
withLcDataFork(lcDataForkAtConsensusFork(consensusFork)):
|
||||
when lcDataFork > LightClientDataFork.None:
|
||||
var obj = T(kind: lcDataFork)
|
||||
obj.forky(lcDataFork) = SSZ.decode(data, T.Forky(lcDataFork))
|
||||
obj.checkForkConsistency(cfg, consensusFork)
|
||||
obj
|
||||
else:
|
||||
raiseRestDecodingBytesError(
|
||||
cstring("Unsupported fork: " & $consensusFork))
|
||||
except SszError as exc:
|
||||
raiseRestDecodingBytesError(cstring("Malformed data: " & $exc.msg))
|
||||
|
||||
elif mediaType == ApplicationJsonMediaType:
|
||||
let objRes = decodeBytes(T, data, contentType)
|
||||
if objRes.isErr:
|
||||
raiseRestDecodingBytesError(objRes.error)
|
||||
template obj: auto = objRes.get
|
||||
obj.checkForkConsistency(cfg, consensusFork)
|
||||
obj
|
||||
|
||||
else:
|
||||
raise newException(RestError, "Unsupported content-type")
|
||||
|
||||
proc decodeHttpLightClientObjects[S: seq[SomeForkedLightClientObject]](
|
||||
x: typedesc[S],
|
||||
data: seq[byte],
|
||||
contentType: Opt[ContentTypeData],
|
||||
cfg: RuntimeConfig,
|
||||
forkDigests: ref ForkDigests): S {.raises: [RestError].} =
|
||||
let mediaTypeRes = decodeMediaType(contentType)
|
||||
if mediaTypeRes.isErr:
|
||||
raise newException(RestError, mediaTypeRes.error)
|
||||
template mediaType: auto = mediaTypeRes.get
|
||||
|
||||
return
|
||||
if mediaType == OctetStreamMediaType:
|
||||
let l = data.len
|
||||
var
|
||||
res: S
|
||||
o = 0
|
||||
while l - o != 0:
|
||||
# response_chunk_len
|
||||
type chunkLenType = uint64
|
||||
const chunkLenLen = sizeof chunkLenType # 8
|
||||
if l - o < chunkLenLen:
|
||||
raiseRestDecodingBytesError("Malformed data: Incomplete length")
|
||||
let responseChunkLen = chunkLenType.fromBytesLE(
|
||||
data.toOpenArray(o, o + chunkLenLen - 1))
|
||||
o = o + chunkLenLen
|
||||
|
||||
# response_chunk
|
||||
if responseChunkLen > int.high.chunkLenType:
|
||||
raiseRestDecodingBytesError("Malformed data: Unsupported length")
|
||||
if l - o < responseChunkLen.int:
|
||||
raiseRestDecodingBytesError("Malformed data: Incomplete chunk")
|
||||
let
|
||||
begin = o
|
||||
after = o + responseChunkLen.int
|
||||
o += responseChunkLen.int
|
||||
|
||||
# context
|
||||
const contextLen = sizeof ForkDigest # 4
|
||||
if responseChunkLen < contextLen.chunkLenType:
|
||||
raiseRestDecodingBytesError("Malformed data: Incomplete context")
|
||||
let
|
||||
context = ForkDigest [
|
||||
data[begin + 0], data[begin + 1], data[begin + 2], data[begin + 3]]
|
||||
consensusFork = forkDigests[].consensusForkForDigest(context).valueOr:
|
||||
raiseRestDecodingBytesError("Malformed data: Invalid context")
|
||||
|
||||
# payload
|
||||
try:
|
||||
withLcDataFork(lcDataForkAtConsensusFork(consensusFork)):
|
||||
when lcDataFork > LightClientDataFork.None:
|
||||
type T = typeof(res[0])
|
||||
var obj = T(kind: lcDataFork)
|
||||
obj.forky(lcDataFork) = SSZ.decode(
|
||||
data.toOpenArray(begin + contextLen, after - 1),
|
||||
T.Forky(lcDataFork))
|
||||
obj.checkForkConsistency(cfg, consensusFork)
|
||||
res.add obj
|
||||
else:
|
||||
raiseRestDecodingBytesError(
|
||||
cstring("Unsupported fork: " & $consensusFork))
|
||||
except SszError as exc:
|
||||
raiseRestDecodingBytesError(cstring("Malformed data: " & $exc.msg))
|
||||
res
|
||||
|
||||
elif mediaType == ApplicationJsonMediaType:
|
||||
let objsRes = decodeBytes(S, data, contentType)
|
||||
if objsRes.isErr:
|
||||
raiseRestDecodingBytesError(objsRes.error)
|
||||
template objs: auto = objsRes.get
|
||||
for obj in objs:
|
||||
obj.checkForkConsistency(cfg)
|
||||
objs
|
||||
|
||||
else:
|
||||
raise newException(RestError, "Unsupported content-type")
|
||||
|
||||
proc getLightClientBootstrapPlain(
|
||||
block_root: Eth2Digest): RestHttpResponseRef {.
|
||||
rest, endpoint: "/eth/v1/beacon/light_client/bootstrap/{block_root}",
|
||||
accept: preferSSZ,
|
||||
meth: MethodGet.}
|
||||
## https://ethereum.github.io/beacon-APIs/#/Beacon/getLightClientBootstrap
|
||||
|
||||
proc getLightClientBootstrap*(
|
||||
client: RestClientRef, block_root: Eth2Digest,
|
||||
cfg: RuntimeConfig, forkDigests: ref ForkDigests,
|
||||
restAccept = ""): Future[ForkedLightClientBootstrap] {.async.} =
|
||||
let resp =
|
||||
if len(restAccept) > 0:
|
||||
await client.getLightClientBootstrapPlain(
|
||||
block_root, restAcceptType = restAccept)
|
||||
else:
|
||||
await client.getLightClientBootstrapPlain(block_root)
|
||||
const maxBodyBytes = 128 * 1024
|
||||
let data = (await resp.getBodyBytesWithCap(maxBodyBytes)).valueOr:
|
||||
raiseRestDecodingBytesError("Response too long")
|
||||
return
|
||||
case resp.status
|
||||
of 200:
|
||||
let consensusForkRes = decodeEthConsensusVersion(
|
||||
resp.headers.getString("eth-consensus-version"))
|
||||
if consensusForkRes.isErr:
|
||||
raiseRestDecodingBytesError(cstring(consensusForkRes.error))
|
||||
ForkedLightClientBootstrap.decodeHttpLightClientObject(
|
||||
data, resp.contentType, consensusForkRes.get, cfg)
|
||||
of 404:
|
||||
default(ForkedLightClientBootstrap)
|
||||
of 400, 406, 500:
|
||||
let error =
|
||||
decodeBytes(RestErrorMessage, data, resp.contentType).valueOr:
|
||||
raiseRestDecodingBytesError(error)
|
||||
raise newException(RestError,
|
||||
"Error response (" & $resp.status & ") [" & error.message & "]")
|
||||
else:
|
||||
raiseRestResponseError(RestPlainResponse(
|
||||
status: resp.status,
|
||||
contentType: resp.contentType,
|
||||
data: data))
|
||||
|
||||
from ../../networking/eth2_network import MAX_REQUEST_LIGHT_CLIENT_UPDATES
|
||||
export MAX_REQUEST_LIGHT_CLIENT_UPDATES
|
||||
|
||||
proc getLightClientUpdatesByRangePlain(
|
||||
start_period: SyncCommitteePeriod, count: uint64): RestHttpResponseRef {.
|
||||
rest, endpoint: "/eth/v1/beacon/light_client/updates",
|
||||
accept: preferSSZ,
|
||||
meth: MethodGet.}
|
||||
## https://ethereum.github.io/beacon-APIs/#/Beacon/getLightClientUpdatesByRange
|
||||
|
||||
proc getLightClientUpdatesByRange*(
|
||||
client: RestClientRef, start_period: SyncCommitteePeriod, count: uint64,
|
||||
cfg: RuntimeConfig, forkDigests: ref ForkDigests,
|
||||
restAccept = ""): Future[seq[ForkedLightClientUpdate]] {.async.} =
|
||||
let resp =
|
||||
if len(restAccept) > 0:
|
||||
await client.getLightClientUpdatesByRangePlain(
|
||||
start_period, count, restAcceptType = restAccept)
|
||||
else:
|
||||
await client.getLightClientUpdatesByRangePlain(start_period, count)
|
||||
const maxBodyBytes = MAX_REQUEST_LIGHT_CLIENT_UPDATES * 128 * 1024
|
||||
let data = (await resp.getBodyBytesWithCap(maxBodyBytes)).valueOr:
|
||||
raiseRestDecodingBytesError("Response too long")
|
||||
return
|
||||
case resp.status
|
||||
of 200:
|
||||
seq[ForkedLightClientUpdate].decodeHttpLightClientObjects(
|
||||
data, resp.contentType, cfg, forkDigests)
|
||||
of 400, 406, 500:
|
||||
let error =
|
||||
decodeBytes(RestErrorMessage, data, resp.contentType).valueOr:
|
||||
raiseRestDecodingBytesError(error)
|
||||
raise newException(RestError,
|
||||
"Error response (" & $resp.status & ") [" & error.message & "]")
|
||||
else:
|
||||
raiseRestResponseError(RestPlainResponse(
|
||||
status: resp.status,
|
||||
contentType: resp.contentType,
|
||||
data: data))
|
||||
|
||||
proc getLightClientFinalityUpdatePlain(): RestHttpResponseRef {.
|
||||
rest, endpoint: "/eth/v1/beacon/light_client/finality_update",
|
||||
accept: preferSSZ,
|
||||
meth: MethodGet.}
|
||||
## https://ethereum.github.io/beacon-APIs/#/Beacon/getLightClientFinalityUpdate
|
||||
|
||||
proc getLightClientFinalityUpdate*(
|
||||
client: RestClientRef,
|
||||
cfg: RuntimeConfig, forkDigests: ref ForkDigests,
|
||||
restAccept = ""): Future[ForkedLightClientFinalityUpdate] {.async.} =
|
||||
let resp =
|
||||
if len(restAccept) > 0:
|
||||
await client.getLightClientFinalityUpdatePlain(
|
||||
restAcceptType = restAccept)
|
||||
else:
|
||||
await client.getLightClientFinalityUpdatePlain()
|
||||
const maxBodyBytes = 128 * 1024
|
||||
let data = (await resp.getBodyBytesWithCap(maxBodyBytes)).valueOr:
|
||||
raiseRestDecodingBytesError("Response too long")
|
||||
return
|
||||
case resp.status
|
||||
of 200:
|
||||
let consensusForkRes = decodeEthConsensusVersion(
|
||||
resp.headers.getString("eth-consensus-version"))
|
||||
if consensusForkRes.isErr:
|
||||
raiseRestDecodingBytesError(cstring(consensusForkRes.error))
|
||||
ForkedLightClientFinalityUpdate.decodeHttpLightClientObject(
|
||||
data, resp.contentType, consensusForkRes.get, cfg)
|
||||
of 404:
|
||||
default(ForkedLightClientFinalityUpdate)
|
||||
of 406, 500:
|
||||
let error =
|
||||
decodeBytes(RestErrorMessage, data, resp.contentType).valueOr:
|
||||
raiseRestDecodingBytesError(error)
|
||||
raise newException(RestError,
|
||||
"Error response (" & $resp.status & ") [" & error.message & "]")
|
||||
else:
|
||||
raiseRestResponseError(RestPlainResponse(
|
||||
status: resp.status,
|
||||
contentType: resp.contentType,
|
||||
data: data))
|
||||
|
||||
proc getLightClientOptimisticUpdatePlain(): RestHttpResponseRef {.
|
||||
rest, endpoint: "/eth/v1/beacon/light_client/optimistic_update",
|
||||
accept: preferSSZ,
|
||||
meth: MethodGet.}
|
||||
## https://ethereum.github.io/beacon-APIs/#/Beacon/getLightClientOptimisticUpdate
|
||||
|
||||
proc getLightClientOptimisticUpdate*(
|
||||
client: RestClientRef,
|
||||
cfg: RuntimeConfig, forkDigests: ref ForkDigests,
|
||||
restAccept = ""): Future[ForkedLightClientOptimisticUpdate] {.async.} =
|
||||
let resp =
|
||||
if len(restAccept) > 0:
|
||||
await client.getLightClientOptimisticUpdatePlain(
|
||||
restAcceptType = restAccept)
|
||||
else:
|
||||
await client.getLightClientOptimisticUpdatePlain()
|
||||
const maxBodyBytes = 128 * 1024
|
||||
let data = (await resp.getBodyBytesWithCap(maxBodyBytes)).valueOr:
|
||||
raiseRestDecodingBytesError("Response too long")
|
||||
return
|
||||
case resp.status
|
||||
of 200:
|
||||
let consensusForkRes = decodeEthConsensusVersion(
|
||||
resp.headers.getString("eth-consensus-version"))
|
||||
if consensusForkRes.isErr:
|
||||
raiseRestDecodingBytesError(cstring(consensusForkRes.error))
|
||||
ForkedLightClientOptimisticUpdate.decodeHttpLightClientObject(
|
||||
data, resp.contentType, consensusForkRes.get, cfg)
|
||||
of 404:
|
||||
default(ForkedLightClientOptimisticUpdate)
|
||||
of 406, 500:
|
||||
let error =
|
||||
decodeBytes(RestErrorMessage, data, resp.contentType).valueOr:
|
||||
raiseRestDecodingBytesError(error)
|
||||
raise newException(RestError,
|
||||
"Error response (" & $resp.status & ") [" & error.message & "]")
|
||||
else:
|
||||
raiseRestResponseError(RestPlainResponse(
|
||||
status: resp.status,
|
||||
contentType: resp.contentType,
|
||||
data: data))
|
|
@ -12,7 +12,7 @@ import
|
|||
./sync/sync_manager,
|
||||
./consensus_object_pools/[block_clearance, blockchain_dag],
|
||||
./spec/eth2_apis/rest_beacon_client,
|
||||
./spec/[beaconstate, eth2_merkleization, forks, presets,
|
||||
./spec/[beaconstate, eth2_merkleization, forks, light_client_sync, presets,
|
||||
state_transition, deposit_snapshots],
|
||||
"."/[beacon_clock, beacon_chain_db, era_db]
|
||||
|
||||
|
@ -45,19 +45,40 @@ proc fetchDepositSnapshot(client: RestClientRef):
|
|||
|
||||
from ./spec/datatypes/deneb import asSigVerified, shortLog
|
||||
|
||||
type
|
||||
TrustedNodeSyncKind* {.pure.} = enum
|
||||
TrustedBlockRoot,
|
||||
StateId
|
||||
|
||||
TrustedNodeSyncTarget* = object
|
||||
case kind*: TrustedNodeSyncKind
|
||||
of TrustedNodeSyncKind.TrustedBlockRoot:
|
||||
trustedBlockRoot*: Eth2Digest
|
||||
of TrustedNodeSyncKind.StateId:
|
||||
stateId*: string
|
||||
|
||||
func shortLog*(v: TrustedNodeSyncTarget): auto =
|
||||
case v.kind
|
||||
of TrustedNodeSyncKind.TrustedBlockRoot:
|
||||
"trustedBlockRoot(" & $v.trustedBlockRoot & ")"
|
||||
of TrustedNodeSyncKind.StateId:
|
||||
v.stateId
|
||||
|
||||
chronicles.formatIt(TrustedNodeSyncTarget): shortLog(it)
|
||||
|
||||
proc doTrustedNodeSync*(
|
||||
cfg: RuntimeConfig,
|
||||
databaseDir: string,
|
||||
eraDir: string,
|
||||
restUrl: string,
|
||||
stateId: string,
|
||||
syncTarget: TrustedNodeSyncTarget,
|
||||
backfill: bool,
|
||||
reindex: bool,
|
||||
downloadDepositSnapshot: bool,
|
||||
genesisState: ref ForkedHashedBeaconState = nil) {.async.} =
|
||||
logScope:
|
||||
restUrl
|
||||
stateId
|
||||
syncTarget
|
||||
|
||||
notice "Starting trusted node sync",
|
||||
databaseDir, backfill, reindex
|
||||
|
@ -134,6 +155,175 @@ proc doTrustedNodeSync*(
|
|||
Opt.none(BlockId)
|
||||
|
||||
if head.isNone:
|
||||
var stateRoot: Opt[Eth2Digest]
|
||||
let stateId =
|
||||
case syncTarget.kind
|
||||
of TrustedNodeSyncKind.TrustedBlockRoot:
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.3/specs/altair/light-client/light-client.md#light-client-sync-process
|
||||
const lcDataFork = LightClientDataFork.high
|
||||
var bestViableCheckpoint: Opt[tuple[slot: Slot, state_root: Eth2Digest]]
|
||||
func trackBestViableCheckpoint(store: lcDataFork.LightClientStore) =
|
||||
if store.finalized_header.beacon.slot.is_epoch:
|
||||
bestViableCheckpoint.ok((
|
||||
slot: store.finalized_header.beacon.slot,
|
||||
state_root: store.finalized_header.beacon.state_root))
|
||||
|
||||
if genesisState == nil:
|
||||
error "Genesis state is required when using `trustedBlockRoot`"
|
||||
quit 1
|
||||
let
|
||||
beaconClock = BeaconClock.init(
|
||||
getStateField(genesisState[], genesis_time))
|
||||
getBeaconTime = beaconClock.getBeaconTimeFn()
|
||||
|
||||
genesis_validators_root =
|
||||
getStateField(genesisState[], genesis_validators_root)
|
||||
forkDigests = newClone ForkDigests.init(cfg, genesis_validators_root)
|
||||
|
||||
trustedBlockRoot = syncTarget.trustedBlockRoot
|
||||
|
||||
var bootstrap =
|
||||
try:
|
||||
notice "Downloading LC bootstrap", trustedBlockRoot
|
||||
awaitWithTimeout(
|
||||
client.getLightClientBootstrap(
|
||||
trustedBlockRoot, cfg, forkDigests),
|
||||
smallRequestsTimeout
|
||||
):
|
||||
error "Attempt to download LC bootstrap timed out"
|
||||
quit 1
|
||||
except CatchableError as exc:
|
||||
error "Unable to download LC bootstrap", error = exc.msg
|
||||
quit 1
|
||||
if bootstrap.kind == LightClientDataFork.None:
|
||||
error "LC bootstrap unavailable on server"
|
||||
quit 1
|
||||
bootstrap.migrateToDataFork(lcDataFork)
|
||||
|
||||
var storeRes =
|
||||
initialize_light_client_store(
|
||||
trustedBlockRoot, bootstrap.forky(lcDataFork), cfg)
|
||||
if storeRes.isErr:
|
||||
error "`initialize_light_client_store` failed", err = storeRes.error
|
||||
quit 1
|
||||
template store: auto = storeRes.get
|
||||
store.trackBestViableCheckpoint()
|
||||
|
||||
while true:
|
||||
let
|
||||
finalized =
|
||||
store.finalized_header.beacon.slot.sync_committee_period
|
||||
optimistic =
|
||||
store.optimistic_header.beacon.slot.sync_committee_period
|
||||
current =
|
||||
getBeaconTime().slotOrZero().sync_committee_period
|
||||
isNextSyncCommitteeKnown =
|
||||
store.is_next_sync_committee_known
|
||||
|
||||
let
|
||||
periods: Slice[SyncCommitteePeriod] =
|
||||
if finalized == optimistic and not isNextSyncCommitteeKnown:
|
||||
if finalized >= current:
|
||||
finalized .. finalized
|
||||
else:
|
||||
finalized ..< current
|
||||
elif finalized + 1 < current:
|
||||
finalized + 1 ..< current
|
||||
else:
|
||||
break
|
||||
startPeriod = periods.a
|
||||
lastPeriod = periods.b
|
||||
count = min(periods.len, MAX_REQUEST_LIGHT_CLIENT_UPDATES).uint64
|
||||
|
||||
var updates =
|
||||
try:
|
||||
notice "Downloading LC updates", startPeriod, count
|
||||
awaitWithTimeout(
|
||||
client.getLightClientUpdatesByRange(
|
||||
startPeriod, count, cfg, forkDigests),
|
||||
smallRequestsTimeout
|
||||
):
|
||||
error "Attempt to download LC updates timed out"
|
||||
quit 1
|
||||
except CatchableError as exc:
|
||||
error "Unable to download LC updates", error = exc.msg
|
||||
quit 1
|
||||
if updates.lenu64 > count:
|
||||
error "Malformed LC updates response: Too many values"
|
||||
quit 1
|
||||
if updates.len == 0:
|
||||
warn "Server does not appear to be fully synced"
|
||||
break
|
||||
var expectedPeriod = startPeriod
|
||||
for i in 0 ..< updates.len:
|
||||
doAssert updates[i].kind > LightClientDataFork.None
|
||||
updates[i].migrateToDataFork(lcDataFork)
|
||||
let
|
||||
attPeriod = updates[i].forky(lcDataFork)
|
||||
.attested_header.beacon.slot.sync_committee_period
|
||||
sigPeriod = updates[i].forky(lcDataFork)
|
||||
.signature_slot.sync_committee_period
|
||||
if attPeriod != sigPeriod:
|
||||
error "Malformed LC updates response: Conflicting periods"
|
||||
quit 1
|
||||
if attPeriod < expectedPeriod:
|
||||
error "Malformed LC updates response: Unexpected period"
|
||||
quit 1
|
||||
if attPeriod > expectedPeriod:
|
||||
if attPeriod > lastPeriod:
|
||||
error "Malformed LC updates response: Period too high"
|
||||
quit 1
|
||||
expectedPeriod = attPeriod
|
||||
inc expectedPeriod
|
||||
|
||||
let res = process_light_client_update(
|
||||
store, updates[i].forky(lcDataFork),
|
||||
getBeaconTime().slotOrZero(), cfg, genesis_validators_root)
|
||||
if not res.isOk:
|
||||
error "`process_light_client_update` failed", resError = res.error
|
||||
quit 1
|
||||
store.trackBestViableCheckpoint()
|
||||
|
||||
var finalityUpdate =
|
||||
try:
|
||||
notice "Downloading LC finality update"
|
||||
awaitWithTimeout(
|
||||
client.getLightClientFinalityUpdate(cfg, forkDigests),
|
||||
smallRequestsTimeout
|
||||
):
|
||||
error "Attempt to download LC finality update timed out"
|
||||
quit 1
|
||||
except CatchableError as exc:
|
||||
error "Unable to download LC finality update", error = exc.msg
|
||||
quit 1
|
||||
if bootstrap.kind == LightClientDataFork.None:
|
||||
error "LC finality update unavailable on server"
|
||||
quit 1
|
||||
finalityUpdate.migrateToDataFork(lcDataFork)
|
||||
|
||||
let res = process_light_client_update(
|
||||
store, finalityUpdate.forky(lcDataFork),
|
||||
getBeaconTime().slotOrZero(), cfg, genesis_validators_root)
|
||||
if not res.isOk:
|
||||
error "`process_light_client_update` failed", resError = res.error
|
||||
quit 1
|
||||
store.trackBestViableCheckpoint()
|
||||
|
||||
if bestViableCheckpoint.isErr:
|
||||
error "CP not on epoch boundary. Retry later",
|
||||
latestCheckpointSlot = store.finalized_header.beacon.slot
|
||||
quit 1
|
||||
if not store.finalized_header.beacon.slot.is_epoch:
|
||||
warn "CP not on epoch boundary. Using older one",
|
||||
latestCheckpointSlot = store.finalized_header.beacon.slot,
|
||||
bestViableCheckpointSlot = bestViableCheckpoint.get.slot
|
||||
|
||||
stateRoot.ok bestViableCheckpoint.get.state_root
|
||||
Base10.toString(distinctBase(bestViableCheckpoint.get.slot))
|
||||
of TrustedNodeSyncKind.StateId:
|
||||
syncTarget.stateId
|
||||
logScope: stateId
|
||||
|
||||
notice "Downloading checkpoint state"
|
||||
|
||||
let
|
||||
|
@ -156,10 +346,18 @@ proc doTrustedNodeSync*(
|
|||
quit 1
|
||||
|
||||
if state == nil:
|
||||
error "No state found a given checkpoint",
|
||||
stateId
|
||||
error "No state found a given checkpoint"
|
||||
quit 1
|
||||
|
||||
if stateRoot.isSome:
|
||||
if state[].getStateRoot() != stateRoot.get:
|
||||
error "Checkpoint state has incorrect root!",
|
||||
expectedStateRoot = stateRoot.get,
|
||||
actualStateRoot = state[].getStateRoot()
|
||||
quit 1
|
||||
info "Checkpoint state validated against LC data",
|
||||
stateRoot = stateRoot.get
|
||||
|
||||
if not getStateField(state[], slot).is_epoch():
|
||||
error "State slot must fall on an epoch boundary",
|
||||
slot = getStateField(state[], slot),
|
||||
|
@ -331,8 +529,12 @@ when isMainModule:
|
|||
std/[os],
|
||||
networking/network_metadata
|
||||
|
||||
let backfill = os.paramCount() > 5 and os.paramStr(6) == "true"
|
||||
let
|
||||
syncTarget = TrustedNodeSyncTarget(
|
||||
kind: TrustedNodeSyncKind.StateId,
|
||||
stateId: os.paramStr(5))
|
||||
backfill = os.paramCount() > 5 and os.paramStr(6) == "true"
|
||||
|
||||
waitFor doTrustedNodeSync(
|
||||
getRuntimeConfig(some os.paramStr(1)), os.paramStr(2), os.paramStr(3),
|
||||
os.paramStr(4), os.paramStr(5), backfill, false, true)
|
||||
os.paramStr(4), syncTarget, backfill, false, true)
|
||||
|
|
Loading…
Reference in New Issue