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:
Etan Kissling 2022-10-04 13:38:09 +02:00 committed by Zahary Karadjov
parent 2a0361cd18
commit 4b7bb4796f
No known key found for this signature in database
GPG Key ID: C1F42EAFF38D570F
4 changed files with 171 additions and 17 deletions

View File

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

View File

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

View File

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

View File

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