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:
Etan Kissling 2023-04-16 08:07:07 +02:00 committed by GitHub
parent 57623af36a
commit cb9e0eed49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 648 additions and 14 deletions

View File

@ -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

View File

@ -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,

View File

@ -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] =

View File

@ -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

View File

@ -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")

View File

@ -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))

View File

@ -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)