diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 19dd6a80d..27cec20d1 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -161,6 +161,10 @@ type desc: "A directory containing validator keystores" name: "validators-dir" .}: Option[InputDir] + validatorsSource* {. + desc: "Remote Web3Signer URL that will be used as a source of validators" + name: "validators-source"}: string + secretsDirFlag* {. desc: "A directory containing validator keystore passwords" name: "secrets-dir" .}: Option[InputDir] @@ -875,6 +879,10 @@ type desc: "A directory containing validator keystores" name: "validators-dir" .}: Option[InputDir] + validatorsSource* {. + desc: "Remote Web3Signer URL that will be used as a source of validators" + name: "validators-source"}: string + secretsDirFlag* {. desc: "A directory containing validator keystore passwords" name: "secrets-dir" .}: Option[InputDir] diff --git a/beacon_chain/nimbus_validator_client.nim b/beacon_chain/nimbus_validator_client.nim index edadc582b..946fec1d6 100644 --- a/beacon_chain/nimbus_validator_client.nim +++ b/beacon_chain/nimbus_validator_client.nim @@ -90,6 +90,9 @@ proc initValidators(vc: ValidatorClientRef): Future[bool] {.async.} = var duplicates: seq[ValidatorPubKey] for keystore in listLoadableKeystores(vc.config, vc.keystoreCache): vc.addValidator(keystore) + let dynamicKeystores = await queryValidatorsSource(vc.config) + for keystore in dynamicKeystores: + vc.addValidator(keystore) return true proc initClock(vc: ValidatorClientRef): Future[BeaconClock] {.async.} = diff --git a/beacon_chain/spec/eth2_apis/rest_remote_signer_calls.nim b/beacon_chain/spec/eth2_apis/rest_remote_signer_calls.nim index 027636970..d8fbad12c 100644 --- a/beacon_chain/spec/eth2_apis/rest_remote_signer_calls.nim +++ b/beacon_chain/spec/eth2_apis/rest_remote_signer_calls.nim @@ -78,6 +78,11 @@ proc getKeys*(): RestResponse[Web3SignerKeysResponse] {. meth: MethodGet, accept: "application/json" .} ## https://consensys.github.io/web3signer/web3signer-eth2.html#tag/Public-Key +proc getKeysPlain*(): RestPlainResponse {. + rest, endpoint: "/api/v1/eth2/publicKeys", + meth: MethodGet, accept: "application/json" .} + ## https://consensys.github.io/web3signer/web3signer-eth2.html#tag/Public-Key + proc signDataPlain*(identifier: ValidatorPubKey, body: Web3SignerRequest): RestPlainResponse {. rest, endpoint: "/api/v1/eth2/sign/{identifier}", diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index b68e46fd9..1e075f9ca 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -130,7 +130,7 @@ type Local, Remote RemoteKeystoreFlag* {.pure.} = enum - IgnoreSSLVerification + IgnoreSSLVerification, DynamicKeystore HttpHostUri* = distinct Uri diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index 3804acaee..65d3b8db1 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -117,8 +117,8 @@ proc getValidator*(validators: auto, proc addValidators*(node: BeaconNode) = info "Loading validators", validatorsDir = node.config.validatorsDir(), keystore_cache_available = not(isNil(node.keystoreCache)) - let - epoch = node.currentSlot().epoch + let epoch = node.currentSlot().epoch + for keystore in listLoadableKeystores(node.config, node.keystoreCache): let data = withState(node.dag.headState): @@ -132,7 +132,33 @@ proc addValidators*(node: BeaconNode) = keystore.pubkey, index, epoch) gasLimit = node.consensusManager[].getGasLimit(keystore.pubkey) - v = node.attachedValidators[].addValidator(keystore, feeRecipient, gasLimit) + v = node.attachedValidators[].addValidator(keystore, feeRecipient, + gasLimit) + v.updateValidator(data) + + let dynamicStores = + try: + waitFor(queryValidatorsSource(node.config)) + except CatchableError as exc: + warn "Unexpected error happens while polling validator's source", + error = $exc.name, reason = $exc.msg + default(seq[KeystoreData]) + + for keystore in dynamicStores: + let + data = + withState(node.dag.headState): + getValidator(forkyState.data.validators.asSeq(), keystore.pubkey) + index = + if data.isSome(): + Opt.some(data.get().index) + else: + Opt.none(ValidatorIndex) + feeRecipient = + node.consensusManager[].getFeeRecipient(keystore.pubkey, index, epoch) + gasLimit = node.consensusManager[].getGasLimit(keystore.pubkey) + v = node.attachedValidators[].addValidator(keystore, feeRecipient, + gasLimit) v.updateValidator(data) proc getValidator*(node: BeaconNode, idx: ValidatorIndex): Opt[AttachedValidator] = diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index 3fcd97004..2ecb41242 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -627,6 +627,67 @@ proc existsKeystore(keystoreDir: string, return true false +proc queryValidatorsSource*(config: AnyConf): Future[seq[KeystoreData]] {. + async.} = + var keystores: seq[KeystoreData] + if len(config.validatorsSource) == 0: + return keystores + + let + httpFlags: HttpClientFlags = {} + prestoFlags = {RestClientFlag.CommaSeparatedArray} + socketFlags = {SocketFlags.TcpNoDelay} + client = + block: + let res = RestClientRef.new(config.validatorsSource, prestoFlags, + httpFlags, socketFlags = socketFlags) + if res.isErr(): + # TODO keep trying in case of temporary network failure + warn "Unable to resolve validator's source distributed signer " & + "address", remote_url = config.validatorsSource + return keystores + else: + res.get() + keys = + try: + let response = await getKeysPlain(client) + if response.status != 200: + warn "Remote validator's source responded with error", + error = response.status + return keystores + + let res = decodeBytes(Web3SignerKeysResponse, response.data, + response.contentType) + if res.isErr(): + warn "Unable to obtain validator's source response", + reason = res.error + return keystores + + res.get() + except RestError as exc: + warn "Unable to poll validator's source", reason = $exc.msg + return keystores + except CancelledError as exc: + debug "The polling of validator's source was interrupted" + raise exc + except CatchableError as exc: + warn "Unexpected error occured while polling validator's source", + error = $exc.name, reason = $exc.msg + return keystores + + for pubkey in keys: + keystores.add(KeystoreData( + kind: KeystoreKind.Remote, + handle: FileLockHandle(opened: false), + pubkey: pubkey, + remotes: @[RemoteSignerInfo( + url: HttpHostUri(parseUri(config.validatorsSource)), + pubkey: pubkey)], + flags: {RemoteKeystoreFlag.DynamicKeystore}, + remoteType: RemoteSignerType.Web3Signer)) + + keystores + iterator listLoadableKeys*(validatorsDir, secretsDir: string, keysMask: set[KeystoreKind]): CookedPubKey = try: @@ -1244,8 +1305,8 @@ proc saveKeystore*( proc importKeystore*(pool: var ValidatorPool, validatorsDir: string, - keystore: RemoteKeystore): ImportResult[KeystoreData] - {.raises: [].} = + keystore: RemoteKeystore): ImportResult[KeystoreData] {. + raises: [].} = let publicKey = keystore.pubkey keyName = publicKey.fsName @@ -1253,9 +1314,9 @@ proc importKeystore*(pool: var ValidatorPool, # We check `publicKey`. let cookedKey = publicKey.load().valueOr: - return err( - AddValidatorFailure.init(AddValidatorStatus.failed, - "Invalid validator's public key")) + return err( + AddValidatorFailure.init(AddValidatorStatus.failed, + "Invalid validator's public key")) # We check `publicKey` in memory storage first. if publicKey in pool: diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index a11bb096a..f4bd0a9be 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -146,12 +146,16 @@ proc addRemoteValidator(pool: var ValidatorPool, keystore: KeystoreData, activationEpoch: FAR_FUTURE_EPOCH, ) pool.validators[v.pubkey] = v - notice "Remote validator attached", - pubkey = v.pubkey, - validator = shortLog(v), - remote_signer = $keystore.remotes, - initial_fee_recipient = feeRecipient.toHex(), - initial_gas_limit = gasLimit + if RemoteKeystoreFlag.DynamicKeystore in keystore.flags: + notice "Dynamic remote validator attached", pubkey = v.pubkey, + validator = shortLog(v), remote_signer = $keystore.remotes, + initial_fee_recipient = feeRecipient.toHex(), + initial_gas_limit = gasLimit + else: + notice "Remote validator attached", pubkey = v.pubkey, + validator = shortLog(v), remote_signer = $keystore.remotes, + initial_fee_recipient = feeRecipient.toHex(), + initial_gas_limit = gasLimit validators.set(pool.count().int64)