mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-02-17 00:47:03 +00:00
Number of REST fixes. (#2987)
* Initial commit. * Fix path. * Add validator keys to indices cache mechanism. Move syncComitteeParticipants to common place. * Fix sync participants order issue. * Fix error code when state could not be found. Refactor `state/validators` to use keysToIndices mechanism. * Fix RestValidatorIndex to ValidatorIndex conversion TODOs. * Address review comments. * Fix REST test rules.
This commit is contained in:
parent
fe8bbb2388
commit
7319f150e8
@ -70,6 +70,7 @@ type
|
|||||||
beaconClock*: BeaconClock
|
beaconClock*: BeaconClock
|
||||||
taskpool*: TaskPoolPtr
|
taskpool*: TaskPoolPtr
|
||||||
onAttestationSent*: OnAttestationCallback
|
onAttestationSent*: OnAttestationCallback
|
||||||
|
restKeysCache*: Table[ValidatorPubKey, ValidatorIndex]
|
||||||
|
|
||||||
const
|
const
|
||||||
MaxEmptySlotCount* = uint64(10*60) div SECONDS_PER_SLOT
|
MaxEmptySlotCount* = uint64(10*60) div SECONDS_PER_SLOT
|
||||||
|
@ -1037,6 +1037,18 @@ iterator syncSubcommittee*(
|
|||||||
yield syncCommittee[i]
|
yield syncCommittee[i]
|
||||||
inc i
|
inc i
|
||||||
|
|
||||||
|
iterator syncSubcommitteePairs*(
|
||||||
|
syncCommittee: openarray[ValidatorIndex],
|
||||||
|
committeeIdx: SyncCommitteeIndex): tuple[validatorIdx: ValidatorIndex,
|
||||||
|
committeeIdx: int] =
|
||||||
|
var
|
||||||
|
i = committeeIdx.asInt * SYNC_SUBCOMMITTEE_SIZE
|
||||||
|
onePastEndIdx = min(syncCommittee.len, i + SYNC_SUBCOMMITTEE_SIZE)
|
||||||
|
|
||||||
|
while i < onePastEndIdx:
|
||||||
|
yield (syncCommittee[i], i)
|
||||||
|
inc i
|
||||||
|
|
||||||
func syncCommitteeParticipants*(dagParam: ChainDAGRef,
|
func syncCommitteeParticipants*(dagParam: ChainDAGRef,
|
||||||
slotParam: Slot): seq[ValidatorPubKey] =
|
slotParam: Slot): seq[ValidatorPubKey] =
|
||||||
# TODO:
|
# TODO:
|
||||||
|
@ -411,9 +411,13 @@ proc init*(T: type BeaconNode,
|
|||||||
requestManager: RequestManager.init(network, blockProcessor),
|
requestManager: RequestManager.init(network, blockProcessor),
|
||||||
beaconClock: beaconClock,
|
beaconClock: beaconClock,
|
||||||
taskpool: taskpool,
|
taskpool: taskpool,
|
||||||
onAttestationSent: onAttestationSent
|
onAttestationSent: onAttestationSent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# initialize REST server cache tables.
|
||||||
|
if config.restEnabled:
|
||||||
|
node.restKeysCache = initTable[ValidatorPubKey, ValidatorIndex]()
|
||||||
|
|
||||||
# set topic validation routine
|
# set topic validation routine
|
||||||
network.setValidTopics(
|
network.setValidTopics(
|
||||||
block:
|
block:
|
||||||
|
@ -99,24 +99,6 @@ proc toString*(kind: ValidatorFilterKind): string =
|
|||||||
of ValidatorFilterKind.WithdrawalDone:
|
of ValidatorFilterKind.WithdrawalDone:
|
||||||
"withdrawal_done"
|
"withdrawal_done"
|
||||||
|
|
||||||
func syncCommitteeParticipants*(forkedState: ForkedHashedBeaconState,
|
|
||||||
epoch: Epoch): Result[seq[ValidatorPubKey], cstring] =
|
|
||||||
withState(forkedState):
|
|
||||||
when stateFork >= forkAltair:
|
|
||||||
let
|
|
||||||
headSlot = state.data.slot
|
|
||||||
epochPeriod = syncCommitteePeriod(epoch.compute_start_slot_at_epoch())
|
|
||||||
currentPeriod = syncCommitteePeriod(headSlot)
|
|
||||||
nextPeriod = currentPeriod + 1'u64
|
|
||||||
if epochPeriod == currentPeriod:
|
|
||||||
ok(@(state.data.current_sync_committee.pubkeys.data))
|
|
||||||
elif epochPeriod == nextPeriod:
|
|
||||||
ok(@(state.data.next_sync_committee.pubkeys.data))
|
|
||||||
else:
|
|
||||||
err("Epoch is outside the sync committee period of the state")
|
|
||||||
else:
|
|
||||||
err("State's fork do not support sync committees")
|
|
||||||
|
|
||||||
proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getGenesis
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getGenesis
|
||||||
router.api(MethodGet, "/api/eth/v1/beacon/genesis") do () -> RestApiResponse:
|
router.api(MethodGet, "/api/eth/v1/beacon/genesis") do () -> RestApiResponse:
|
||||||
@ -150,7 +132,7 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
bres.get()
|
bres.get()
|
||||||
node.withStateForBlockSlot(bslot):
|
node.withStateForBlockSlot(bslot):
|
||||||
return RestApiResponse.jsonResponse((root: stateRoot))
|
return RestApiResponse.jsonResponse((root: stateRoot))
|
||||||
return RestApiResponse.jsonError(Http500, InternalServerError)
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateFork
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateFork
|
||||||
router.api(MethodGet, "/api/eth/v1/beacon/states/{state_id}/fork") do (
|
router.api(MethodGet, "/api/eth/v1/beacon/states/{state_id}/fork") do (
|
||||||
@ -182,7 +164,7 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
getStateField(stateData.data, fork).epoch
|
getStateField(stateData.data, fork).epoch
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return RestApiResponse.jsonError(Http500, InternalServerError)
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateFinalityCheckpoints
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateFinalityCheckpoints
|
||||||
router.api(MethodGet,
|
router.api(MethodGet,
|
||||||
@ -214,7 +196,7 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
finalized: getStateField(stateData.data, finalized_checkpoint)
|
finalized: getStateField(stateData.data, finalized_checkpoint)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return RestApiResponse.jsonError(Http500, InternalServerError)
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidators
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidators
|
||||||
router.api(MethodGet, "/api/eth/v1/beacon/states/{state_id}/validators") do (
|
router.api(MethodGet, "/api/eth/v1/beacon/states/{state_id}/validators") do (
|
||||||
@ -259,59 +241,97 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
$res.error())
|
$res.error())
|
||||||
res.get()
|
res.get()
|
||||||
|
|
||||||
let (keySet, indexSet) =
|
|
||||||
block:
|
|
||||||
var res1: HashSet[ValidatorPubKey]
|
|
||||||
var res2: HashSet[ValidatorIndex]
|
|
||||||
for item in validatorIds:
|
|
||||||
case item.kind
|
|
||||||
of ValidatorQueryKind.Key:
|
|
||||||
if item.key in res1:
|
|
||||||
return RestApiResponse.jsonError(Http400, UniqueValidatorKeyError)
|
|
||||||
res1.incl(item.key)
|
|
||||||
of ValidatorQueryKind.Index:
|
|
||||||
let vitem =
|
|
||||||
block:
|
|
||||||
let vres = item.index.toValidatorIndex()
|
|
||||||
if vres.isErr():
|
|
||||||
case vres.error()
|
|
||||||
of ValidatorIndexError.TooHighValue:
|
|
||||||
return RestApiResponse.jsonError(Http400,
|
|
||||||
TooHighValidatorIndexValueError)
|
|
||||||
of ValidatorIndexError.UnsupportedValue:
|
|
||||||
return RestApiResponse.jsonError(Http500,
|
|
||||||
UnsupportedValidatorIndexValueError)
|
|
||||||
vres.get()
|
|
||||||
|
|
||||||
if vitem in res2:
|
|
||||||
return RestApiResponse.jsonError(Http400,
|
|
||||||
UniqueValidatorIndexError)
|
|
||||||
res2.incl(vitem)
|
|
||||||
(res1, res2)
|
|
||||||
|
|
||||||
node.withStateForBlockSlot(bslot):
|
node.withStateForBlockSlot(bslot):
|
||||||
let current_epoch = get_current_epoch(node.dag.headState.data)
|
let
|
||||||
var res: seq[RestValidator]
|
current_epoch = getStateField(stateData.data, slot).epoch()
|
||||||
for index, validator in getStateField(stateData.data, validators).pairs():
|
validatorsCount = lenu64(getStateField(stateData.data, validators))
|
||||||
let includeFlag =
|
|
||||||
(len(keySet) == 0) and (len(indexSet) == 0) or
|
|
||||||
(len(indexSet) > 0 and (ValidatorIndex(index) in indexSet)) or
|
|
||||||
(len(keySet) > 0 and (validator.pubkey in keySet))
|
|
||||||
let sres = validator.getStatus(current_epoch)
|
|
||||||
if sres.isOk():
|
|
||||||
let vstatus = sres.get()
|
|
||||||
let statusFlag = vstatus in validatorsMask
|
|
||||||
if includeFlag and statusFlag:
|
|
||||||
res.add(RestValidator(
|
|
||||||
index: ValidatorIndex(index),
|
|
||||||
balance:
|
|
||||||
Base10.toString(getStateField(stateData.data, balances)[index]),
|
|
||||||
status: toString(vstatus),
|
|
||||||
validator: validator
|
|
||||||
))
|
|
||||||
return RestApiResponse.jsonResponse(res)
|
|
||||||
|
|
||||||
return RestApiResponse.jsonError(Http500, InternalServerError)
|
let indices =
|
||||||
|
block:
|
||||||
|
var keyset: HashSet[ValidatorPubKey]
|
||||||
|
var indexset: HashSet[ValidatorIndex]
|
||||||
|
for item in validatorIds:
|
||||||
|
case item.kind
|
||||||
|
of ValidatorQueryKind.Key:
|
||||||
|
if item.key in keyset:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
UniqueValidatorKeyError)
|
||||||
|
keyset.incl(item.key)
|
||||||
|
of ValidatorQueryKind.Index:
|
||||||
|
let vindex =
|
||||||
|
block:
|
||||||
|
let vres = item.index.toValidatorIndex()
|
||||||
|
if vres.isErr():
|
||||||
|
case vres.error()
|
||||||
|
of ValidatorIndexError.TooHighValue:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
TooHighValidatorIndexValueError)
|
||||||
|
of ValidatorIndexError.UnsupportedValue:
|
||||||
|
return RestApiResponse.jsonError(Http500,
|
||||||
|
UnsupportedValidatorIndexValueError)
|
||||||
|
let index = vres.get()
|
||||||
|
if uint64(index) >= validatorsCount:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
ValidatorNotFoundError)
|
||||||
|
if index in indexset:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
UniqueValidatorIndexError)
|
||||||
|
index
|
||||||
|
indexset.incl(vindex)
|
||||||
|
|
||||||
|
if len(keyset) > 0:
|
||||||
|
let optIndices = keysToIndices(node.restKeysCache, stateData.data,
|
||||||
|
keyset.toSeq())
|
||||||
|
for item in optIndices:
|
||||||
|
if item.isNone():
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
ValidatorNotFoundError)
|
||||||
|
let vindex = item.get()
|
||||||
|
if vindex in indexset:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
UniqueValidatorIndexError)
|
||||||
|
indexset.incl(vindex)
|
||||||
|
indexset.toSeq()
|
||||||
|
|
||||||
|
let response =
|
||||||
|
block:
|
||||||
|
var res: seq[RestValidator]
|
||||||
|
if len(indices) == 0:
|
||||||
|
# There is no indices, so we going to filter all the validators.
|
||||||
|
for index, validator in getStateField(stateData.data,
|
||||||
|
validators).pairs():
|
||||||
|
let
|
||||||
|
balance = getStateField(stateData.data, balances)[index]
|
||||||
|
status =
|
||||||
|
block:
|
||||||
|
let sres = validator.getStatus(current_epoch)
|
||||||
|
if sres.isErr():
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
ValidatorStatusNotFoundError,
|
||||||
|
$sres.get())
|
||||||
|
sres.get()
|
||||||
|
if status in validatorsMask:
|
||||||
|
res.add(RestValidator.init(ValidatorIndex(index), balance,
|
||||||
|
toString(status), validator))
|
||||||
|
else:
|
||||||
|
for index in indices:
|
||||||
|
let
|
||||||
|
validator = getStateField(stateData.data, validators)[index]
|
||||||
|
balance = getStateField(stateData.data, balances)[index]
|
||||||
|
status =
|
||||||
|
block:
|
||||||
|
let sres = validator.getStatus(current_epoch)
|
||||||
|
if sres.isErr():
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
ValidatorStatusNotFoundError,
|
||||||
|
$sres.get())
|
||||||
|
sres.get()
|
||||||
|
if status in validatorsMask:
|
||||||
|
res.add(RestValidator.init(index, balance, toString(status),
|
||||||
|
validator))
|
||||||
|
res
|
||||||
|
return RestApiResponse.jsonResponse(response)
|
||||||
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidator
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidator
|
||||||
router.api(MethodGet,
|
router.api(MethodGet,
|
||||||
@ -337,33 +357,21 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
return RestApiResponse.jsonError(Http400, InvalidValidatorIdValueError,
|
return RestApiResponse.jsonError(Http400, InvalidValidatorIdValueError,
|
||||||
$validator_id.error())
|
$validator_id.error())
|
||||||
node.withStateForBlockSlot(bslot):
|
node.withStateForBlockSlot(bslot):
|
||||||
let current_epoch = get_current_epoch(node.dag.headState.data)
|
let
|
||||||
let vid = validator_id.get()
|
current_epoch = getStateField(stateData.data, slot).epoch()
|
||||||
case vid.kind
|
validatorsCount = lenu64(getStateField(stateData.data, validators))
|
||||||
of ValidatorQueryKind.Key:
|
|
||||||
for index, validator in getStateField(stateData.data,
|
let vindex =
|
||||||
validators).pairs():
|
block:
|
||||||
if validator.pubkey == vid.key:
|
let vid = validator_id.get()
|
||||||
let sres = validator.getStatus(current_epoch)
|
case vid.kind
|
||||||
if sres.isOk():
|
of ValidatorQueryKind.Key:
|
||||||
return RestApiResponse.jsonResponse(
|
let optIndices = keysToIndices(node.restKeysCache, stateData.data,
|
||||||
(
|
[vid.key])
|
||||||
index: ValidatorIndex(index),
|
if optIndices[0].isNone():
|
||||||
balance:
|
return RestApiResponse.jsonError(Http400, ValidatorNotFoundError)
|
||||||
Base10.toString(
|
optIndices[0].get()
|
||||||
getStateField(stateData.data, balances)[index]
|
of ValidatorQueryKind.Index:
|
||||||
),
|
|
||||||
status: toString(sres.get()),
|
|
||||||
validator: validator
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return RestApiResponse.jsonError(Http400,
|
|
||||||
ValidatorStatusNotFoundError)
|
|
||||||
return RestApiResponse.jsonError(Http404, ValidatorNotFoundError)
|
|
||||||
of ValidatorQueryKind.Index:
|
|
||||||
let vindex =
|
|
||||||
block:
|
|
||||||
let vres = vid.index.toValidatorIndex()
|
let vres = vid.index.toValidatorIndex()
|
||||||
if vres.isErr():
|
if vres.isErr():
|
||||||
case vres.error()
|
case vres.error()
|
||||||
@ -373,28 +381,26 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
of ValidatorIndexError.UnsupportedValue:
|
of ValidatorIndexError.UnsupportedValue:
|
||||||
return RestApiResponse.jsonError(Http500,
|
return RestApiResponse.jsonError(Http500,
|
||||||
UnsupportedValidatorIndexValueError)
|
UnsupportedValidatorIndexValueError)
|
||||||
vres.get()
|
let index = vres.get()
|
||||||
|
if uint64(index) >= validatorsCount:
|
||||||
|
return RestApiResponse.jsonError(Http400, ValidatorNotFoundError)
|
||||||
|
index
|
||||||
|
|
||||||
if uint64(vindex) >=
|
let
|
||||||
uint64(len(getStateField(stateData.data, validators))):
|
validator = getStateField(stateData.data, validators)[vindex]
|
||||||
return RestApiResponse.jsonError(Http404, ValidatorNotFoundError)
|
balance = getStateField(stateData.data, balances)[vindex]
|
||||||
let validator = getStateField(stateData.data, validators)[vindex]
|
status =
|
||||||
let sres = validator.getStatus(current_epoch)
|
block:
|
||||||
if sres.isOk():
|
let sres = validator.getStatus(current_epoch)
|
||||||
return RestApiResponse.jsonResponse(
|
if sres.isErr():
|
||||||
(
|
return RestApiResponse.jsonError(Http400,
|
||||||
index: vindex,
|
ValidatorStatusNotFoundError,
|
||||||
balance: Base10.toString(
|
$sres.get())
|
||||||
getStateField(stateData.data, balances)[vindex]
|
toString(sres.get())
|
||||||
),
|
return RestApiResponse.jsonResponse(
|
||||||
status: toString(sres.get()),
|
RestValidator.init(vindex, balance, status, validator)
|
||||||
validator: validator
|
)
|
||||||
)
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
)
|
|
||||||
else:
|
|
||||||
return RestApiResponse.jsonError(Http400,
|
|
||||||
ValidatorStatusNotFoundError)
|
|
||||||
return RestApiResponse.jsonError(Http500, InternalServerError)
|
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidatorBalances
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getStateValidatorBalances
|
||||||
router.api(MethodGet,
|
router.api(MethodGet,
|
||||||
@ -426,55 +432,77 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
return RestApiResponse.jsonError(Http400,
|
return RestApiResponse.jsonError(Http400,
|
||||||
MaximumNumberOfValidatorIdsError)
|
MaximumNumberOfValidatorIdsError)
|
||||||
ires
|
ires
|
||||||
let (keySet, indexSet) =
|
|
||||||
block:
|
|
||||||
var res1: HashSet[ValidatorPubKey]
|
|
||||||
var res2: HashSet[ValidatorIndex]
|
|
||||||
for item in validatorIds:
|
|
||||||
case item.kind
|
|
||||||
of ValidatorQueryKind.Key:
|
|
||||||
if item.key in res1:
|
|
||||||
return RestApiResponse.jsonError(Http400,
|
|
||||||
UniqueValidatorKeyError)
|
|
||||||
res1.incl(item.key)
|
|
||||||
of ValidatorQueryKind.Index:
|
|
||||||
let vitem =
|
|
||||||
block:
|
|
||||||
let vres = item.index.toValidatorIndex()
|
|
||||||
if vres.isErr():
|
|
||||||
case vres.error()
|
|
||||||
of ValidatorIndexError.TooHighValue:
|
|
||||||
return RestApiResponse.jsonError(Http400,
|
|
||||||
TooHighValidatorIndexValueError)
|
|
||||||
of ValidatorIndexError.UnsupportedValue:
|
|
||||||
return RestApiResponse.jsonError(Http500,
|
|
||||||
UnsupportedValidatorIndexValueError)
|
|
||||||
vres.get()
|
|
||||||
if vitem in res2:
|
|
||||||
return RestApiResponse.jsonError(Http400,
|
|
||||||
UniqueValidatorIndexError)
|
|
||||||
res2.incl(vitem)
|
|
||||||
(res1, res2)
|
|
||||||
node.withStateForBlockSlot(bslot):
|
|
||||||
let current_epoch = get_current_epoch(node.dag.headState.data)
|
|
||||||
var res: seq[RestValidatorBalance]
|
|
||||||
for index, validator in getStateField(stateData.data, validators).pairs():
|
|
||||||
let includeFlag =
|
|
||||||
(len(keySet) == 0) and (len(indexSet) == 0) or
|
|
||||||
(len(indexSet) > 0 and (ValidatorIndex(index) in indexSet)) or
|
|
||||||
(len(keySet) > 0 and (validator.pubkey in keySet))
|
|
||||||
let sres = validator.getStatus(current_epoch)
|
|
||||||
if sres.isOk():
|
|
||||||
let vstatus = sres.get()
|
|
||||||
if includeFlag:
|
|
||||||
res.add(RestValidatorBalance(
|
|
||||||
index: ValidatorIndex(index),
|
|
||||||
balance:
|
|
||||||
Base10.toString(getStateField(stateData.data, balances)[index]),
|
|
||||||
))
|
|
||||||
return RestApiResponse.jsonResponse(res)
|
|
||||||
|
|
||||||
return RestApiResponse.jsonError(Http500, InternalServerError)
|
node.withStateForBlockSlot(bslot):
|
||||||
|
let
|
||||||
|
current_epoch = getStateField(stateData.data, slot).epoch()
|
||||||
|
validatorsCount = lenu64(getStateField(stateData.data, validators))
|
||||||
|
|
||||||
|
let indices =
|
||||||
|
block:
|
||||||
|
var keyset: HashSet[ValidatorPubKey]
|
||||||
|
var indexset: HashSet[ValidatorIndex]
|
||||||
|
for item in validatorIds:
|
||||||
|
case item.kind
|
||||||
|
of ValidatorQueryKind.Key:
|
||||||
|
if item.key in keyset:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
UniqueValidatorKeyError)
|
||||||
|
keyset.incl(item.key)
|
||||||
|
of ValidatorQueryKind.Index:
|
||||||
|
let vindex =
|
||||||
|
block:
|
||||||
|
let vres = item.index.toValidatorIndex()
|
||||||
|
if vres.isErr():
|
||||||
|
case vres.error()
|
||||||
|
of ValidatorIndexError.TooHighValue:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
TooHighValidatorIndexValueError)
|
||||||
|
of ValidatorIndexError.UnsupportedValue:
|
||||||
|
return RestApiResponse.jsonError(Http500,
|
||||||
|
UnsupportedValidatorIndexValueError)
|
||||||
|
let index = vres.get()
|
||||||
|
if uint64(index) >= validatorsCount:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
ValidatorNotFoundError)
|
||||||
|
if index in indexset:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
UniqueValidatorIndexError)
|
||||||
|
index
|
||||||
|
indexset.incl(vindex)
|
||||||
|
|
||||||
|
if len(keyset) > 0:
|
||||||
|
let optIndices = keysToIndices(node.restKeysCache, stateData.data,
|
||||||
|
keyset.toSeq())
|
||||||
|
for item in optIndices:
|
||||||
|
if item.isNone():
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
ValidatorNotFoundError)
|
||||||
|
let vindex = item.get()
|
||||||
|
if vindex in indexset:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
UniqueValidatorIndexError)
|
||||||
|
indexset.incl(vindex)
|
||||||
|
indexset.toSeq()
|
||||||
|
|
||||||
|
let response =
|
||||||
|
block:
|
||||||
|
var res: seq[RestValidatorBalance]
|
||||||
|
if len(indices) == 0:
|
||||||
|
# There is no indices, so we going to filter all the validators.
|
||||||
|
for index, validator in getStateField(stateData.data,
|
||||||
|
validators).pairs():
|
||||||
|
let balance = getStateField(stateData.data, balances)[index]
|
||||||
|
res.add(RestValidatorBalance.init(ValidatorIndex(index),
|
||||||
|
balance))
|
||||||
|
else:
|
||||||
|
for index in indices:
|
||||||
|
let balance = getStateField(stateData.data, balances)[index]
|
||||||
|
res.add(RestValidatorBalance.init(index, balance))
|
||||||
|
res
|
||||||
|
return RestApiResponse.jsonResponse(response)
|
||||||
|
|
||||||
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochCommittees
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochCommittees
|
||||||
router.api(MethodGet,
|
router.api(MethodGet,
|
||||||
@ -564,7 +592,7 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
|
|
||||||
return RestApiResponse.jsonResponse(res)
|
return RestApiResponse.jsonResponse(res)
|
||||||
|
|
||||||
return RestApiResponse.jsonError(Http500, InternalServerError)
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochSyncCommittees
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getEpochSyncCommittees
|
||||||
router.api(MethodGet,
|
router.api(MethodGet,
|
||||||
@ -618,16 +646,15 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
let indices =
|
let indices =
|
||||||
block:
|
block:
|
||||||
var res: seq[ValidatorIndex]
|
var res: seq[ValidatorIndex]
|
||||||
let keyset = keys.toHashSet()
|
let optIndices = keysToIndices(node.restKeysCache, stateData().data,
|
||||||
for index, validator in getStateField(stateData.data,
|
keys)
|
||||||
validators).pairs():
|
for item in optIndices:
|
||||||
if validator.pubkey in keyset:
|
if item.isNone():
|
||||||
res.add(ValidatorIndex(uint64(index)))
|
return RestApiResponse.jsonError(Http500, InternalServerError,
|
||||||
|
"Could not get validator indices")
|
||||||
|
res.add(item.get())
|
||||||
res
|
res
|
||||||
|
|
||||||
if len(indices) != len(keys):
|
|
||||||
return RestApiResponse.jsonError(Http500, InternalServerError,
|
|
||||||
"Could not get validator indices")
|
|
||||||
let aggregates =
|
let aggregates =
|
||||||
block:
|
block:
|
||||||
var
|
var
|
||||||
@ -645,7 +672,7 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
validators: indices, validator_aggregates: aggregates)
|
validators: indices, validator_aggregates: aggregates)
|
||||||
)
|
)
|
||||||
|
|
||||||
return RestApiResponse.jsonError(Http400, "Could not get requested state")
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeaders
|
# https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeaders
|
||||||
router.api(MethodGet, "/api/eth/v1/beacon/headers") do (
|
router.api(MethodGet, "/api/eth/v1/beacon/headers") do (
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import presto,
|
import std/options,
|
||||||
|
presto,
|
||||||
nimcrypto/utils as ncrutils,
|
nimcrypto/utils as ncrutils,
|
||||||
../spec/[forks],
|
../spec/[forks],
|
||||||
../spec/eth2_apis/[rest_types, eth2_rest_serialization],
|
../spec/eth2_apis/[rest_types, eth2_rest_serialization],
|
||||||
@ -6,7 +7,7 @@ import presto,
|
|||||||
../consensus_object_pools/[block_pools_types, blockchain_dag]
|
../consensus_object_pools/[block_pools_types, blockchain_dag]
|
||||||
|
|
||||||
export
|
export
|
||||||
eth2_rest_serialization, blockchain_dag, presto, rest_types
|
options, eth2_rest_serialization, blockchain_dag, presto, rest_types
|
||||||
|
|
||||||
const
|
const
|
||||||
MaxEpoch* = compute_epoch_at_slot(not(0'u64))
|
MaxEpoch* = compute_epoch_at_slot(not(0'u64))
|
||||||
@ -85,6 +86,8 @@ const
|
|||||||
"Invalid graffiti bytes value"
|
"Invalid graffiti bytes value"
|
||||||
InvalidEpochValueError* =
|
InvalidEpochValueError* =
|
||||||
"Invalid epoch value"
|
"Invalid epoch value"
|
||||||
|
EpochFromFutureError* =
|
||||||
|
"Epoch value is far from the future"
|
||||||
InvalidStateIdValueError* =
|
InvalidStateIdValueError* =
|
||||||
"Invalid state identifier value"
|
"Invalid state identifier value"
|
||||||
InvalidBlockIdValueError* =
|
InvalidBlockIdValueError* =
|
||||||
@ -114,7 +117,7 @@ const
|
|||||||
UniqueValidatorIndexError* =
|
UniqueValidatorIndexError* =
|
||||||
"Only unique validator's index are allowed"
|
"Only unique validator's index are allowed"
|
||||||
StateNotFoundError* =
|
StateNotFoundError* =
|
||||||
"State not found"
|
"Could not get requested state"
|
||||||
SlotNotFoundError* =
|
SlotNotFoundError* =
|
||||||
"Slot number is too far away"
|
"Slot number is too far away"
|
||||||
SlotNotInNextWallSlotEpochError* =
|
SlotNotInNextWallSlotEpochError* =
|
||||||
@ -322,5 +325,48 @@ proc toValidatorIndex*(value: RestValidatorIndex): Result[ValidatorIndex,
|
|||||||
else:
|
else:
|
||||||
doAssert(false, "ValidatorIndex type size is incorrect")
|
doAssert(false, "ValidatorIndex type size is incorrect")
|
||||||
|
|
||||||
|
func syncCommitteeParticipants*(forkedState: ForkedHashedBeaconState,
|
||||||
|
epoch: Epoch): Result[seq[ValidatorPubKey], cstring] =
|
||||||
|
withState(forkedState):
|
||||||
|
when stateFork >= forkAltair:
|
||||||
|
let
|
||||||
|
headSlot = state.data.slot
|
||||||
|
epochPeriod = syncCommitteePeriod(epoch.compute_start_slot_at_epoch())
|
||||||
|
currentPeriod = syncCommitteePeriod(headSlot)
|
||||||
|
nextPeriod = currentPeriod + 1'u64
|
||||||
|
if epochPeriod == currentPeriod:
|
||||||
|
ok(@(state.data.current_sync_committee.pubkeys.data))
|
||||||
|
elif epochPeriod == nextPeriod:
|
||||||
|
ok(@(state.data.next_sync_committee.pubkeys.data))
|
||||||
|
else:
|
||||||
|
err("Epoch is outside the sync committee period of the state")
|
||||||
|
else:
|
||||||
|
err("State's fork do not support sync committees")
|
||||||
|
|
||||||
|
func keysToIndices*(cacheTable: var Table[ValidatorPubKey, ValidatorIndex],
|
||||||
|
forkedState: ForkedHashedBeaconState,
|
||||||
|
keys: openArray[ValidatorPubKey]
|
||||||
|
): seq[Option[ValidatorIndex]] =
|
||||||
|
var indices = newSeq[Option[ValidatorIndex]](len(keys))
|
||||||
|
var keyset =
|
||||||
|
block:
|
||||||
|
var res: Table[ValidatorPubKey, int]
|
||||||
|
for inputIndex, pubkey in keys.pairs():
|
||||||
|
# Try to search in cache first.
|
||||||
|
cacheTable.withValue(pubkey, vindex):
|
||||||
|
indices[inputIndex] = some(vindex[])
|
||||||
|
do:
|
||||||
|
res[pubkey] = inputIndex
|
||||||
|
res
|
||||||
|
if len(keyset) > 0:
|
||||||
|
for validatorIndex, validator in getStateField(forkedState,
|
||||||
|
validators).pairs():
|
||||||
|
keyset.withValue(validator.pubkey, listIndex):
|
||||||
|
# Store pair (pubkey, index) into cache table.
|
||||||
|
cacheTable[validator.pubkey] = ValidatorIndex(validatorIndex)
|
||||||
|
# Fill result sequence.
|
||||||
|
indices[listIndex[]] = some(ValidatorIndex(validatorIndex))
|
||||||
|
indices
|
||||||
|
|
||||||
proc getRouter*(): RestRouter =
|
proc getRouter*(): RestRouter =
|
||||||
RestRouter.init(validate)
|
RestRouter.init(validate)
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
# * 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).
|
# * 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.
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||||
import std/[typetraits, strutils]
|
import std/[typetraits, strutils, sequtils]
|
||||||
import stew/[results, base10], chronicles, json_serialization,
|
import stew/[results, base10], chronicles, json_serialization,
|
||||||
json_serialization/std/[options, net],
|
json_serialization/std/[options, net],
|
||||||
nimcrypto/utils as ncrutils
|
nimcrypto/utils as ncrutils
|
||||||
@ -142,6 +142,117 @@ proc installValidatorApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
res
|
res
|
||||||
return RestApiResponse.jsonResponseWRoot(duties, droot)
|
return RestApiResponse.jsonResponseWRoot(duties, droot)
|
||||||
|
|
||||||
|
router.api(MethodPost, "/api/eth/v1/validator/duties/sync/{epoch}") do (
|
||||||
|
epoch: Epoch, contentBody: Option[ContentBody]) -> RestApiResponse:
|
||||||
|
let indexList =
|
||||||
|
block:
|
||||||
|
if contentBody.isNone():
|
||||||
|
return RestApiResponse.jsonError(Http400, EmptyRequestBodyError)
|
||||||
|
let dres = decodeBody(seq[RestValidatorIndex], contentBody.get())
|
||||||
|
if dres.isErr():
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
InvalidValidatorIndexValueError,
|
||||||
|
$dres.error())
|
||||||
|
var res: seq[ValidatorIndex]
|
||||||
|
let items = dres.get()
|
||||||
|
for item in items:
|
||||||
|
let vres = item.toValidatorIndex()
|
||||||
|
if vres.isErr():
|
||||||
|
case vres.error()
|
||||||
|
of ValidatorIndexError.TooHighValue:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
TooHighValidatorIndexValueError)
|
||||||
|
of ValidatorIndexError.UnsupportedValue:
|
||||||
|
return RestApiResponse.jsonError(Http500,
|
||||||
|
UnsupportedValidatorIndexValueError)
|
||||||
|
res.add(vres.get())
|
||||||
|
if len(res) == 0:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
EmptyValidatorIndexArrayError)
|
||||||
|
res
|
||||||
|
let qepoch =
|
||||||
|
block:
|
||||||
|
if epoch.isErr():
|
||||||
|
return RestApiResponse.jsonError(Http400, InvalidEpochValueError,
|
||||||
|
$epoch.error())
|
||||||
|
let res = epoch.get()
|
||||||
|
if res > MaxEpoch:
|
||||||
|
return RestApiResponse.jsonError(Http400, EpochOverflowValueError)
|
||||||
|
|
||||||
|
if res > node.dag.head.slot.epoch() + 1:
|
||||||
|
if not(node.isSynced(node.dag.head)):
|
||||||
|
return RestApiResponse.jsonError(Http503, BeaconNodeInSyncError)
|
||||||
|
else:
|
||||||
|
return RestApiResponse.jsonError(Http400, EpochFromFutureError)
|
||||||
|
res
|
||||||
|
|
||||||
|
let bslot = node.dag.head.atSlot(qepoch.compute_start_slot_at_epoch())
|
||||||
|
|
||||||
|
node.withStateForBlockSlot(bslot):
|
||||||
|
let validatorsCount = lenu64(getStateField(stateData.data, validators))
|
||||||
|
let participants =
|
||||||
|
block:
|
||||||
|
let res = syncCommitteeParticipants(stateData().data, qepoch)
|
||||||
|
if res.isErr():
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
$res.error())
|
||||||
|
let kres = res.get()
|
||||||
|
if len(kres) == 0:
|
||||||
|
return RestApiResponse.jsonError(Http500, InternalServerError,
|
||||||
|
"List of sync committee participants is empty")
|
||||||
|
kres
|
||||||
|
|
||||||
|
# TODO: We doing this because `participants` are stored as array of
|
||||||
|
# validator keys, so we need to convert it to indices.
|
||||||
|
let participantIndices =
|
||||||
|
block:
|
||||||
|
var res: seq[ValidatorIndex]
|
||||||
|
let optIndices = keysToIndices(node.restKeysCache, stateData().data,
|
||||||
|
participants)
|
||||||
|
for item in optIndices:
|
||||||
|
if item.isNone():
|
||||||
|
return RestApiResponse.jsonError(Http500, InternalServerError,
|
||||||
|
"Could not get validator indices")
|
||||||
|
res.add(item.get())
|
||||||
|
res
|
||||||
|
|
||||||
|
let validatorsSet =
|
||||||
|
block:
|
||||||
|
var res: Table[ValidatorIndex, int]
|
||||||
|
for listIndex, validatorIndex in indexList.pairs():
|
||||||
|
if uint64(validatorIndex) >= validatorsCount:
|
||||||
|
return RestApiResponse.jsonError(Http400,
|
||||||
|
ValidatorNotFoundError)
|
||||||
|
res[validatorIndex] = listIndex
|
||||||
|
res
|
||||||
|
|
||||||
|
template isEmpty(duty: RestSyncCommitteeDuty): bool =
|
||||||
|
len(duty.validator_sync_committee_indices) == 0
|
||||||
|
|
||||||
|
var duties =
|
||||||
|
block:
|
||||||
|
var res = newSeq[RestSyncCommitteeDuty](len(indexList))
|
||||||
|
for committeeIdx in allSyncCommittees():
|
||||||
|
for valIndex, arrIndex in syncSubcommitteePairs(participantIndices,
|
||||||
|
committeeIdx):
|
||||||
|
let listIndex = validatorsSet.getOrDefault(valIndex, -1)
|
||||||
|
if listIndex >= 0:
|
||||||
|
if res[listIndex].isEmpty():
|
||||||
|
let key =
|
||||||
|
getStateField(stateData().data, validators)[valIndex].pubkey
|
||||||
|
res[listIndex] = RestSyncCommitteeDuty(
|
||||||
|
validator_index: valIndex,
|
||||||
|
pubkey: key
|
||||||
|
)
|
||||||
|
res[listIndex].validator_sync_committee_indices.add(
|
||||||
|
committeeIdx)
|
||||||
|
res.keepItIf(not(isEmpty(it)))
|
||||||
|
res
|
||||||
|
|
||||||
|
return RestApiResponse.jsonResponse(duties)
|
||||||
|
|
||||||
|
return RestApiResponse.jsonError(Http404, StateNotFoundError)
|
||||||
|
|
||||||
# https://ethereum.github.io/beacon-APIs/#/Validator/produceBlock
|
# https://ethereum.github.io/beacon-APIs/#/Validator/produceBlock
|
||||||
router.api(MethodGet, "/api/eth/v1/validator/blocks/{slot}") do (
|
router.api(MethodGet, "/api/eth/v1/validator/blocks/{slot}") do (
|
||||||
slot: Slot, randao_reveal: Option[ValidatorSig],
|
slot: Slot, randao_reveal: Option[ValidatorSig],
|
||||||
@ -558,6 +669,11 @@ proc installValidatorApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|||||||
"/eth/v1/validator/duties/proposer/{epoch}",
|
"/eth/v1/validator/duties/proposer/{epoch}",
|
||||||
"/api/eth/v1/validator/duties/proposer/{epoch}"
|
"/api/eth/v1/validator/duties/proposer/{epoch}"
|
||||||
)
|
)
|
||||||
|
router.redirect(
|
||||||
|
MethodPost,
|
||||||
|
"/eth/v1/validator/duties/sync/{epoch}",
|
||||||
|
"/api/eth/v1/validator/duties/sync/{epoch}"
|
||||||
|
)
|
||||||
router.redirect(
|
router.redirect(
|
||||||
MethodGet,
|
MethodGet,
|
||||||
"/eth/v1/validator/blocks/{slot}",
|
"/eth/v1/validator/blocks/{slot}",
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
import
|
import
|
||||||
std/[json, typetraits],
|
std/[json, typetraits],
|
||||||
|
stew/base10,
|
||||||
".."/forks,
|
".."/forks,
|
||||||
".."/datatypes/[phase0, altair]
|
".."/datatypes/[phase0, altair]
|
||||||
|
|
||||||
@ -101,6 +102,11 @@ type
|
|||||||
validator_index*: ValidatorIndex
|
validator_index*: ValidatorIndex
|
||||||
slot*: Slot
|
slot*: Slot
|
||||||
|
|
||||||
|
RestSyncCommitteeDuty* = object
|
||||||
|
pubkey*: ValidatorPubKey
|
||||||
|
validator_index*: ValidatorIndex
|
||||||
|
validator_sync_committee_indices*: seq[SyncCommitteeIndex]
|
||||||
|
|
||||||
RestCommitteeSubscription* = object
|
RestCommitteeSubscription* = object
|
||||||
validator_index*: ValidatorIndex
|
validator_index*: ValidatorIndex
|
||||||
committee_index*: CommitteeIndex
|
committee_index*: CommitteeIndex
|
||||||
@ -355,6 +361,9 @@ type
|
|||||||
ProduceBlockResponse* = DataEnclosedObject[phase0.BeaconBlock]
|
ProduceBlockResponse* = DataEnclosedObject[phase0.BeaconBlock]
|
||||||
ProduceBlockResponseV2* = ForkedBeaconBlock
|
ProduceBlockResponseV2* = ForkedBeaconBlock
|
||||||
|
|
||||||
|
func `==`*(a, b: RestValidatorIndex): bool =
|
||||||
|
uint64(a) == uint64(b)
|
||||||
|
|
||||||
func init*(t: typedesc[StateIdent], v: StateIdentType): StateIdent =
|
func init*(t: typedesc[StateIdent], v: StateIdentType): StateIdent =
|
||||||
StateIdent(kind: StateQueryKind.Named, value: v)
|
StateIdent(kind: StateQueryKind.Named, value: v)
|
||||||
|
|
||||||
@ -382,3 +391,13 @@ func init*(t: typedesc[ValidatorIdent], v: ValidatorPubKey): ValidatorIdent =
|
|||||||
func init*(t: typedesc[RestBlockInfo],
|
func init*(t: typedesc[RestBlockInfo],
|
||||||
v: ForkedTrustedSignedBeaconBlock): RestBlockInfo =
|
v: ForkedTrustedSignedBeaconBlock): RestBlockInfo =
|
||||||
RestBlockInfo(slot: v.slot(), blck: v.root())
|
RestBlockInfo(slot: v.slot(), blck: v.root())
|
||||||
|
|
||||||
|
func init*(t: typedesc[RestValidator], index: ValidatorIndex,
|
||||||
|
balance: uint64, status: string,
|
||||||
|
validator: Validator): RestValidator =
|
||||||
|
RestValidator(index: index, balance: Base10.toString(balance),
|
||||||
|
status: status, validator: validator)
|
||||||
|
|
||||||
|
func init*(t: typedesc[RestValidatorBalance], index: ValidatorIndex,
|
||||||
|
balance: uint64): RestValidatorBalance =
|
||||||
|
RestValidatorBalance(index: index, balance: Base10.toString(balance))
|
||||||
|
@ -544,7 +544,7 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "oneof", "value": ["404", "200"]},
|
"status": {"operator": "oneof", "value": ["400", "200"]},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -672,9 +672,9 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "equals", "value": "200"},
|
"status": {"operator": "equals", "value": "400"},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
"body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"index": "", "balance": "", "status": "", "validator": {"pubkey": "", "withdrawal_credentials": "", "effective_balance": "", "slashed": false, "activation_eligibility_epoch": "", "activation_epoch": "", "exit_epoch": "", "withdrawable_epoch": ""}}]}]
|
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -685,9 +685,9 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "equals", "value": "200"},
|
"status": {"operator": "equals", "value": "400"},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
"body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"index": "", "balance": "", "status": "", "validator": {"pubkey": "", "withdrawal_credentials": "", "effective_balance": "", "slashed": false, "activation_eligibility_epoch": "", "activation_epoch": "", "exit_epoch": "", "withdrawable_epoch": ""}}]}]
|
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -698,9 +698,9 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "equals", "value": "200"},
|
"status": {"operator": "equals", "value": "400"},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
"body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"index": "", "balance": "", "status": "", "validator": {"pubkey": "", "withdrawal_credentials": "", "effective_balance": "", "slashed": false, "activation_eligibility_epoch": "", "activation_epoch": "", "exit_epoch": "", "withdrawable_epoch": ""}}]}]
|
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -997,7 +997,7 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "oneof", "value": ["404", "200"]},
|
"status": {"operator": "oneof", "value": ["400", "200"]},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -1155,7 +1155,7 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "equals", "value": "404"},
|
"status": {"operator": "equals", "value": "400"},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
||||||
}
|
}
|
||||||
@ -1168,7 +1168,7 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "equals", "value": "404"},
|
"status": {"operator": "equals", "value": "400"},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
||||||
}
|
}
|
||||||
@ -1231,7 +1231,7 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "oneof", "value": ["404", "200"]},
|
"status": {"operator": "oneof", "value": ["400", "200"]},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1359,9 +1359,9 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "equals", "value": "200"},
|
"status": {"operator": "equals", "value": "400"},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
"body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"index": "", "balance": "", "status": "", "validator": {"pubkey": "", "withdrawal_credentials": "", "effective_balance": "", "slashed": false, "activation_eligibility_epoch": "", "activation_epoch": "", "exit_epoch": "", "withdrawable_epoch": ""}}]}]
|
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1372,9 +1372,9 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "equals", "value": "200"},
|
"status": {"operator": "equals", "value": "400"},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
"body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"index": "", "balance": "", "status": "", "validator": {"pubkey": "", "withdrawal_credentials": "", "effective_balance": "", "slashed": false, "activation_eligibility_epoch": "", "activation_epoch": "", "exit_epoch": "", "withdrawable_epoch": ""}}]}]
|
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -1385,9 +1385,9 @@
|
|||||||
"headers": {"Accept": "application/json"}
|
"headers": {"Accept": "application/json"}
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"status": {"operator": "equals", "value": "200"},
|
"status": {"operator": "equals", "value": "400"},
|
||||||
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
"headers": [{"key": "Content-Type", "value": "application/json", "operator": "equals"}],
|
||||||
"body": [{"operator": "jstructcmps", "start": ["data"], "value": [{"index": "", "balance": "", "status": "", "validator": {"pubkey": "", "withdrawal_credentials": "", "effective_balance": "", "slashed": false, "activation_eligibility_epoch": "", "activation_epoch": "", "exit_epoch": "", "withdrawable_epoch": ""}}]}]
|
"body": [{"operator": "jstructcmpns", "value": {"code": "", "message": ""}}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user