update for latest LC REST proposal (#4213)
Implements the latest proposal for providing LC data via REST, as of https://github.com/ethereum/beacon-APIs/pull/247 with a v0 suffix. Requests: - `/eth/v0/beacon/light_client/bootstrap/{block_root}` - `/eth/v0/beacon/light_client/updates?start_period={start_period}&count={count}` - `/eth/v0/beacon/light_client/finality_update` - `/eth/v0/beacon/light_client/optimistic_update` HTTP Server-Sent Events (SSE): - `light_client_finality_update_v0` - `light_client_optimistic_update_v0`
This commit is contained in:
parent
2a0361cd18
commit
4b7bb4796f
|
@ -22,6 +22,14 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||
"/eth/v0/beacon/light_client/bootstrap/{block_root}") do (
|
||||
block_root: Eth2Digest) -> RestApiResponse:
|
||||
doAssert node.dag.lcDataStore.serve
|
||||
let contentType =
|
||||
block:
|
||||
let res = preferredContentType(jsonMediaType,
|
||||
sszMediaType)
|
||||
if res.isErr():
|
||||
return RestApiResponse.jsonError(Http406, ContentNotAcceptableError)
|
||||
res.get()
|
||||
|
||||
let vroot = block:
|
||||
if block_root.isErr():
|
||||
return RestApiResponse.jsonError(Http400, InvalidBlockRootValueError,
|
||||
|
@ -29,17 +37,35 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||
block_root.get()
|
||||
|
||||
let bootstrap = node.dag.getLightClientBootstrap(vroot)
|
||||
if bootstrap.isOk:
|
||||
return RestApiResponse.jsonResponse(bootstrap)
|
||||
else:
|
||||
if bootstrap.isNone:
|
||||
return RestApiResponse.jsonError(Http404, LCBootstrapUnavailable)
|
||||
|
||||
let
|
||||
contextEpoch = bootstrap.get.contextEpoch
|
||||
contextFork = node.dag.cfg.stateForkAtEpoch(contextEpoch)
|
||||
return
|
||||
if contentType == sszMediaType:
|
||||
let headers = [("eth-consensus-version", contextFork.toString())]
|
||||
RestApiResponse.sszResponse(bootstrap.get, headers)
|
||||
elif contentType == jsonMediaType:
|
||||
RestApiResponse.jsonResponseWVersion(bootstrap.get, contextFork)
|
||||
else:
|
||||
RestApiResponse.jsonError(Http500, InvalidAcceptError)
|
||||
|
||||
# https://github.com/ethereum/beacon-APIs/pull/181
|
||||
router.api(MethodGet,
|
||||
"/eth/v0/beacon/light_client/updates") do (
|
||||
start_period: Option[SyncCommitteePeriod], count: Option[uint64]
|
||||
) -> RestApiResponse:
|
||||
doAssert node.dag.lcDataStore.serve
|
||||
let contentType =
|
||||
block:
|
||||
let res = preferredContentType(jsonMediaType,
|
||||
sszMediaType)
|
||||
if res.isErr():
|
||||
return RestApiResponse.jsonError(Http406, ContentNotAcceptableError)
|
||||
res.get()
|
||||
|
||||
let vstart = block:
|
||||
if start_period.isNone():
|
||||
return RestApiResponse.jsonError(Http400, MissingStartPeriodValueError)
|
||||
|
@ -58,7 +84,7 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||
rcount.get()
|
||||
let
|
||||
headPeriod = node.dag.head.slot.sync_committee_period
|
||||
# Limit number of updates in response
|
||||
# Limit number of updates in response
|
||||
maxSupportedCount =
|
||||
if vstart > headPeriod:
|
||||
0'u64
|
||||
|
@ -67,31 +93,80 @@ proc installLightClientApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||
numPeriods = min(vcount, maxSupportedCount)
|
||||
onePastPeriod = vstart + numPeriods
|
||||
|
||||
var updates = newSeqOfCap[LightClientUpdate](numPeriods)
|
||||
var updates = newSeqOfCap[RestVersioned[LightClientUpdate]](numPeriods)
|
||||
for period in vstart..<onePastPeriod:
|
||||
let update = node.dag.getLightClientUpdateForPeriod(period)
|
||||
if update.isSome:
|
||||
updates.add update.get
|
||||
return RestApiResponse.jsonResponse(updates)
|
||||
let
|
||||
contextEpoch = update.get.contextEpoch
|
||||
contextFork = node.dag.cfg.stateForkAtEpoch(contextEpoch)
|
||||
updates.add RestVersioned[LightClientUpdate](
|
||||
data: update.get,
|
||||
jsonVersion: contextFork,
|
||||
sszContext: node.dag.forkDigests[].atStateFork(contextFork))
|
||||
|
||||
return
|
||||
if contentType == sszMediaType:
|
||||
RestApiResponse.sszResponseVersionedList(updates)
|
||||
elif contentType == jsonMediaType:
|
||||
RestApiResponse.jsonResponseVersionedList(updates)
|
||||
else:
|
||||
RestApiResponse.jsonError(Http500, InvalidAcceptError)
|
||||
|
||||
# https://github.com/ethereum/beacon-APIs/pull/181
|
||||
router.api(MethodGet,
|
||||
"/eth/v0/beacon/light_client/finality_update") do (
|
||||
) -> RestApiResponse:
|
||||
doAssert node.dag.lcDataStore.serve
|
||||
let contentType =
|
||||
block:
|
||||
let res = preferredContentType(jsonMediaType,
|
||||
sszMediaType)
|
||||
if res.isErr():
|
||||
return RestApiResponse.jsonError(Http406, ContentNotAcceptableError)
|
||||
res.get()
|
||||
|
||||
let finality_update = node.dag.getLightClientFinalityUpdate()
|
||||
if finality_update.isSome:
|
||||
return RestApiResponse.jsonResponse(finality_update)
|
||||
else:
|
||||
if finality_update.isNone:
|
||||
return RestApiResponse.jsonError(Http404, LCFinUpdateUnavailable)
|
||||
|
||||
let
|
||||
contextEpoch = finality_update.get.contextEpoch
|
||||
contextFork = node.dag.cfg.stateForkAtEpoch(contextEpoch)
|
||||
return
|
||||
if contentType == sszMediaType:
|
||||
let headers = [("eth-consensus-version", contextFork.toString())]
|
||||
RestApiResponse.sszResponse(finality_update.get, headers)
|
||||
elif contentType == jsonMediaType:
|
||||
RestApiResponse.jsonResponseWVersion(finality_update.get, contextFork)
|
||||
else:
|
||||
RestApiResponse.jsonError(Http500, InvalidAcceptError)
|
||||
|
||||
# https://github.com/ethereum/beacon-APIs/pull/181
|
||||
router.api(MethodGet,
|
||||
"/eth/v0/beacon/light_client/optimistic_update") do (
|
||||
) -> RestApiResponse:
|
||||
doAssert node.dag.lcDataStore.serve
|
||||
let contentType =
|
||||
block:
|
||||
let res = preferredContentType(jsonMediaType,
|
||||
sszMediaType)
|
||||
if res.isErr():
|
||||
return RestApiResponse.jsonError(Http406, ContentNotAcceptableError)
|
||||
res.get()
|
||||
|
||||
let optimistic_update = node.dag.getLightClientOptimisticUpdate()
|
||||
if optimistic_update.isSome:
|
||||
return RestApiResponse.jsonResponse(optimistic_update)
|
||||
else:
|
||||
if optimistic_update.isNone:
|
||||
return RestApiResponse.jsonError(Http404, LCOptUpdateUnavailable)
|
||||
|
||||
let
|
||||
contextEpoch = optimistic_update.get.contextEpoch
|
||||
contextFork = node.dag.cfg.stateForkAtEpoch(contextEpoch)
|
||||
return
|
||||
if contentType == sszMediaType:
|
||||
let headers = [("eth-consensus-version", contextFork.toString())]
|
||||
RestApiResponse.sszResponse(optimistic_update.get, headers)
|
||||
elif contentType == jsonMediaType:
|
||||
RestApiResponse.jsonResponseWVersion(optimistic_update.get, contextFork)
|
||||
else:
|
||||
RestApiResponse.jsonError(Http500, InvalidAcceptError)
|
||||
|
|
|
@ -279,6 +279,53 @@ proc jsonResponseWOpt*(t: typedesc[RestApiResponse], data: auto,
|
|||
default
|
||||
RestApiResponse.response(res, Http200, "application/json")
|
||||
|
||||
proc jsonResponseWVersion*(t: typedesc[RestApiResponse], data: auto,
|
||||
version: BeaconStateFork): RestApiResponse =
|
||||
let
|
||||
headers = [("eth-consensus-version", version.toString())]
|
||||
res =
|
||||
block:
|
||||
var default: seq[byte]
|
||||
try:
|
||||
var stream = memoryOutput()
|
||||
var writer = JsonWriter[RestJson].init(stream)
|
||||
writer.beginRecord()
|
||||
writer.writeField("version", version.toString())
|
||||
writer.writeField("data", data)
|
||||
writer.endRecord()
|
||||
stream.getOutput(seq[byte])
|
||||
except SerializationError:
|
||||
default
|
||||
except IOError:
|
||||
default
|
||||
RestApiResponse.response(res, Http200, "application/json", headers = headers)
|
||||
|
||||
type RestVersioned*[T] = object
|
||||
data*: T
|
||||
jsonVersion*: BeaconStateFork
|
||||
sszContext*: ForkDigest
|
||||
|
||||
proc jsonResponseVersionedList*[T](t: typedesc[RestApiResponse],
|
||||
entries: openArray[RestVersioned[T]]
|
||||
): RestApiResponse =
|
||||
let res =
|
||||
block:
|
||||
var default: seq[byte]
|
||||
try:
|
||||
var stream = memoryOutput()
|
||||
var writer = JsonWriter[RestJson].init(stream)
|
||||
for e in writer.stepwiseArrayCreation(entries):
|
||||
writer.beginRecord()
|
||||
writer.writeField("version", e.jsonVersion.toString())
|
||||
writer.writeField("data", e.data)
|
||||
writer.endRecord()
|
||||
stream.getOutput(seq[byte])
|
||||
except SerializationError:
|
||||
default
|
||||
except IOError:
|
||||
default
|
||||
RestApiResponse.response(res, Http200, "application/json")
|
||||
|
||||
proc jsonResponsePlain*(t: typedesc[RestApiResponse],
|
||||
data: auto): RestApiResponse =
|
||||
let res =
|
||||
|
@ -415,6 +462,28 @@ proc jsonErrorList*(t: typedesc[RestApiResponse],
|
|||
default
|
||||
RestApiResponse.error(status, data, "application/json")
|
||||
|
||||
proc sszResponseVersionedList*[T](t: typedesc[RestApiResponse],
|
||||
entries: openArray[RestVersioned[T]]
|
||||
): RestApiResponse =
|
||||
let res =
|
||||
block:
|
||||
var default: seq[byte]
|
||||
try:
|
||||
var stream = memoryOutput()
|
||||
for e in entries:
|
||||
var cursor = stream.delayFixedSizeWrite(sizeof(uint64))
|
||||
let initPos = stream.pos
|
||||
stream.write e.sszContext.data
|
||||
var writer = SszWriter.init(stream)
|
||||
writer.writeValue e.data
|
||||
cursor.finalWrite (stream.pos - initPos).uint64.toBytesLE()
|
||||
stream.getOutput(seq[byte])
|
||||
except SerializationError:
|
||||
default
|
||||
except IOError:
|
||||
default
|
||||
RestApiResponse.response(res, Http200, "application/octet-stream")
|
||||
|
||||
proc sszResponsePlain*(t: typedesc[RestApiResponse], res: seq[byte],
|
||||
headers: openArray[RestKeyValueTuple] = []
|
||||
): RestApiResponse =
|
||||
|
|
|
@ -308,6 +308,16 @@ template is_better_update*[A, B: SomeLightClientUpdate](
|
|||
new_update: A, old_update: B): bool =
|
||||
is_better_data(toMeta(new_update), toMeta(old_update))
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/light-client/p2p-interface.md#getlightclientbootstrap
|
||||
func contextEpoch*(bootstrap: altair.LightClientBootstrap): Epoch =
|
||||
bootstrap.header.slot.epoch
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/light-client/p2p-interface.md#lightclientupdatesbyrange
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/light-client/p2p-interface.md#getlightclientfinalityupdate
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/altair/light-client/p2p-interface.md#getlightclientoptimisticupdate
|
||||
func contextEpoch*(update: SomeLightClientUpdate): Epoch =
|
||||
update.attested_header.slot.epoch
|
||||
|
||||
# https://github.com/ethereum/consensus-specs/blob/v1.2.0/specs/bellatrix/beacon-chain.md#is_merge_transition_complete
|
||||
func is_merge_transition_complete*(state: bellatrix.BeaconState): bool =
|
||||
const defaultExecutionPayloadHeader = default(ExecutionPayloadHeader)
|
||||
|
|
|
@ -561,7 +561,7 @@ p2pProtocol BeaconSync(version = 1,
|
|||
let bootstrap = dag.getLightClientBootstrap(blockRoot)
|
||||
if bootstrap.isOk:
|
||||
let
|
||||
contextEpoch = bootstrap.get.header.slot.epoch
|
||||
contextEpoch = bootstrap.get.contextEpoch
|
||||
contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data
|
||||
await response.send(bootstrap.get, contextBytes)
|
||||
else:
|
||||
|
@ -605,7 +605,7 @@ p2pProtocol BeaconSync(version = 1,
|
|||
let update = dag.getLightClientUpdateForPeriod(period)
|
||||
if update.isSome:
|
||||
let
|
||||
contextEpoch = update.get.attested_header.slot.epoch
|
||||
contextEpoch = update.get.contextEpoch
|
||||
contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data
|
||||
await response.write(update.get, contextBytes)
|
||||
inc found
|
||||
|
@ -629,7 +629,7 @@ p2pProtocol BeaconSync(version = 1,
|
|||
let finality_update = dag.getLightClientFinalityUpdate()
|
||||
if finality_update.isSome:
|
||||
let
|
||||
contextEpoch = finality_update.get.attested_header.slot.epoch
|
||||
contextEpoch = finality_update.get.contextEpoch
|
||||
contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data
|
||||
await response.send(finality_update.get, contextBytes)
|
||||
else:
|
||||
|
@ -655,7 +655,7 @@ p2pProtocol BeaconSync(version = 1,
|
|||
let optimistic_update = dag.getLightClientOptimisticUpdate()
|
||||
if optimistic_update.isSome:
|
||||
let
|
||||
contextEpoch = optimistic_update.get.attested_header.slot.epoch
|
||||
contextEpoch = optimistic_update.get.contextEpoch
|
||||
contextBytes = peer.networkState.forkDigestAtEpoch(contextEpoch).data
|
||||
await response.send(optimistic_update.get, contextBytes)
|
||||
else:
|
||||
|
|
Loading…
Reference in New Issue