- work towards more REST API endpoints being implemented

- testnets can now be launched with a separate validator client - make altona SCRIPT_PARAMS="--separateVC"
- reverted the ctrl+C signal handler code reuse - not necessary for the VC anyway (default is good enough)
- added a bit more logging in the VC
- removed unnecessary code in the VC - connect() just parses the address & port...
- fixed a couple more VC issues - when fetching the duties for an epoch fails on the BN side ==> the VC shouldn't be left in a broken state
- documented the currently supported json-rpc endpoints
- added more checks on the BN side for the API - bounds-checking the requests & also checking if the BN itself is synced
- other cleanup

currently a local sim doesn't finalize, but participation in the altona network with a separate VC is painless and works just as well as with in-process validators in a BN
This commit is contained in:
Viktor Kirilov 2020-07-08 13:11:22 +03:00
parent fc8502c54e
commit 1482b0430d
10 changed files with 528 additions and 145 deletions

View File

@ -1223,7 +1223,14 @@ programMain:
var node = waitFor BeaconNode.init(rng, config)
ctrlCHandling: status = BeaconNodeStatus.Stopping
## Ctrl+C handling
proc controlCHandler() {.noconv.} =
when defined(windows):
# workaround for https://github.com/nim-lang/Nim/issues/4057
setupForeignThreadGc()
info "Shutting down after having received SIGINT"
status = BeaconNodeStatus.Stopping
setControlCHook(controlCHandler)
when hasPrompt:
initPrompt(node)

View File

@ -44,16 +44,6 @@ proc setupMainProc*(logLevel: string) =
stderr.write "Invalid value for --log-level. " & err.msg
quit 1
template ctrlCHandling*(extraCode: untyped) =
## Ctrl+C handling
proc controlCHandler() {.noconv.} =
when defined(windows):
# workaround for https://github.com/nim-lang/Nim/issues/4057
setupForeignThreadGc()
info "Shutting down after having received SIGINT"
extraCode
setControlCHook(controlCHandler)
template makeBannerAndConfig*(clientId: string, ConfType: type): untyped =
let
version = clientId & "\p" & copyrights & "\p\p" &

View File

@ -12,6 +12,55 @@ proc get_v1_beacon_states_root(stateId: string): Eth2Digest
# TODO stateId is part of the REST path
proc get_v1_beacon_states_fork(stateId: string): Fork
# TODO stateId is part of the REST path
proc get_v1_beacon_states_finality_checkpoints(
stateId: string): BeaconStatesFinalityCheckpointsTuple
# TODO stateId is part of the REST path
proc get_v1_beacon_states_stateId_validators(
stateId: string, validatorIds: seq[string],
status: string): seq[BeaconStatesValidatorsTuple]
# TODO stateId and validatorId are part of the REST path
proc get_v1_beacon_states_stateId_validators_validatorId(
stateId: string, validatorId: string): BeaconStatesValidatorsTuple
# TODO stateId and epoch are part of the REST path
proc get_v1_beacon_states_stateId_committees_epoch(stateId: string,
epoch: uint64, index: uint64, slot: uint64): seq[BeaconStatesCommitteesTuple]
proc get_v1_beacon_headers(slot: uint64, parent_root: Eth2Digest): seq[BeaconHeadersTuple]
# TODO blockId is part of the REST path
proc get_v1_beacon_headers_blockId(blockId: string):
tuple[canonical: bool, header: SignedBeaconBlockHeader]
# TODO blockId is part of the REST path
proc get_v1_beacon_blocks_blockId(blockId: string): SignedBeaconBlock
# TODO blockId is part of the REST path
proc get_v1_beacon_blocks_blockId_root(blockId: string): Eth2Digest
# TODO blockId is part of the REST path
proc get_v1_beacon_blocks_blockId_attestations(blockId: string): seq[Attestation]
# TODO POST /v1/beacon/pool/attester_slashings
# TODO GET /v1/beacon/pool/attester_slashings
# TODO POST /v1/beacon/pool/proposer_slashings
# TODO GET /v1/beacon/pool/proposer_slashings
# TODO POST /v1/beacon/pool/voluntary_exits
# TODO GET /v1/beacon/pool/voluntary_exits
# TODO POST /v1/beacon/pool/attestations
# TODO GET /v1/beacon/pool/attestations
proc post_v1_beacon_pool_attestations(attestation: Attestation): bool
proc get_v1_config_fork_schedule(): seq[tuple[epoch: uint64, version: Version]]
# TODO stateId is part of the REST path
proc get_v1_debug_beacon_states_stateId(stateId: string): BeaconState
# TODO: delete old stuff

View File

@ -21,3 +21,23 @@ type
genesis_time: uint64
genesis_validators_root: Eth2Digest
genesis_fork_version: Version
BeaconStatesFinalityCheckpointsTuple* = tuple
previous_justified: Checkpoint
current_justified: Checkpoint
finalized: Checkpoint
BeaconStatesValidatorsTuple* = tuple
validator: Validator
status: string
balance: uint64
BeaconStatesCommitteesTuple* = tuple
index: uint64
slot: uint64
validators: seq[uint64] # each object in the sequence should have an index field...
BeaconHeadersTuple* = tuple
root: Eth2Digest
canonical: bool
header: SignedBeaconBlockHeader

View File

@ -10,19 +10,16 @@ import
# calls that return a bool are actually without a return type in the main REST API
# spec but nim-json-rpc requires that all RPC calls have a return type.
proc post_v1_beacon_pool_attestations(attestation: Attestation): bool
proc get_v1_validator_block(slot: Slot, graffiti: Eth2Digest, randao_reveal: ValidatorSig): BeaconBlock
# TODO slot is part of the REST path
proc get_v1_validator_blocks(slot: Slot, graffiti: Eth2Digest, randao_reveal: ValidatorSig): BeaconBlock
proc post_v1_validator_block(body: SignedBeaconBlock): bool
proc post_v1_beacon_blocks(body: SignedBeaconBlock): bool
proc get_v1_validator_attestation_data(slot: Slot, committee_index: CommitteeIndex): AttestationData
proc get_v1_validator_attestation(slot: Slot, committee_index: CommitteeIndex): AttestationData
# TODO at the time of writing (10.06.2020) the API specifies this call to have a hash of
# the attestation data instead of the object itself but we also need the slot.. see here:
# https://docs.google.com/spreadsheets/d/1kVIx6GvzVLwNYbcd-Fj8YUlPf4qGrWUlS35uaTnIAVg/edit?disco=AAAAGh7r_fQ
proc get_v1_validator_aggregate_attestation(attestation_data: AttestationData): Attestation
proc get_v1_validator_aggregate_and_proof(attestation_data: AttestationData): Attestation
proc post_v1_validator_aggregate_and_proof(payload: SignedAggregateAndProof): bool

View File

@ -7,10 +7,10 @@
import
# Standard library
tables, strutils, parseutils,
tables, strutils, parseutils, sequtils,
# Nimble packages
stew/[objects],
stew/[byteutils, objects],
chronos, metrics, json_rpc/[rpcserver, jsonmarshal],
chronicles,
@ -27,86 +27,281 @@ type
logScope: topics = "valapi"
proc toBlockSlot(blckRef: BlockRef): BlockSlot =
blckRef.atSlot(blckRef.slot)
proc parseRoot(str: string): Eth2Digest =
return Eth2Digest(data: hexToByteArray[32](str))
proc parsePubkey(str: string): ValidatorPubKey =
let pubkeyRes = fromHex(ValidatorPubKey, str)
if pubkeyRes.isErr:
raise newException(CatchableError, "Not a valid public key")
return pubkeyRes[]
proc doChecksAndGetCurrentHead(node: BeaconNode, slot: Slot): BlockRef =
result = node.blockPool.head.blck
if not node.isSynced(result):
raise newException(CatchableError, "Cannot fulfill request until ndoe is synced")
# TODO for now we limit the requests arbitrarily by up to 2 epochs into the future
if result.slot + uint64(2 * SLOTS_PER_EPOCH) < slot:
raise newException(CatchableError, "Requesting way ahead of the current head")
proc doChecksAndGetCurrentHead(node: BeaconNode, epoch: Epoch): BlockRef =
node.doChecksAndGetCurrentHead(epoch.compute_start_slot_at_epoch)
# TODO currently this function throws if the validator isn't found - is this OK?
proc getValidatorInfoFromValidatorId(
state: BeaconState,
current_epoch: Epoch,
validatorId: string,
status = ""):
Option[BeaconStatesValidatorsTuple] =
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 status notin allowedStatuses:
raise newException(CatchableError, "Invalid status requested")
let validator = if validatorId.startsWith("0x"):
let pubkey = parsePubkey(validatorId)
let idx = state.validators.asSeq.findIt(it.pubKey == pubkey)
if idx == -1:
raise newException(CatchableError, "Could not find validator")
state.validators[idx]
else:
var valIdx: BiggestUInt
if parseBiggestUInt(validatorId, valIdx) != validatorId.len:
raise newException(CatchableError, "Not a valid index")
if state.validators.len >= valIdx.int:
raise newException(CatchableError, "Index out of bounds")
state.validators[valIdx]
# time to determine the status of the validator - the code mimics
# whatever is detailed here: https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ
let actual_status = if validator.activation_epoch > current_epoch:
# pending
if validator.activation_eligibility_epoch == FAR_FUTURE_EPOCH:
"pending_initialized"
else:
# validator.activation_eligibility_epoch < FAR_FUTURE_EPOCH:
"pending_queued"
elif validator.activation_epoch <= current_epoch and
current_epoch < validator.exit_epoch:
# active
if validator.exit_epoch == FAR_FUTURE_EPOCH:
"active_ongoing"
elif not validator.slashed:
# validator.exit_epoch < FAR_FUTURE_EPOCH
"active_exiting"
else:
# validator.exit_epoch < FAR_FUTURE_EPOCH and validator.slashed:
"active_slashed"
elif validator.exit_epoch <= current_epoch and
current_epoch < validator.withdrawable_epoch:
# exited
if not validator.slashed:
"exited_unslashed"
else:
# validator.slashed
"exited_slashed"
elif validator.withdrawable_epoch <= current_epoch:
# withdrawal
if validator.effective_balance != 0:
"withdrawal_possible"
else:
# validator.effective_balance == 0
"withdrawal_done"
else:
raise newException(CatchableError, "Invalid validator status")
# if the requested status doesn't match the actual status
if status != "" and status notin actual_status:
return none(BeaconStatesValidatorsTuple)
return some((validator: validator, status: actual_status,
balance: validator.effective_balance))
proc getBlockSlotFromString(node: BeaconNode, slot: string): BlockSlot =
var parsed: BiggestUInt
if parseBiggestUInt(slot, parsed) != slot.len:
raise newException(CatchableError, "Not a valid slot number")
let head = node.doChecksAndGetCurrentHead(parsed.Slot)
return head.atSlot(parsed.Slot)
proc getBlockDataFromBlockId(node: BeaconNode, blockId: string): BlockData =
result = case blockId:
of "head":
node.blockPool.get(node.blockPool.head.blck)
of "genesis":
node.blockPool.get(node.blockPool.tail)
of "finalized":
node.blockPool.get(node.blockPool.finalizedHead.blck)
else:
if blockId.startsWith("0x"):
let blckRoot = parseRoot(blockId)
let blockData = node.blockPool.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.blockPool.get(blockSlot.blck)
proc stateIdToBlockSlot(node: BeaconNode, stateId: string): BlockSlot =
result = case stateId:
of "head":
node.blockPool.head.blck.toBlockSlot()
of "genesis":
node.blockPool.tail.toBlockSlot()
of "finalized":
node.blockPool.finalizedHead
of "justified":
node.blockPool.justifiedState.blck.toBlockSlot()
else:
if stateId.startsWith("0x"):
let blckRoot = parseRoot(stateId)
let blckRef = node.blockPool.getRef(blckRoot)
if blckRef.isNil:
raise newException(CatchableError, "Block not found")
blckRef.toBlockSlot()
else:
node.getBlockSlotFromString(stateId)
# TODO Probably the `beacon` ones should be defined elsewhere...?
proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) =
template withStateForSlot(stateId: string, body: untyped): untyped =
var res: BiggestInt
if parseBiggestInt(stateId, res) == stateId.len:
raise newException(CatchableError, "Not a valid slot number")
let head = node.updateHead()
let blockSlot = head.atSlot(res.Slot)
node.blockPool.withState(node.blockPool.tmpState, blockSlot):
template withStateForStateId(stateId: string, body: untyped): untyped =
node.blockPool.withState(node.blockPool.tmpState,
node.stateIdToBlockSlot(stateId)):
body
rpcServer.rpc("get_v1_beacon_genesis") do () -> BeaconGenesisTuple:
debug "get_v1_beacon_genesis"
return (genesis_time: node.blockPool.headState.data.data.genesis_time,
genesis_validators_root:
node.blockPool.headState.data.data.genesis_validators_root,
genesis_fork_version: Version(GENESIS_FORK_VERSION))
rpcServer.rpc("get_v1_beacon_states_root") do (stateId: string) -> Eth2Digest:
debug "get_v1_beacon_states_root", stateId = stateId
# TODO do we need to call node.updateHead() before using headState?
result = case stateId:
of "head":
node.blockPool.headState.blck.root
of "genesis":
node.blockPool.headState.data.data.genesis_validators_root
of "finalized":
node.blockPool.headState.data.data.finalized_checkpoint.root
of "justified":
node.blockPool.headState.data.data.current_justified_checkpoint.root
else:
if stateId.startsWith("0x"):
# TODO not sure if `fromHex` is the right thing here...
# https://github.com/ethereum/eth2.0-APIs/issues/37#issuecomment-638566144
# we return whatever was passed to us (this is a nonsense request)
fromHex(Eth2Digest, stateId[2..<stateId.len]) # skip first 2 chars
else:
withStateForSlot(stateId):
hashedState.root
withStateForStateId(stateId):
return hashedState.root
rpcServer.rpc("get_v1_beacon_states_fork") do (stateId: string) -> Fork:
debug "get_v1_beacon_states_fork", stateId = stateId
result = case stateId:
of "head":
node.blockPool.headState.data.data.fork
of "genesis":
Fork(previous_version: Version(GENESIS_FORK_VERSION),
current_version: Version(GENESIS_FORK_VERSION),
epoch: GENESIS_EPOCH)
of "finalized":
node.blockPool.withState(node.blockPool.tmpState, node.blockPool.finalizedHead):
state.fork
of "justified":
node.blockPool.justifiedState.data.data.fork
else:
if stateId.startsWith("0x"):
# TODO not sure if `fromHex` is the right thing here...
# https://github.com/ethereum/eth2.0-APIs/issues/37#issuecomment-638566144
let blckRoot = fromHex(Eth2Digest, stateId[2..<stateId.len]) # skip first 2 chars
let blckRef = node.blockPool.getRef(blckRoot)
if blckRef.isNil:
raise newException(CatchableError, "Block not found")
let blckSlot = blckRef.atSlot(blckRef.slot)
node.blockPool.withState(node.blockPool.tmpState, blckSlot):
state.fork
withStateForStateId(stateId):
return state.fork
rpcServer.rpc("get_v1_beacon_states_finality_checkpoints") do (
stateId: string) -> BeaconStatesFinalityCheckpointsTuple:
withStateForStateId(stateId):
return (previous_justified: state.previous_justified_checkpoint,
current_justified: state.current_justified_checkpoint,
finalized: state.finalized_checkpoint)
rpcServer.rpc("get_v1_beacon_states_stateId_validators") do (
stateId: string, validatorIds: seq[string],
status: string) -> seq[BeaconStatesValidatorsTuple]:
let current_epoch = get_current_epoch(node.blockPool.headState.data.data)
withStateForStateId(stateId):
for validatorId in validatorIds:
let res = state.getValidatorInfoFromValidatorId(
current_epoch, validatorId, status)
if res.isSome():
result.add(res.get())
rpcServer.rpc("get_v1_beacon_states_stateId_validators_validatorId") do (
stateId: string, validatorId: string) -> BeaconStatesValidatorsTuple:
let current_epoch = get_current_epoch(node.blockPool.headState.data.data)
withStateForStateId(stateId):
let res = state.getValidatorInfoFromValidatorId(current_epoch, validatorId)
if res.isNone:
# TODO should we raise here? Maybe this is different from the array case...
raise newException(CatchableError, "Validator status differs")
return res.get()
rpcServer.rpc("get_v1_beacon_states_stateId_committees_epoch") do (
stateId: string, epoch: uint64, index: uint64, slot: uint64) ->
seq[BeaconStatesCommitteesTuple]:
withStateForStateId(stateId):
var cache = get_empty_per_epoch_cache() # TODO is this OK?
proc getCommittee(slot: Slot, index: CommitteeIndex): BeaconStatesCommitteesTuple =
let vals = get_beacon_committee(state, slot, index, cache).mapIt(it.uint64)
return (index: index.uint64, slot: slot.uint64, validators: vals)
proc forSlot(slot: Slot, res: var seq[BeaconStatesCommitteesTuple]) =
if index == 0: # TODO this means if the parameter is missing (its optional)
let committees_per_slot = get_committee_count_at_slot(state, slot)
for committee_index in 0'u64..<committees_per_slot:
res.add(getCommittee(slot, committee_index.CommitteeIndex))
else:
withStateForSlot(stateId):
state.fork
res.add(getCommittee(slot, index.CommitteeIndex))
if slot == 0: # TODO this means if the parameter is missing (its optional)
for i in 0 ..< SLOTS_PER_EPOCH:
forSlot((compute_start_slot_at_epoch(epoch.Epoch).int + i).Slot, result)
else:
forSlot(slot.Slot, result)
rpcServer.rpc("get_v1_beacon_headers") do (
slot: uint64, parent_root: Eth2Digest) -> seq[BeaconHeadersTuple]:
# @mratsim: I'm adding a toposorted iterator that returns all blocks from last finalization to all heads in the dual fork choice PR @viktor
# filterIt(dag.blocks.values(), it.blck.slot == slot_of_interest)
# maybe usesBlockPool.heads ??? or getBlockRange ???
# https://discordapp.com/channels/613988663034118151/614014714590134292/726095138484518912
discard # raise newException(CatchableError, "Not implemented") # cannot compile...
rpcServer.rpc("get_v1_beacon_headers_blockId") do (
blockId: string) -> tuple[canonical: bool, header: SignedBeaconBlockHeader]:
let bd = node.getBlockDataFromBlockId(blockId)
let tsbb = bd.data
result.header.signature.blob = tsbb.signature.data
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.blockPool.head.blck)
rpcServer.rpc("get_v1_beacon_blocks_blockId") do (
blockId: string) -> TrustedSignedBeaconBlock:
return node.getBlockDataFromBlockId(blockId).data
rpcServer.rpc("get_v1_beacon_blocks_blockId_root") do (
blockId: string) -> Eth2Digest:
return node.getBlockDataFromBlockId(blockId).data.message.state_root
rpcServer.rpc("get_v1_beacon_blocks_blockId_attestations") do (
blockId: string) -> seq[TrustedAttestation]:
return node.getBlockDataFromBlockId(blockId).data.message.body.attestations.asSeq
rpcServer.rpc("post_v1_beacon_pool_attestations") do (
attestation: Attestation) -> bool:
node.sendAttestation(attestation)
return true
rpcServer.rpc("get_v1_validator_blocks") do (
rpcServer.rpc("get_v1_config_fork_schedule") do (
) -> seq[tuple[epoch: uint64, version: Version]]:
discard # raise newException(CatchableError, "Not implemented") # cannot compile...
rpcServer.rpc("get_v1_debug_beacon_states_stateId") do (
stateId: string) -> BeaconState:
withStateForStateId(stateId):
return state
rpcServer.rpc("get_v1_validator_block") do (
slot: Slot, graffiti: Eth2Digest, randao_reveal: ValidatorSig) -> BeaconBlock:
debug "get_v1_validator_blocks", slot = slot
let head = node.updateHead()
debug "get_v1_validator_block", slot = slot
let head = node.doChecksAndGetCurrentHead(slot)
let proposer = node.blockPool.getProposer(head, slot)
if proposer.isNone():
raise newException(CatchableError, "could not retrieve block for slot: " & $slot)
@ -118,18 +313,13 @@ proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) =
raise newException(CatchableError, "could not retrieve block for slot: " & $slot)
return res.message.get()
rpcServer.rpc("post_v1_beacon_blocks") do (body: SignedBeaconBlock) -> bool:
debug "post_v1_beacon_blocks",
rpcServer.rpc("post_v1_validator_block") do (body: SignedBeaconBlock) -> bool:
debug "post_v1_validator_block",
slot = body.message.slot,
prop_idx = body.message.proposer_index
let head = node.doChecksAndGetCurrentHead(body.message.slot)
let head = node.updateHead()
if head.slot >= body.message.slot:
warn "Skipping proposal, have newer head already",
headSlot = shortLog(head.slot),
headBlockRoot = shortLog(head.root),
slot = shortLog(body.message.slot),
cat = "fastforward"
raise newException(CatchableError,
"Proposal is for a past slot: " & $body.message.slot)
if head == await proposeSignedBlock(node, head, AttachedValidator(),
@ -137,26 +327,29 @@ proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) =
raise newException(CatchableError, "Could not propose block")
return true
rpcServer.rpc("get_v1_validator_attestation_data") do (
rpcServer.rpc("get_v1_validator_attestation") do (
slot: Slot, committee_index: CommitteeIndex) -> AttestationData:
let head = node.updateHead()
let attestationHead = head.atSlot(slot)
node.blockPool.withState(node.blockPool.tmpState, attestationHead):
debug "get_v1_validator_attestation", slot = slot
let head = node.doChecksAndGetCurrentHead(slot)
node.blockPool.withState(node.blockPool.tmpState, head.atSlot(slot)):
return makeAttestationData(state, slot, committee_index.uint64, blck.root)
rpcServer.rpc("get_v1_validator_aggregate_attestation") do (
rpcServer.rpc("get_v1_validator_aggregate_and_proof") do (
attestation_data: AttestationData)-> Attestation:
debug "get_v1_validator_aggregate_attestation"
debug "get_v1_validator_aggregate_and_proof"
raise newException(CatchableError, "Not implemented")
rpcServer.rpc("post_v1_validator_aggregate_and_proof") do (
payload: SignedAggregateAndProof) -> bool:
node.network.broadcast(node.topicAggregateAndProofs, payload)
return true
debug "post_v1_validator_aggregate_and_proof"
raise newException(CatchableError, "Not implemented")
rpcServer.rpc("post_v1_validator_duties_attester") do (
epoch: Epoch, public_keys: seq[ValidatorPubKey]) -> seq[AttesterDuties]:
debug "post_v1_validator_duties_attester", epoch = epoch
let head = node.updateHead()
let head = node.doChecksAndGetCurrentHead(epoch)
let attestationHead = head.atSlot(compute_start_slot_at_epoch(epoch))
node.blockPool.withState(node.blockPool.tmpState, attestationHead):
for pubkey in public_keys:
@ -174,7 +367,8 @@ proc installValidatorApiHandlers*(rpcServer: RpcServer, node: BeaconNode) =
rpcServer.rpc("get_v1_validator_duties_proposer") do (
epoch: Epoch) -> seq[ValidatorPubkeySlotPair]:
debug "get_v1_validator_duties_proposer", epoch = epoch
let head = node.updateHead()
let head = node.doChecksAndGetCurrentHead(epoch)
for i in 0 ..< SLOTS_PER_EPOCH:
let currSlot = (compute_start_slot_at_epoch(epoch).int + i).Slot
let proposer = node.blockPool.getProposer(head, currSlot)

View File

@ -10,7 +10,7 @@ import
os, strutils, json, times,
# Nimble packages
stew/shims/[tables, macros],
stew/byteutils, stew/shims/[tables, macros],
chronos, confutils, metrics, json_rpc/[rpcclient, jsonmarshal],
chronicles,
blscurve, json_serialization/std/[options, sets, net],
@ -44,18 +44,6 @@ type
attestationsForEpoch: Table[Epoch, Table[Slot, seq[AttesterDuties]]]
beaconGenesis: BeaconGenesisTuple
proc connectToBN(vc: ValidatorClient) {.gcsafe, async.} =
while true:
try:
await vc.client.connect($vc.config.rpcAddress, Port(vc.config.rpcPort))
info "Connected to BN",
port = vc.config.rpcPort,
address = vc.config.rpcAddress
return
except CatchableError as err:
warn "Could not connect to the BN - retrying!", err = err.msg
await sleepAsync(chronos.seconds(1)) # 1 second before retrying
template attemptUntilSuccess(vc: ValidatorClient, body: untyped) =
while true:
try:
@ -63,9 +51,11 @@ template attemptUntilSuccess(vc: ValidatorClient, body: untyped) =
break
except CatchableError as err:
warn "Caught an unexpected error", err = err.msg
waitFor vc.connectToBN()
waitFor sleepAsync(chronos.seconds(1)) # 1 second before retrying
proc getValidatorDutiesForEpoch(vc: ValidatorClient, epoch: Epoch) {.gcsafe, async.} =
info "Getting validator duties for epoch", epoch = epoch
let proposals = await vc.client.get_v1_validator_duties_proposer(epoch)
# update the block proposal duties this VC should do during this epoch
vc.proposalsForCurrentEpoch.clear()
@ -79,29 +69,35 @@ proc getValidatorDutiesForEpoch(vc: ValidatorClient, epoch: Epoch) {.gcsafe, asy
validatorPubkeys.add key
proc getAttesterDutiesForEpoch(epoch: Epoch) {.gcsafe, async.} =
let attestations = await vc.client.post_v1_validator_duties_attester(
epoch, validatorPubkeys)
# make sure there's an entry
if not vc.attestationsForEpoch.contains epoch:
vc.attestationsForEpoch.add(epoch, Table[Slot, seq[AttesterDuties]]())
let attestations = await vc.client.post_v1_validator_duties_attester(
epoch, validatorPubkeys)
for a in attestations:
if vc.attestationsForEpoch[epoch].hasKeyOrPut(a.slot, @[a]):
vc.attestationsForEpoch[epoch][a.slot].add(a)
# clear both for the current epoch and the next because a change of
# fork could invalidate the attester duties even the current epoch
vc.attestationsForEpoch.clear()
await getAttesterDutiesForEpoch(epoch)
# obtain the attestation duties this VC should do during the next epoch
# TODO currently we aren't making use of this but perhaps we should
await getAttesterDutiesForEpoch(epoch + 1)
# also get the attestation duties for the current epoch if missing
if not vc.attestationsForEpoch.contains epoch:
await getAttesterDutiesForEpoch(epoch)
# cleanup old epoch attestation duties
vc.attestationsForEpoch.del(epoch - 1)
# TODO handle subscriptions to beacon committees for both the next epoch and
# for the current if missing (beacon_committee_subscriptions from the REST api)
# for now we will get the fork each time we update the validator duties for each epoch
# TODO should poll occasionally `/v1/config/fork_schedule`
vc.fork = await vc.client.get_v1_beacon_states_fork("head")
var numAttestationsForEpoch = 0
for _, dutiesForSlot in vc.attestationsForEpoch[epoch]:
numAttestationsForEpoch += dutiesForSlot.len
info "Got validator duties for epoch",
num_proposals = vc.proposalsForCurrentEpoch.len,
num_attestations = numAttestationsForEpoch
proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, async.} =
let
@ -135,18 +131,22 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a
let public_key = vc.proposalsForCurrentEpoch[slot]
let validator = vc.attachedValidators.validators[public_key]
info "Proposing block", slot = slot, public_key = public_key
let randao_reveal = validator.genRandaoReveal(
vc.fork, vc.beaconGenesis.genesis_validators_root, slot)
var graffiti: Eth2Digest
graffiti.data[0..<5] = toBytes("quack")
var newBlock = SignedBeaconBlock(
message: await vc.client.get_v1_validator_blocks(slot, Eth2Digest(), randao_reveal)
message: await vc.client.get_v1_validator_block(slot, graffiti, randao_reveal)
)
let blockRoot = hash_tree_root(newBlock.message)
newBlock.signature = await validator.signBlockProposal(
vc.fork, vc.beaconGenesis.genesis_validators_root, slot, blockRoot)
discard await vc.client.post_v1_beacon_blocks(newBlock)
discard await vc.client.post_v1_validator_block(newBlock)
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.1/specs/phase0/validator.md#attesting
# A validator should create and broadcast the attestation to the associated
@ -158,11 +158,13 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a
seconds(int64(SECONDS_PER_SLOT)) div 3, slot, "Waiting to send attestations")
# check if we have validators which need to attest on this slot
if vc.attestationsForEpoch[epoch].contains slot:
if vc.attestationsForEpoch.contains(epoch) and
vc.attestationsForEpoch[epoch].contains slot:
for a in vc.attestationsForEpoch[epoch][slot]:
let validator = vc.attachedValidators.validators[a.public_key]
info "Attesting", slot = slot, public_key = a.public_key
let ad = await vc.client.get_v1_validator_attestation_data(slot, a.committee_index)
let validator = vc.attachedValidators.validators[a.public_key]
let ad = await vc.client.get_v1_validator_attestation(slot, a.committee_index)
# TODO I don't like these (u)int64-to-int conversions...
let attestation = await validator.produceAndSignAttestation(
@ -173,7 +175,6 @@ proc onSlotStart(vc: ValidatorClient, lastSlot, scheduledSlot: Slot) {.gcsafe, a
except CatchableError as err:
warn "Caught an unexpected error", err = err.msg, slot = shortLog(slot)
await vc.connectToBN()
let
nextSlotStart = saturate(vc.beaconClock.fromNow(nextSlot))
@ -202,10 +203,6 @@ programMain:
setupMainProc(config.logLevel)
# TODO figure out how to re-enable this without the VCs continuing
# to run when `make eth2_network_simulation` is killed with CTRL+C
#ctrlCHandling: discard
case config.cmd
of VCNoCommand:
debug "Launching validator client",
@ -222,7 +219,10 @@ programMain:
for curr in vc.config.validatorKeys:
vc.attachedValidators.addLocalValidator(curr.toPubKey, curr)
waitFor vc.connectToBN()
waitFor vc.client.connect($vc.config.rpcAddress, Port(vc.config.rpcPort))
info "Connected to BN",
port = vc.config.rpcPort,
address = vc.config.rpcAddress
vc.attemptUntilSuccess:
# init the beacon clock

View File

@ -73,7 +73,7 @@ func getAttachedValidator*(node: BeaconNode,
let validatorKey = state.validators[idx].pubkey
node.attachedValidators.getValidator(validatorKey)
proc isSynced(node: BeaconNode, head: BlockRef): bool =
proc isSynced*(node: BeaconNode, head: BlockRef): bool =
## TODO This function is here as a placeholder for some better heurestics to
## determine if we're in sync and should be producing blocks and
## attestations. Generally, the problem is that slot time keeps advancing

View File

@ -19,6 +19,8 @@ Before you can access the API, make sure it's enabled using the RPC flag (`beaco
--rpc-address Listening address of the RPC server.
```
One difference is that currently endpoints that correspond to specific ones from the [spec](https://ethereum.github.io/eth2.0-APIs/) are named weirdly - for example an endpoint such as [`getGenesis`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getGenesis) is currently named `get_v1_beacon_genesis` which would map 1:1 to the actual REST path in the future - verbose but unambiguous.
## Beacon Node API
### getBeaconHead
@ -53,10 +55,104 @@ curl -d '{"jsonrpc":"2.0","id":"id","method":"getChainHead","params":[] }' -H 'C
curl -d '{"jsonrpc":"2.0","id":"id","method":"getNetworkEnr","params":[] }' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_beacon_genesis`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getGenesis)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_beacon_genesis","params":[],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_beacon_states_root`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getStateRoot)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_beacon_states_root","params":["finalized"],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_beacon_states_fork`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getStateFork)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_beacon_states_fork","params":["finalized"],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_beacon_states_finality_checkpoints`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getStateFinalityCheckpoints)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_beacon_states_finality_checkpoints","params":["finalized"],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_beacon_states_stateId_validators`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getStateValidators)
### [`get_v1_beacon_states_stateId_validators_validatorId`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getStateValidator)
### [`get_v1_beacon_states_stateId_committees_epoch`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getEpochCommittees)
### [`get_v1_beacon_headers`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getBlockHeaders)
### [`get_v1_beacon_headers_blockId`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getBlockHeader)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_beacon_headers_blockId","params":["finalized"],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_beacon_blocks_blockId`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getBlock)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_beacon_blocks_blockId","params":["finalized"],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_beacon_blocks_blockId_root`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getBlockRoot)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_beacon_blocks_blockId_root","params":["finalized"],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_beacon_blocks_blockId_attestations`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/getBlockAttestations)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_beacon_blocks_blockId_attestations","params":["finalized"],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`post_v1_beacon_pool_attestations`](https://ethereum.github.io/eth2.0-APIs/#/Beacon/submitPoolAttestations)
## Valdiator API
### [`get_v1_validator_block`](https://ethereum.github.io/eth2.0-APIs/#/ValidatorRequiredApi/produceBlock)
### [`post_v1_validator_block`](https://ethereum.github.io/eth2.0-APIs/#/ValidatorRequiredApi/publishBlock)
### [`get_v1_validator_attestation`](https://ethereum.github.io/eth2.0-APIs/#/ValidatorRequiredApi/produceAttestation)
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_validator_attestation_data","params":[0,3],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_validator_aggregate_and_proof`](https://ethereum.github.io/eth2.0-APIs/#/ValidatorRequiredApi/getAggregatedAttestation)
### [`post_v1_validator_aggregate_and_proof`](https://ethereum.github.io/eth2.0-APIs/#/ValidatorRequiredApi/publishAggregateAndProof)
### [`post_v1_validator_duties_attester`](https://ethereum.github.io/eth2.0-APIs/#/ValidatorRequiredApi/getAttesterDuties)
```
curl -d '{"jsonrpc":"2.0","method":"post_v1_validator_duties_attester","params":[1,["a7a0502eae26043d1ac39a39457a6cdf68fae2055d89c7dc59092c25911e4ee55c4e7a31ade61c39480110a393be28e8","a1826dd94cd96c48a81102d316a2af4960d19ca0b574ae5695f2d39a88685a43997cef9a5c26ad911847674d20c46b75"]],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### [`get_v1_validator_duties_proposer`](https://ethereum.github.io/eth2.0-APIs/#/ValidatorRequiredApi/getProposerDuties)
```
curl -d '{"jsonrpc":"2.0","id":"id","method":"get_v1_validator_duties_proposer","params":[1] }' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
## Config
### [`get_v1_config_fork_schedule`](https://ethereum.github.io/eth2.0-APIs/#/Config/getForkSchedule)
## Administrative / Debug API
### `get_v1_debug_beacon_states_stateId` - returns an entire `BeaconState` object for the specified `stateId`
```
curl -d '{"jsonrpc":"2.0","method":"get_v1_debug_beacon_states_stateId","params":["head"],"id":1}' -H 'Content-Type: application/json' localhost:9190 -s | jq
```
### getNodeVersion
Show version of the software

View File

@ -40,10 +40,11 @@ proc updateTestnetsRepo(allTestnetsDir, buildDir: string) =
proc makePrometheusConfig(nodeID, baseMetricsPort: int, dataDir: string) =
# macOS may not have gnu-getopts installed and in the PATH
execIgnoringExitCode &"""./scripts/make_prometheus_config.sh --nodes """ & $(1 + nodeID) & &""" --base-metrics-port {baseMetricsPort} --config-file "{dataDir}/prometheus.yml""""
execIgnoringExitCode &"""./scripts/make_prometheus_config.sh --nodes """ & $(1 + nodeID) & &""" --base-metrics-port {baseMetricsPort} --config-file "{dataDir}/prometheus.yml" """
proc buildNode(nimFlags, preset, beaconNodeBinary: string) =
exec &"""nim c {nimFlags} -d:"const_preset={preset}" -o:"{beaconNodeBinary}" beacon_chain/beacon_node.nim"""
proc buildBinary(nimFlags, preset, binary, nimFile: string) =
if binary != "":
exec &"""nim c {nimFlags} -d:"const_preset={preset}" -o:"{binary}" beacon_chain/{nimFile}"""
proc becomeValidator(validatorsDir, beaconNodeBinary, secretsDir, depositContractOpt, privateGoerliKey: string,
becomeValidatorOnly: bool) =
@ -75,8 +76,8 @@ proc becomeValidator(validatorsDir, beaconNodeBinary, secretsDir, depositContrac
echo "\nDeposit sent, wait for confirmation then press enter to continue"
discard readLineFromStdin()
proc runNode(dataDir, beaconNodeBinary, bootstrapFileOpt, depositContractOpt,
genesisFileOpt, extraBeaconNodeOptions: string,
proc runNode(dataDir, beaconNodeBinary, validatorClientBinary, bootstrapFileOpt,
depositContractOpt, genesisFileOpt, extraBeaconNodeOptions: string,
basePort, nodeID, baseMetricsPort, baseRpcPort: int,
printCmdOnly: bool) =
let logLevel = getEnv("LOG_LEVEL")
@ -99,21 +100,36 @@ proc runNode(dataDir, beaconNodeBinary, bootstrapFileOpt, depositContractOpt,
echo &"cd {dataDir}; exec {cmd}"
else:
cd dataDir
mkDir dataDir & "/empty_dummy_folder"
# if launching a VC as well - send the BN looking nowhere for validators/secrets
# TODO use `start` (or something else) on windows (instead of `&`) for the 2 processes
let vcCommand = if validatorClientBinary == "": "" else:
&"""
--secrets-dir={dataDir}/empty_dummy_folder
--validators-dir={dataDir}/empty_dummy_folder
& {validatorClientBinary}
--rpc-port={baseRpcPort + nodeID}
--data-dir="{dataDir}"
{logLevelOpt}
"""
cmd = replace(&"""{beaconNodeBinary}
--data-dir="{dataDir}"
--dump
--web3-url={web3Url}
--tcp-port=""" & $(basePort + nodeID) & &"""
--udp-port=""" & $(basePort + nodeID) & &"""
--tcp-port={basePort + nodeID}
--udp-port={basePort + nodeID}
--metrics
--metrics-port=""" & $(baseMetricsPort + nodeID) & &"""
--metrics-port={baseMetricsPort + nodeID}
--rpc
--rpc-port=""" & $(baseRpcPort + nodeID) & &"""
--rpc-port={baseRpcPort + nodeID}
{bootstrapFileOpt}
{logLevelOpt}
{depositContractOpt}
{genesisFileOpt}
{extraBeaconNodeOptions}""", "\n", " ")
{extraBeaconNodeOptions}
{vcCommand} """, "\n", " ")
execIgnoringExitCode cmd
cli do (skipGoerliKey {.
@ -157,6 +173,9 @@ cli do (skipGoerliKey {.
runOnly {.
desc: "Just run it." .} = false,
separateVC {.
desc: "use a separate validator client process." .} = false,
printCmdOnly {.
desc: "Just print the commands (suitable for passing to 'eval'; might replace current shell)." .} = false,
@ -220,6 +239,11 @@ cli do (skipGoerliKey {.
doAssert specVersion in ["v0.11.3", "v0.12.1"]
if defined(windows) and separateVC:
# TODO use `start` (or something else) on windows (instead of `&`) for the 2 processes
echo "Cannot use a separate validator client process on Windows! (remove --separateVC)"
quit(1)
let
dataDirName = testnetName.replace("/", "_")
.replace("(", "_")
@ -228,12 +252,17 @@ cli do (skipGoerliKey {.
validatorsDir = dataDir / "validators"
secretsDir = dataDir / "secrets"
beaconNodeBinary = buildDir / "beacon_node_" & dataDirName
# using a separate VC is disabled on windows until we find a substitute for `&`
validatorClientBinary = if separateVC: buildDir / "validator_client_" & dataDirName else: ""
var
nimFlags = &"-d:chronicles_log_level=TRACE " & getEnv("NIM_PARAMS")
nimFlagsBN = &"-d:chronicles_log_level=TRACE " & getEnv("NIM_PARAMS")
nimFlagsVC = nimFlagsBN
if writeLogFile:
# write the logs to a file
nimFlags.add """ -d:"chronicles_sinks=textlines,json[file(nbc""" & staticExec("date +\"%Y%m%d%H%M%S\"") & """.log)]" """
let logFileNimParams = """ -d:"chronicles_sinks=textlines,json[file(placeholder_""" & staticExec("date +\"%Y%m%d%H%M%S\"") & """.log)]" """
nimFlagsBN.add logFileNimParams.replace("placeholder_", "nbc_bn_")
nimFlagsVC.add logFileNimParams.replace("placeholder_", "nbc_vc_")
let depositContractFile = testnetDir / depositContractFileName
if system.fileExists(depositContractFile):
@ -248,7 +277,8 @@ cli do (skipGoerliKey {.
if doBuild:
makePrometheusConfig(nodeID, baseMetricsPort, dataDir)
buildNode(nimFlags, preset, beaconNodeBinary)
buildBinary(nimFlagsBN, preset, beaconNodeBinary, "beacon_node.nim")
buildBinary(nimFlagsVC, preset, validatorClientBinary, "validator_client.nim")
if doBecomeValidator and depositContractOpt.len > 0 and not system.dirExists(validatorsDir):
becomeValidator(validatorsDir, beaconNodeBinary, secretsDir, depositContractOpt, privateGoerliKey, becomeValidatorOnly)
@ -256,7 +286,7 @@ cli do (skipGoerliKey {.
echo &"extraBeaconNodeOptions = '{extraBeaconNodeOptions}'"
if doRun:
runNode(dataDir, beaconNodeBinary, bootstrapFileOpt, depositContractOpt,
genesisFileOpt, extraBeaconNodeOptions,
runNode(dataDir, beaconNodeBinary, validatorClientBinary, bootstrapFileOpt,
depositContractOpt, genesisFileOpt, extraBeaconNodeOptions,
basePort, nodeID, baseMetricsPort, baseRpcPort,
printCmdOnly)