nimbus-eth2/beacon_chain/rpc/rpc_beacon_api.nim

523 lines
20 KiB
Nim
Raw Normal View History

# beacon_chain
# Copyright (c) 2018-2021 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: [Defect].}
import
std/[parseutils, sequtils, strutils, sets],
stew/results,
json_rpc/servers/httpserver,
chronicles,
nimcrypto/utils as ncrutils,
../beacon_node_common,
../networking/eth2_network,
../validators/validator_duties,
../consensus_object_pools/blockchain_dag,
disentangle eth2 types from the ssz library (#2785) * reorganize ssz dependencies This PR continues the work in https://github.com/status-im/nimbus-eth2/pull/2646, https://github.com/status-im/nimbus-eth2/pull/2779 as well as past issues with serialization and type, to disentangle SSZ from eth2 and at the same time simplify imports and exports with a structured approach. The principal idea here is that when a library wants to introduce SSZ support, they do so via 3 files: * `ssz_codecs` which imports and reexports `codecs` - this covers the basic byte conversions and ensures no overloads get lost * `xxx_merkleization` imports and exports `merkleization` to specialize and get access to `hash_tree_root` and friends * `xxx_ssz_serialization` imports and exports `ssz_serialization` to specialize ssz for a specific library Those that need to interact with SSZ always import the `xxx_` versions of the modules and never `ssz` itself so as to keep imports simple and safe. This is similar to how the REST / JSON-RPC serializers are structured in that someone wanting to serialize spec types to REST-JSON will import `eth2_rest_serialization` and nothing else. * split up ssz into a core library that is independendent of eth2 types * rename `bytes_reader` to `codec` to highlight that it contains coding and decoding of bytes and native ssz types * remove tricky List init overload that causes compile issues * get rid of top-level ssz import * reenable merkleization tests * move some "standard" json serializers to spec * remove `ValidatorIndex` serialization for now * remove test_ssz_merkleization * add tests for over/underlong byte sequences * fix broken seq[byte] test - seq[byte] is not an SSZ type There are a few things this PR doesn't solve: * like #2646 this PR is weak on how to handle root and other dontSerialize fields that "sometimes" should be computed - the same problem appears in REST / JSON-RPC etc * Fix a build problem on macOS * Another way to fix the macOS builds Co-authored-by: Zahary Karadjov <zahary@gmail.com>
2021-08-18 18:57:58 +00:00
../spec/[eth2_merkleization, forks, network],
../spec/datatypes/[phase0],
./rpc_utils
logScope: topics = "beaconapi"
type
RpcServer = RpcHttpServer
ValidatorQuery = object
keyset: HashSet[ValidatorPubKey]
ids: seq[uint64]
StatusQuery = object
statset: HashSet[string]
template unimplemented() =
raise (ref CatchableError)(msg: "Unimplemented")
proc parsePubkey(str: string): ValidatorPubKey {.raises: [Defect, ValueError].} =
const expectedLen = RawPubKeySize * 2 + 2
if str.len != expectedLen: # +2 because of the `0x` prefix
raise newException(ValueError,
"A hex public key should be exactly " & $expectedLen & " characters. " &
$str.len & " provided")
let pubkeyRes = fromHex(ValidatorPubKey, str)
if pubkeyRes.isErr:
raise newException(ValueError, "Not a valid public key")
return pubkeyRes[]
proc createIdQuery(ids: openArray[string]): Result[ValidatorQuery, string] =
# validatorIds array should have maximum 30 items, and all items should be
# unique.
if len(ids) > 30:
return err("The number of ids exceeds the limit")
# All ids in validatorIds must be unique.
if len(ids) != len(toHashSet(ids)):
return err("ids array must have unique item")
var res = ValidatorQuery(
keyset: initHashSet[ValidatorPubKey](),
ids: newSeq[uint64]()
)
for item in ids:
if item.startsWith("0x"):
if len(item) != RawPubKeySize * 2 + 2:
return err("Incorrect hexadecimal key")
let pubkeyRes = ValidatorPubKey.fromHex(item)
if pubkeyRes.isErr:
return err("Incorrect public key")
res.keyset.incl(pubkeyRes.get())
else:
var tmp: uint64
try:
if parseBiggestUInt(item, tmp) != len(item):
return err("Incorrect index value")
except ValueError:
return err("Cannot parse index value: " & item)
res.ids.add(tmp)
ok(res)
proc createStatusQuery(status: openArray[string]): Result[StatusQuery, string] =
const AllowedStatuses = [
"pending", "pending_initialized", "pending_queued",
"active", "active_ongoing", "active_exiting", "active_slashed",
"exited", "exited_unslashed", "exited_slashed",
"withdrawal", "withdrawal_possible", "withdrawal_done"
]
if len(status) > len(AllowedStatuses):
return err("The number of statuses exceeds the limit")
var res = StatusQuery(statset: initHashSet[string]())
# All ids in validatorIds must be unique.
if len(status) != len(toHashSet(status)):
return err("Status array must have unique items")
for item in status:
if item notin AllowedStatuses:
return err("Invalid status requested")
case item
of "pending":
res.statset.incl("pending_initialized")
res.statset.incl("pending_queued")
of "active":
res.statset.incl("active_ongoing")
res.statset.incl("active_exiting")
res.statset.incl("active_slashed")
of "exited":
res.statset.incl("exited_unslashed")
res.statset.incl("exited_slashed")
of "withdrawal":
res.statset.incl("withdrawal_possible")
res.statset.incl("withdrawal_done")
else:
res.statset.incl(item)
proc getStatus(validator: Validator,
current_epoch: Epoch): Result[string, string] =
if validator.activation_epoch > current_epoch:
# pending
if validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH:
ok("pending_initialized")
else:
# validator.activation_eligibility_epoch < FAR_FUTURE_EPOCH:
ok("pending_queued")
elif (validator.activation_epoch <= current_epoch) and
(current_epoch < validator.exit_epoch):
# active
if validator.exit_epoch == FAR_FUTURE_EPOCH:
ok("active_ongoing")
elif not validator.slashed:
# validator.exit_epoch < FAR_FUTURE_EPOCH
ok("active_exiting")
else:
# validator.exit_epoch < FAR_FUTURE_EPOCH and validator.slashed:
ok("active_slashed")
elif (validator.exit_epoch <= current_epoch) and
(current_epoch < validator.withdrawable_epoch):
# exited
if not validator.slashed:
ok("exited_unslashed")
else:
# validator.slashed
ok("exited_slashed")
elif validator.withdrawable_epoch <= current_epoch:
# withdrawal
if validator.effective_balance != 0:
ok("withdrawal_possible")
else:
# validator.effective_balance == 0
ok("withdrawal_done")
else:
err("Invalid validator status")
proc getBlockDataFromBlockId(node: BeaconNode, blockId: string): BlockData {.
raises: [Defect, CatchableError].} =
result = case blockId:
of "head":
node.dag.get(node.dag.head)
of "genesis":
node.dag.getGenesisBlockData()
of "finalized":
node.dag.get(node.dag.finalizedHead.blck)
else:
if blockId.startsWith("0x"):
let blckRoot = parseRoot(blockId)
let blockData = node.dag.get(blckRoot)
if blockData.isNone:
raise newException(CatchableError, "Block not found")
blockData.get()
else:
let blockSlot = node.getBlockSlotFromString(blockId)
if blockSlot.blck.isNil:
raise newException(CatchableError, "Block not found")
node.dag.get(blockSlot.blck)
proc installBeaconApiHandlers*(rpcServer: RpcServer, node: BeaconNode) {.
raises: [Defect, CatchableError].} =
rpcServer.rpc("get_v1_beacon_genesis") do () -> RpcBeaconGenesis:
return (
genesis_time: getStateField(node.dag.headState.data, genesis_time),
genesis_validators_root:
getStateField(node.dag.headState.data, genesis_validators_root),
genesis_fork_version: node.dag.cfg.GENESIS_FORK_VERSION
)
rpcServer.rpc("get_v1_beacon_states_root") do (stateId: string) -> Eth2Digest:
withStateForStateId(stateId):
return stateRoot
rpcServer.rpc("get_v1_beacon_states_fork") do (stateId: string) -> Fork:
withStateForStateId(stateId):
return getStateField(stateData.data, fork)
rpcServer.rpc("get_v1_beacon_states_finality_checkpoints") do (
stateId: string) -> RpcBeaconStatesFinalityCheckpoints:
withStateForStateId(stateId):
return (previous_justified:
getStateField(stateData.data, previous_justified_checkpoint),
current_justified:
getStateField(stateData.data, current_justified_checkpoint),
finalized: getStateField(stateData.data, finalized_checkpoint))
rpcServer.rpc("get_v1_beacon_states_stateId_validators") do (
stateId: string, validatorIds: Option[seq[string]],
status: Option[seq[string]]) -> seq[RpcBeaconStatesValidators]:
var vquery: ValidatorQuery
var squery: StatusQuery
let current_epoch = getStateField(node.dag.headState.data, slot).epoch
template statusCheck(status, statusQuery, vstatus, current_epoch): bool =
if status.isNone():
true
else:
if vstatus in squery.statset:
true
else:
false
var res: seq[RpcBeaconStatesValidators]
withStateForStateId(stateId):
if status.isSome:
let sqres = createStatusQuery(status.get())
if sqres.isErr:
raise newException(CatchableError, sqres.error)
squery = sqres.get()
if validatorIds.isSome:
let vqres = createIdQuery(validatorIds.get())
if vqres.isErr:
raise newException(CatchableError, vqres.error)
vquery = vqres.get()
if validatorIds.isNone():
for index, validator in getStateField(stateData.data, validators).pairs():
let sres = validator.getStatus(current_epoch)
if sres.isOk:
let vstatus = sres.get()
let includeFlag = statusCheck(status, squery, vstatus,
current_epoch)
if includeFlag:
res.add((validator: validator,
index: uint64(index),
status: vstatus,
balance: getStateField(stateData.data, balances)[index]))
else:
for index in vquery.ids:
if index < lenu64(getStateField(stateData.data, validators)):
let validator = getStateField(stateData.data, validators)[index]
let sres = validator.getStatus(current_epoch)
if sres.isOk:
let vstatus = sres.get()
let includeFlag = statusCheck(status, squery, vstatus,
current_epoch)
if includeFlag:
vquery.keyset.excl(validator.pubkey)
res.add((validator: validator,
index: uint64(index),
status: vstatus,
balance: getStateField(stateData.data, balances)[index]))
for index, validator in getStateField(stateData.data, validators).pairs():
if validator.pubkey in vquery.keyset:
let sres = validator.getStatus(current_epoch)
if sres.isOk:
let vstatus = sres.get()
let includeFlag = statusCheck(status, squery, vstatus,
current_epoch)
if includeFlag:
res.add((validator: validator,
index: uint64(index),
status: vstatus,
balance: getStateField(stateData.data, balances)[index]))
return res
rpcServer.rpc("get_v1_beacon_states_stateId_validators_validatorId") do (
stateId: string, validatorId: string) -> RpcBeaconStatesValidators:
let current_epoch = getStateField(node.dag.headState.data, slot).epoch
let vqres = createIdQuery([validatorId])
if vqres.isErr:
raise newException(CatchableError, vqres.error)
let vquery = vqres.get()
withStateForStateId(stateId):
if len(vquery.ids) > 0:
let index = vquery.ids[0]
if index < lenu64(getStateField(stateData.data, validators)):
let validator = getStateField(stateData.data, validators)[index]
let sres = validator.getStatus(current_epoch)
if sres.isOk:
return (validator: validator, index: uint64(index),
status: sres.get(),
balance: getStateField(stateData.data, balances)[index])
else:
raise newException(CatchableError, "Incorrect validator's state")
else:
for index, validator in getStateField(stateData.data, validators).pairs():
if validator.pubkey in vquery.keyset:
let sres = validator.getStatus(current_epoch)
if sres.isOk:
return (validator: validator, index: uint64(index),
status: sres.get(),
balance: getStateField(stateData.data, balances)[index])
else:
raise newException(CatchableError, "Incorrect validator's state")
rpcServer.rpc("get_v1_beacon_states_stateId_validator_balances") do (
stateId: string, validatorsId: Option[seq[string]]) -> seq[RpcBalance]:
var res: seq[RpcBalance]
withStateForStateId(stateId):
if validatorsId.isNone():
for index, value in getStateField(stateData.data, balances).pairs():
let balance = (index: uint64(index), balance: value)
res.add(balance)
else:
let vqres = createIdQuery(validatorsId.get())
if vqres.isErr:
raise newException(CatchableError, vqres.error)
var vquery = vqres.get()
for index in vquery.ids:
if index < lenu64(getStateField(stateData.data, validators)):
let validator = getStateField(stateData.data, validators)[index]
vquery.keyset.excl(validator.pubkey)
let balance = (index: uint64(index),
balance: getStateField(stateData.data, balances)[index])
res.add(balance)
for index, validator in getStateField(stateData.data, validators).pairs():
if validator.pubkey in vquery.keyset:
let balance = (index: uint64(index),
balance: getStateField(stateData.data, balances)[index])
res.add(balance)
return res
rpcServer.rpc("get_v1_beacon_states_stateId_committees_epoch") do (
stateId: string, epoch: Option[uint64], index: Option[uint64],
slot: Option[uint64]) -> seq[RpcBeaconStatesCommittees]:
withStateForStateId(stateId):
proc getCommittee(slot: Slot,
index: CommitteeIndex): RpcBeaconStatesCommittees =
let vals = get_beacon_committee(
stateData.data, slot, index, cache).mapIt(it.uint64)
return (index: index.uint64, slot: slot.uint64, validators: vals)
proc forSlot(slot: Slot, res: var seq[RpcBeaconStatesCommittees]) =
let committees_per_slot =
get_committee_count_per_slot(stateData.data, slot.epoch, cache)
if index.isNone:
for committee_index in 0'u64..<committees_per_slot:
res.add(getCommittee(slot, committee_index.CommitteeIndex))
else:
if index.get() < committees_per_slot:
res.add(getCommittee(slot, CommitteeIndex(index.get())))
var res: seq[RpcBeaconStatesCommittees]
let qepoch =
if epoch.isNone:
compute_epoch_at_slot(getStateField(stateData.data, slot))
else:
Epoch(epoch.get())
if slot.isNone:
for i in 0 ..< SLOTS_PER_EPOCH:
forSlot(compute_start_slot_at_epoch(qepoch) + i, res)
else:
forSlot(Slot(slot.get()), res)
return res
rpcServer.rpc("get_v1_beacon_headers") do (
slot: Option[uint64], parent_root: Option[string]) ->
seq[RpcBeaconHeaders]:
unimplemented()
rpcServer.rpc("get_v1_beacon_headers_blockId") do (
blockId: string) ->
tuple[canonical: bool, header: SignedBeaconBlockHeader]:
let bd = node.getBlockDataFromBlockId(blockId)
# TODO check for Altair blocks and fail, because /v1/
let tsbb = bd.data.phase0Block
static: doAssert tsbb.signature is TrustedSig and
sizeof(ValidatorSig) == sizeof(tsbb.signature)
result.header.signature = cast[ValidatorSig](tsbb.signature)
result.header.message.slot = tsbb.message.slot
result.header.message.proposer_index = tsbb.message.proposer_index
result.header.message.parent_root = tsbb.message.parent_root
result.header.message.state_root = tsbb.message.state_root
result.header.message.body_root = tsbb.message.body.hash_tree_root()
result.canonical = bd.refs.isAncestorOf(node.dag.head)
rpcServer.rpc("post_v1_beacon_blocks") do (blck: phase0.SignedBeaconBlock) -> int:
if not(node.syncManager.inProgress):
raise newException(CatchableError,
"Beacon node is currently syncing, try again later.")
let head = node.dag.head
if head.slot >= blck.message.slot:
# TODO altair-transition, but not immediate testnet-priority to detect
# Altair and fail, since /v1/ doesn't support Altair
node.network.broadcastBeaconBlock(ForkedSignedBeaconBlock.init(blck))
# The block failed validation, but was successfully broadcast anyway.
# It was not integrated into the beacon node's database.
return 202
else:
let res = await proposeSignedBlock(
node, head, AttachedValidator(),
ForkedSignedBeaconBlock.init(blck))
if res == head:
# TODO altair-transition, but not immediate testnet-priority
node.network.broadcastBeaconBlock(ForkedSignedBeaconBlock.init(blck))
# The block failed validation, but was successfully broadcast anyway.
# It was not integrated into the beacon node''s database.
return 202
else:
# The block was validated successfully and has been broadcast.
# It has also been integrated into the beacon node's database.
return 200
rpcServer.rpc("get_v1_beacon_blocks_blockId") do (
blockId: string) -> phase0.TrustedSignedBeaconBlock:
# TODO detect Altair and fail: /v1/ APIs don't support Altair
return node.getBlockDataFromBlockId(blockId).data.phase0Block
rpcServer.rpc("get_v1_beacon_blocks_blockId_root") do (
blockId: string) -> Eth2Digest:
# TODO detect Altair and fail: /v1/ APIs don't support Altair
return node.getBlockDataFromBlockId(blockId).data.phase0Block.message.state_root
rpcServer.rpc("get_v1_beacon_blocks_blockId_attestations") do (
blockId: string) -> seq[TrustedAttestation]:
# TODO detect Altair and fail: /v1/ APIs don't support Altair
return node.getBlockDataFromBlockId(blockId).data.phase0Block.message.body.attestations.asSeq
rpcServer.rpc("get_v1_beacon_pool_attestations") do (
slot: Option[uint64], committee_index: Option[uint64]) ->
seq[RpcAttestation]:
var res: seq[RpcAttestation]
let qslot =
if slot.isSome():
some(Slot(slot.get()))
else:
none[Slot]()
let qindex =
if committee_index.isSome():
some(CommitteeIndex(committee_index.get()))
else:
none[CommitteeIndex]()
for item in node.attestationPool[].attestations(qslot, qindex):
let atuple = (
aggregation_bits: "0x" & ncrutils.toHex(item.aggregation_bits.bytes),
data: item.data,
signature: item.signature
)
res.add(atuple)
return res
rpcServer.rpc("post_v1_beacon_pool_attestations") do (
attestation: Attestation) -> bool:
let res = await node.sendAttestation(attestation)
return res.isOk()
rpcServer.rpc("get_v1_beacon_pool_attester_slashings") do (
) -> seq[AttesterSlashing]:
var res: seq[AttesterSlashing]
if isNil(node.exitPool):
return res
let length = len(node.exitPool.attester_slashings)
res = newSeqOfCap[AttesterSlashing](length)
for item in node.exitPool.attester_slashings.items():
res.add(item)
return res
rpcServer.rpc("post_v1_beacon_pool_attester_slashings") do (
slashing: AttesterSlashing) -> bool:
let res = node.sendAttesterSlashing(slashing)
return res.isOk()
rpcServer.rpc("get_v1_beacon_pool_proposer_slashings") do (
) -> seq[ProposerSlashing]:
var res: seq[ProposerSlashing]
if isNil(node.exitPool):
return res
let length = len(node.exitPool.proposer_slashings)
res = newSeqOfCap[ProposerSlashing](length)
for item in node.exitPool.proposer_slashings.items():
res.add(item)
return res
rpcServer.rpc("post_v1_beacon_pool_proposer_slashings") do (
slashing: ProposerSlashing) -> bool:
let res = node.sendProposerSlashing(slashing)
return res.isOk()
rpcServer.rpc("get_v1_beacon_pool_voluntary_exits") do (
) -> seq[SignedVoluntaryExit]:
var res: seq[SignedVoluntaryExit]
if isNil(node.exitPool):
return res
let length = len(node.exitPool.voluntary_exits)
res = newSeqOfCap[SignedVoluntaryExit](length)
for item in node.exitPool.voluntary_exits.items():
res.add(item)
return res
rpcServer.rpc("post_v1_beacon_pool_voluntary_exits") do (
exit: SignedVoluntaryExit) -> bool:
let res = node.sendVoluntaryExit(exit)
return res.isOk()