diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index e679efb34..ca3987e07 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -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) diff --git a/beacon_chain/nimbus_binary_common.nim b/beacon_chain/nimbus_binary_common.nim index 2eb67ab28..16e9a0063 100644 --- a/beacon_chain/nimbus_binary_common.nim +++ b/beacon_chain/nimbus_binary_common.nim @@ -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" & diff --git a/beacon_chain/spec/eth2_apis/beacon_callsigs.nim b/beacon_chain/spec/eth2_apis/beacon_callsigs.nim index b4e082cc2..45aa86cfb 100644 --- a/beacon_chain/spec/eth2_apis/beacon_callsigs.nim +++ b/beacon_chain/spec/eth2_apis/beacon_callsigs.nim @@ -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 diff --git a/beacon_chain/spec/eth2_apis/callsigs_types.nim b/beacon_chain/spec/eth2_apis/callsigs_types.nim index d90ffce9b..14388f826 100644 --- a/beacon_chain/spec/eth2_apis/callsigs_types.nim +++ b/beacon_chain/spec/eth2_apis/callsigs_types.nim @@ -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 diff --git a/beacon_chain/spec/eth2_apis/validator_callsigs.nim b/beacon_chain/spec/eth2_apis/validator_callsigs.nim index 18b35f44f..7fcfc2a4b 100644 --- a/beacon_chain/spec/eth2_apis/validator_callsigs.nim +++ b/beacon_chain/spec/eth2_apis/validator_callsigs.nim @@ -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 diff --git a/beacon_chain/validator_api.nim b/beacon_chain/validator_api.nim index 44aca3cfb..e00ae8047 100644 --- a/beacon_chain/validator_api.nim +++ b/beacon_chain/validator_api.nim @@ -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.. 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.. 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.. 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) diff --git a/beacon_chain/validator_client.nim b/beacon_chain/validator_client.nim index 5dd298393..a3b9eeb68 100644 --- a/beacon_chain/validator_client.nim +++ b/beacon_chain/validator_client.nim @@ -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 diff --git a/beacon_chain/validator_duties.nim b/beacon_chain/validator_duties.nim index 793842a7e..c20e34c01 100644 --- a/beacon_chain/validator_duties.nim +++ b/beacon_chain/validator_duties.nim @@ -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 diff --git a/docs/the_nimbus_book/src/api.md b/docs/the_nimbus_book/src/api.md index c1dacf8de..ff22c8caf 100644 --- a/docs/the_nimbus_book/src/api.md +++ b/docs/the_nimbus_book/src/api.md @@ -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 diff --git a/scripts/connect_to_testnet.nims b/scripts/connect_to_testnet.nims index 551a01a38..5178792b6 100644 --- a/scripts/connect_to_testnet.nims +++ b/scripts/connect_to_testnet.nims @@ -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)