nimbus-eth2/beacon_chain/spec/eth2_apis/rest_light_client_calls.nim

355 lines
13 KiB
Nim

# 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, endians2],
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))
func decodeSszLightClientObject[T: SomeForkedLightClientObject](
x: typedesc[T],
data: openArray[byte],
consensusFork: ConsensusFork,
cfg: RuntimeConfig): T {.raises: [RestError].} =
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 SerializationError as exc:
raiseRestDecodingBytesError(cstring("Malformed data: " & $exc.msg))
proc decodeJsonLightClientObject*[T: SomeForkedLightClientObject](
x: typedesc[T],
data: openArray[byte],
consensusFork: Opt[ConsensusFork],
cfg: RuntimeConfig): T {.raises: [RestError].} =
let objRes = decodeBytes(T, data, Opt.none(ContentTypeData))
if objRes.isErr:
raiseRestDecodingBytesError(objRes.error)
template obj: auto = objRes.get
obj.checkForkConsistency(cfg, consensusFork)
obj
proc decodeHttpLightClientObject*[T: SomeForkedLightClientObject](
x: typedesc[T],
data: openArray[byte],
mediaType: MediaType,
consensusFork: ConsensusFork,
cfg: RuntimeConfig): T {.raises: [RestError].} =
if mediaType == OctetStreamMediaType:
x.decodeSszLightClientObject(data, consensusFork, cfg)
elif mediaType == ApplicationJsonMediaType:
x.decodeJsonLightClientObject(data, Opt.some(consensusFork), cfg)
else:
raise newException(RestError, "Unsupported content-type")
proc decodeHttpLightClientObject[T: SomeForkedLightClientObject](
x: typedesc[T],
data: openArray[byte],
contentType: Opt[ContentTypeData],
consensusFork: ConsensusFork,
cfg: RuntimeConfig): T {.raises: [RestError].} =
let mediaTypeRes = decodeMediaType(contentType)
if mediaTypeRes.isErr:
raise newException(RestError, mediaTypeRes.error)
x.decodeHttpLightClientObject(data, mediaTypeRes.get, consensusFork, cfg)
proc decodeSszLightClientObjects[S: seq[SomeForkedLightClientObject]](
x: typedesc[S],
data: openArray[byte],
cfg: RuntimeConfig,
forkDigests: ref ForkDigests): S {.raises: [RestError].} =
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 SerializationError as exc:
raiseRestDecodingBytesError(cstring("Malformed data: " & $exc.msg))
res
proc decodeJsonLightClientObjects[S: seq[SomeForkedLightClientObject]](
x: typedesc[S],
data: openArray[byte],
cfg: RuntimeConfig,
forkDigests: ref ForkDigests): S {.raises: [RestError].} =
let objsRes = decodeBytes(S, data, Opt.none(ContentTypeData))
if objsRes.isErr:
raiseRestDecodingBytesError(objsRes.error)
template objs: auto = objsRes.get
for obj in objs:
obj.checkForkConsistency(cfg)
objs
proc decodeHttpLightClientObjects*[S: seq[SomeForkedLightClientObject]](
x: typedesc[S],
data: openArray[byte],
mediaType: MediaType,
cfg: RuntimeConfig,
forkDigests: ref ForkDigests): S {.raises: [RestError].} =
if mediaType == OctetStreamMediaType:
x.decodeSszLightClientObjects(data, cfg, forkDigests)
elif mediaType == ApplicationJsonMediaType:
x.decodeJsonLightClientObjects(data, cfg, forkDigests)
else:
raise newException(RestError, "Unsupported content-type")
proc decodeHttpLightClientObjects[S: seq[SomeForkedLightClientObject]](
x: typedesc[S],
data: openArray[byte],
contentType: Opt[ContentTypeData],
cfg: RuntimeConfig,
forkDigests: ref ForkDigests): S {.raises: [RestError].} =
let mediaTypeRes = decodeMediaType(contentType)
if mediaTypeRes.isErr:
raise newException(RestError, mediaTypeRes.error)
x.decodeHttpLightClientObjects(data, mediaTypeRes.get, cfg, forkDigests)
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 = ConsensusFork.decodeString(
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 ../../spec/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 = ConsensusFork.decodeString(
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 = ConsensusFork.decodeString(
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))