From 35bf03a3fbb6a55c52911479760c9bbb69e7c2cc Mon Sep 17 00:00:00 2001 From: zah Date: Fri, 13 Oct 2023 15:42:00 +0300 Subject: [PATCH] Add the --verifying-web3-signer-url configuration option (#5504) --- beacon_chain/conf.nim | 34 +++++++++++-- beacon_chain/nimbus_beacon_node.nim | 2 +- beacon_chain/nimbus_validator_client.nim | 4 +- beacon_chain/spec/keystore.nim | 22 ++++++++ .../validator_client/duties_service.nim | 9 ++-- beacon_chain/validators/beacon_validators.nim | 8 +-- .../validators/keystore_management.nim | 27 +++++++--- beacon_chain/validators/validator_pool.nim | 6 +-- docs/the_nimbus_book/src/options.md | 3 ++ docs/the_nimbus_book/src/web3signer.md | 2 + tests/test_validator_pool.nim | 51 ++++++++++--------- 11 files changed, 119 insertions(+), 49 deletions(-) diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 226cb00be..c0be92f2d 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -127,6 +127,10 @@ type Poll = "poll" Event = "event" + Web3SignerUrl* = object + url*: Uri + provenBlockProperties*: seq[string] # empty if this is not a verifying Web3Signer + BeaconNodeConf* = object configFile* {. desc: "Loads the configuration from a TOML file" @@ -164,7 +168,15 @@ type desc: "A directory containing validator keystores" name: "validators-dir" .}: Option[InputDir] - web3signers* {. + verifyingWeb3Signers* {. + desc: "Remote Web3Signer URL that will be used as a source of validators" + name: "verifying-web3-signer-url" .}: seq[Uri] + + provenBlockProperties* {. + desc: "The field path of a block property that will be sent for verification to the verifying Web3Signer (for example \".execution_payload.fee_recipient\")" + name: "proven-block-property" .}: seq[string] + + web3Signers* {. desc: "Remote Web3Signer URL that will be used as a source of validators" name: "web3-signer-url" .}: seq[Uri] @@ -896,15 +908,23 @@ type desc: "A directory containing validator keystores" name: "validators-dir" .}: Option[InputDir] - web3signers* {. + verifyingWeb3Signers* {. desc: "Remote Web3Signer URL that will be used as a source of validators" - name: "web3-signer-url" .}: seq[Uri] + name: "verifying-web3-signer-url" .}: seq[Uri] + + provenBlockProperties* {. + desc: "The field path of a block property that will be sent for verification to the verifying Web3Signer (for example \".execution_payload.fee_recipient\")" + name: "proven-block-property" .}: seq[string] web3signerUpdateInterval* {. desc: "Number of seconds between validator list updates" name: "web3-signer-update-interval" defaultValue: 3600 .}: Natural + web3Signers* {. + desc: "Remote Web3Signer URL that will be used as a source of validators" + name: "web3-signer-url" .}: seq[Uri] + secretsDirFlag* {. desc: "A directory containing validator keystore passwords" name: "secrets-dir" .}: Option[InputDir] @@ -1287,6 +1307,14 @@ func runAsService*(config: BeaconNodeConf): bool = else: false +func web3SignerUrls*(conf: AnyConf): seq[Web3SignerUrl] = + for url in conf.web3signers: + result.add Web3SignerUrl(url: url) + + for url in conf.verifyingWeb3signers: + result.add Web3SignerUrl(url: url, + provenBlockProperties: conf.provenBlockProperties) + template writeValue*(writer: var JsonWriter, value: TypedInputFile|InputFile|InputDir|OutPath|OutDir|OutFile) = writer.writeValue(string value) diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 2436e143b..8c77c85f2 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -1732,7 +1732,7 @@ proc run(node: BeaconNode) {.raises: [CatchableError].} = waitFor node.updateGossipStatus(wallSlot) - for web3signerUrl in node.config.web3signers: + for web3signerUrl in node.config.web3SignerUrls: # TODO # The current strategy polls all remote signers independently # from each other which may lead to some race conditions of diff --git a/beacon_chain/nimbus_validator_client.nim b/beacon_chain/nimbus_validator_client.nim index 7e5da53d8..cb51e5d20 100644 --- a/beacon_chain/nimbus_validator_client.nim +++ b/beacon_chain/nimbus_validator_client.nim @@ -98,7 +98,7 @@ proc initGenesis(vc: ValidatorClientRef): Future[RestGenesis] {.async.} = dec(counter) return melem -proc addValidatorsFromWeb3Signer(vc: ValidatorClientRef, web3signerUrl: Uri) {.async.} = +proc addValidatorsFromWeb3Signer(vc: ValidatorClientRef, web3signerUrl: Web3SignerUrl) {.async.} = let res = await queryValidatorsSource(web3signerUrl) if res.isOk(): let dynamicKeystores = res.get() @@ -111,7 +111,7 @@ proc initValidators(vc: ValidatorClientRef): Future[bool] {.async.} = vc.addValidator(keystore) let web3signerValidatorsFuts = mapIt( - vc.config.web3signers, + vc.config.web3SignerUrls, vc.addValidatorsFromWeb3Signer(it)) # We use `allFutures` because all failures are already reported as diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index e6ca7a0ae..68be780f9 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -725,6 +725,26 @@ template writeValue*(w: var JsonWriter, value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) = writeJsonHexString(w.stream, distinctBase value) +func parseProvenBlockProperty*(propertyPath: string): Result[ProvenProperty, string] = + if propertyPath == ".execution_payload.fee_recipient": + ok ProvenProperty( + path: propertyPath, + bellatrixIndex: some GeneralizedIndex(401), + capellaIndex: some GeneralizedIndex(401), + denebIndex: some GeneralizedIndex(801)) + elif propertyPath == ".graffiti": + ok ProvenProperty( + path: propertyPath, + # TODO: graffiti is present since genesis, so the correct index in the early + # forks can be supplied here + bellatrixIndex: some GeneralizedIndex(18), + capellaIndex: some GeneralizedIndex(18), + denebIndex: some GeneralizedIndex(18)) + else: + err("Keystores with proven properties different than " & + "`.execution_payload.fee_recipient` and `.graffiti` " & + "require a more recent version of Nimbus") + proc readValue*(reader: var JsonReader, value: var RemoteKeystore) {.raises: [SerializationError, IOError].} = var @@ -830,6 +850,8 @@ proc readValue*(reader: var JsonReader, value: var RemoteKeystore) prop.capellaIndex = some GeneralizedIndex(401) prop.denebIndex = some GeneralizedIndex(801) elif prop.path == ".graffiti": + # TODO: graffiti is present since genesis, so the correct index in the early + # forks can be supplied here prop.bellatrixIndex = some GeneralizedIndex(18) prop.capellaIndex = some GeneralizedIndex(18) prop.denebIndex = some GeneralizedIndex(18) diff --git a/beacon_chain/validator_client/duties_service.nim b/beacon_chain/validator_client/duties_service.nim index f75888288..fe5a6a431 100644 --- a/beacon_chain/validator_client/duties_service.nim +++ b/beacon_chain/validator_client/duties_service.nim @@ -601,7 +601,7 @@ proc validatorIndexLoop(service: DutiesServiceRef) {.async.} = await service.waitForNextSlot() proc dynamicValidatorsLoop*(service: DutiesServiceRef, - web3signerUrl: Uri, + web3signerUrl: Web3SignerUrl, intervalInSeconds: int) {.async.} = let vc = service.client doAssert(intervalInSeconds > 0) @@ -624,7 +624,7 @@ proc dynamicValidatorsLoop*(service: DutiesServiceRef, let keystores = res.get() debug "Web3Signer has been polled for validators", keystores_found = len(keystores), - web3signer_url = web3signerUrl + web3signer_url = web3signerUrl.url vc.attachedValidators.updateDynamicValidators(web3signerUrl, keystores, addValidatorProc) @@ -710,11 +710,12 @@ proc mainLoop(service: DutiesServiceRef) {.async.} = nil dynamicFuts = if vc.config.web3signerUpdateInterval > 0: - mapIt(vc.config.web3signers, + mapIt(vc.config.web3SignerUrls, service.dynamicValidatorsLoop(it, vc.config.web3signerUpdateInterval)) else: debug "Dynamic validators update loop disabled" @[] + web3SignerUrls = vc.config.web3SignerUrls while true: # This loop could look much more nicer/better, when @@ -746,7 +747,7 @@ proc mainLoop(service: DutiesServiceRef) {.async.} = for i in 0 ..< dynamicFuts.len: checkAndRestart(DynamicValidatorsLoop, dynamicFuts[i], service.dynamicValidatorsLoop( - vc.config.web3signers[i], + web3SignerUrls[i], vc.config.web3signerUpdateInterval)) false except CancelledError: diff --git a/beacon_chain/validators/beacon_validators.nim b/beacon_chain/validators/beacon_validators.nim index c122cef82..02e216bf3 100644 --- a/beacon_chain/validators/beacon_validators.nim +++ b/beacon_chain/validators/beacon_validators.nim @@ -115,7 +115,7 @@ proc getValidator*(validators: auto, Opt.some ValidatorAndIndex(index: ValidatorIndex(idx), validator: validators[idx]) -proc addValidatorsFromWeb3Signer(node: BeaconNode, web3signerUrl: Uri, epoch: Epoch) {.async.} = +proc addValidatorsFromWeb3Signer(node: BeaconNode, web3signerUrl: Web3SignerUrl, epoch: Epoch) {.async.} = let dynamicStores = try: let res = await queryValidatorsSource(web3signerUrl) @@ -173,7 +173,7 @@ proc addValidators*(node: BeaconNode) = # user-visible warnings in `queryValidatorsSource`. # We don't consider them fatal because the Web3Signer may be experiencing # a temporary hiccup that will be resolved later. - waitFor allFutures(mapIt(node.config.web3signers, + waitFor allFutures(mapIt(node.config.web3SignerUrls, node.addValidatorsFromWeb3Signer(it, epoch))) except CatchableError as err: # This should never happen because all errors are handled within @@ -184,7 +184,7 @@ proc addValidators*(node: BeaconNode) = err = err.msg proc pollForDynamicValidators*(node: BeaconNode, - web3signerUrl: Uri, + web3signerUrl: Web3SignerUrl, intervalInSeconds: int) {.async.} = if intervalInSeconds == 0: return @@ -215,7 +215,7 @@ proc pollForDynamicValidators*(node: BeaconNode, let keystores = res.get() debug "Validators source has been polled for validators", keystores_found = len(keystores), - web3signer_url = web3signerUrl + web3signer_url = web3signerUrl.url node.attachedValidators.updateDynamicValidators(web3signerUrl, keystores, addValidatorProc) diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index a9674eae9..241bba84a 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -8,7 +8,7 @@ {.push raises: [].} import - std/[os, unicode], + std/[os, unicode, sequtils], chronicles, chronos, json_serialization, bearssl/rand, serialization, blscurve, eth/common/eth_types, confutils, @@ -28,7 +28,7 @@ from std/wordwrap import wrapWords from zxcvbn import passwordEntropy export - keystore, validator_pool, crypto, rand + keystore, validator_pool, crypto, rand, Web3SignerUrl when defined(windows): import stew/[windows/acl] @@ -632,11 +632,11 @@ proc existsKeystore(keystoreDir: string, return true false -proc queryValidatorsSource*(web3signerUrl: Uri): Future[QueryResult] {.async.} = +proc queryValidatorsSource*(web3signerUrl: Web3SignerUrl): Future[QueryResult] {.async.} = var keystores: seq[KeystoreData] logScope: - web3signer_url = web3signerUrl + web3signer_url = web3signerUrl.url let httpFlags: HttpClientFlags = {} @@ -644,7 +644,7 @@ proc queryValidatorsSource*(web3signerUrl: Uri): Future[QueryResult] {.async.} = socketFlags = {SocketFlags.TcpNoDelay} client = block: - let res = RestClientRef.new($web3signerUrl, prestoFlags, + let res = RestClientRef.new($web3signerUrl.url, prestoFlags, httpFlags, socketFlags = socketFlags) if res.isErr(): warn "Unable to resolve validator's source distributed signer " & @@ -679,16 +679,29 @@ proc queryValidatorsSource*(web3signerUrl: Uri): Future[QueryResult] {.async.} = error = $exc.name, reason = $exc.msg return QueryResult.err($exc.msg) + remoteType = if web3signerUrl.provenBlockProperties.len == 0: + RemoteSignerType.Web3Signer + else: + RemoteSignerType.VerifyingWeb3Signer + + provenBlockProperties = mapIt(web3signerUrl.provenBlockProperties, + block: + parseProvenBlockProperty(it).valueOr: + return QueryResult.err(error)) + for pubkey in keys: keystores.add(KeystoreData( kind: KeystoreKind.Remote, handle: FileLockHandle(opened: false), pubkey: pubkey, remotes: @[RemoteSignerInfo( - url: HttpHostUri(web3signerUrl), + url: HttpHostUri(web3signerUrl.url), pubkey: pubkey)], flags: {RemoteKeystoreFlag.DynamicKeystore}, - remoteType: RemoteSignerType.Web3Signer)) + remoteType: remoteType)) + + if provenBlockProperties.len > 0: + keystores[^1].provenBlockProperties = provenBlockProperties QueryResult.ok(keystores) diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index 8f8428a3b..b83daeff7 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -17,7 +17,7 @@ import ../spec/datatypes/[phase0, altair], ../spec/eth2_apis/[rest_types, eth2_rest_serialization, rest_remote_signer_calls], - ../filepath, + ../filepath, ../conf, ./slashing_protection export @@ -380,7 +380,7 @@ func triggersDoppelganger*( v.isSome() and v[].triggersDoppelganger(epoch) proc updateDynamicValidators*(pool: ref ValidatorPool, - web3signerUrl: Uri, + web3signerUrl: Web3SignerUrl, keystores: openArray[KeystoreData], addProc: AddValidatorProc) = var @@ -400,7 +400,7 @@ proc updateDynamicValidators*(pool: ref ValidatorPool, if keystore.isSome(): # Just update validator's `data` field with new data from keystore. validator.data = keystore.get() - elif validator.data.remotes[0].url == HttpHostUri(web3signerUrl): + elif validator.data.remotes[0].url == HttpHostUri(web3signerUrl.url): # The "dynamic" keystores are guaratneed to not be distributed # so they have a single remote. This code ensures that we are # deleting all previous dynamically obtained keystores which diff --git a/docs/the_nimbus_book/src/options.md b/docs/the_nimbus_book/src/options.md index c1c8ce79e..03c070926 100644 --- a/docs/the_nimbus_book/src/options.md +++ b/docs/the_nimbus_book/src/options.md @@ -33,6 +33,9 @@ The following options are available: --network The Eth2 network to join [=mainnet]. -d, --data-dir The directory where nimbus will store all blockchain data. --validators-dir A directory containing validator keystores. + --verifying-web3-signer-url Remote Web3Signer URL that will be used as a source of validators. + --proven-block-property The field path of a block property that will be sent for verification to the + verifying Web3Signer (for example ".execution_payload.fee_recipient"). --web3-signer-url Remote Web3Signer URL that will be used as a source of validators. --web3-signer-update-interval Number of seconds between validator list updates [=3600]. --secrets-dir A directory containing validator keystore passwords. diff --git a/docs/the_nimbus_book/src/web3signer.md b/docs/the_nimbus_book/src/web3signer.md index 3617dd5aa..1e7d3f39d 100644 --- a/docs/the_nimbus_book/src/web3signer.md +++ b/docs/the_nimbus_book/src/web3signer.md @@ -156,3 +156,5 @@ Since the generalized index of a particular field may change in a hard-fork, in Nimbus automatically computes the generalized index depending on the currently active fork. The remote signer is expected to verify the incoming Merkle proof through the standardized [is_valid_merkle_branch](https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.2/specs/phase0/beacon-chain.md#is_valid_merkle_branch) function by utilizing a similar automatic mapping mechanism for the generalized index. + +You can instruct Nimbus to use the verifying Web3Signer protocol by either supplying the `--verifying-web3-signer` command-line option or by creating a remote keystore file in the format described above. You can use the command-line option `--proven-block-property` once or multiple times to enumerate the properties of the block for which Merkle proofs will be supplied. diff --git a/tests/test_validator_pool.nim b/tests/test_validator_pool.nim index cd371a563..0fdd4cf54 100644 --- a/tests/test_validator_pool.nim +++ b/tests/test_validator_pool.nim @@ -31,7 +31,7 @@ func createDynamic(url: Uri, pubkey: ValidatorPubKey): KeystoreData = flags: {RemoteKeystoreFlag.DynamicKeystore}) const - remoteSignerUrl = parseUri("http://nimbus.team/signer1") + remoteSignerUrl = Web3SignerUrl(url: parseUri("http://nimbus.team/signer1")) func makeValidatorAndIndex( index: ValidatorIndex, activation_epoch: Epoch): Opt[ValidatorAndIndex] = @@ -166,35 +166,36 @@ suite "Validator pool": mapIt(["--web3-signer-url=http://" & $serverAddress], it)) except Exception as exc: raiseAssert exc.msg + web3SignerUrls = config.web3SignerUrls server.start() try: block: testStage = 0 - let res = await queryValidatorsSource(config.web3signers[0]) + let res = await queryValidatorsSource(web3SignerUrls[0]) check: res.isOk() checkResponse( res.get(), [ - createDynamic(remoteSignerUrl, createPubKey(1)), - createDynamic(remoteSignerUrl, createPubKey(2)) + createDynamic(remoteSignerUrl.url, createPubKey(1)), + createDynamic(remoteSignerUrl.url, createPubKey(2)) ]) block: testStage = 1 - let res = await queryValidatorsSource(config.web3signers[0]) + let res = await queryValidatorsSource(web3SignerUrls[0]) check: res.isOk() - checkResponse(res.get(), [createDynamic(remoteSignerUrl, createPubKey(1))]) + checkResponse(res.get(), [createDynamic(remoteSignerUrl.url, createPubKey(1))]) block: testStage = 2 - let res = await queryValidatorsSource(config.web3signers[0]) + let res = await queryValidatorsSource(web3SignerUrls[0]) check: res.isOk() len(res.get()) == 0 block: testStage = 3 - let res = await queryValidatorsSource(config.web3signers[0]) + let res = await queryValidatorsSource(web3SignerUrls[0]) check: res.isErr() finally: @@ -221,7 +222,7 @@ suite "Validator pool": var pool = (ref ValidatorPool)() discard pool[].addValidator(createLocal(createPubKey(1)), fee, gas) discard pool[].addValidator(createRemote(createPubKey(2)), fee, gas) - discard pool[].addValidator(createDynamic(remoteSignerUrl, createPubKey(3)), fee, gas) + discard pool[].addValidator(createDynamic(remoteSignerUrl.url, createPubKey(3)), fee, gas) proc addValidator(data: KeystoreData) {.gcsafe.} = discard pool[].addValidator(data, fee, gas) @@ -232,14 +233,14 @@ suite "Validator pool": expected = [ createLocal(createPubKey(1)), createRemote(createPubKey(2)), - createDynamic(remoteSignerUrl, createPubKey(3)), - createDynamic(remoteSignerUrl, createPubKey(4)), - createDynamic(remoteSignerUrl, createPubKey(5)) + createDynamic(remoteSignerUrl.url, createPubKey(3)), + createDynamic(remoteSignerUrl.url, createPubKey(4)), + createDynamic(remoteSignerUrl.url, createPubKey(5)) ] keystores = [ - createDynamic(remoteSignerUrl, createPubKey(3)), - createDynamic(remoteSignerUrl, createPubKey(4)), - createDynamic(remoteSignerUrl, createPubKey(5)) + createDynamic(remoteSignerUrl.url, createPubKey(3)), + createDynamic(remoteSignerUrl.url, createPubKey(4)), + createDynamic(remoteSignerUrl.url, createPubKey(5)) ] pool.updateDynamicValidators(remoteSignerUrl, keystores, addValidator) pool[].checkPool(expected) @@ -250,10 +251,10 @@ suite "Validator pool": expected = [ createLocal(createPubKey(1)), createRemote(createPubKey(2)), - createDynamic(remoteSignerUrl, createPubKey(3)) + createDynamic(remoteSignerUrl.url, createPubKey(3)) ] keystores = [ - createDynamic(remoteSignerUrl, createPubKey(3)), + createDynamic(remoteSignerUrl.url, createPubKey(3)), ] pool.updateDynamicValidators(remoteSignerUrl, keystores, addValidator) pool[].checkPool(expected) @@ -264,12 +265,12 @@ suite "Validator pool": expected = [ createLocal(createPubKey(1)), createRemote(createPubKey(2)), - createDynamic(remoteSignerUrl, createPubKey(4)), - createDynamic(remoteSignerUrl, createPubKey(5)) + createDynamic(remoteSignerUrl.url, createPubKey(4)), + createDynamic(remoteSignerUrl.url, createPubKey(5)) ] keystores = [ - createDynamic(remoteSignerUrl, createPubKey(4)), - createDynamic(remoteSignerUrl, createPubKey(5)) + createDynamic(remoteSignerUrl.url, createPubKey(4)), + createDynamic(remoteSignerUrl.url, createPubKey(5)) ] pool.updateDynamicValidators(remoteSignerUrl, keystores, addValidator) pool[].checkPool(expected) @@ -280,12 +281,12 @@ suite "Validator pool": expected = [ createLocal(createPubKey(1)), createRemote(createPubKey(2)), - createDynamic(remoteSignerUrl, createPubKey(3)) + createDynamic(remoteSignerUrl.url, createPubKey(3)) ] keystores = [ - createDynamic(remoteSignerUrl, createPubKey(1)), - createDynamic(remoteSignerUrl, createPubKey(2)), - createDynamic(remoteSignerUrl, createPubKey(3)), + createDynamic(remoteSignerUrl.url, createPubKey(1)), + createDynamic(remoteSignerUrl.url, createPubKey(2)), + createDynamic(remoteSignerUrl.url, createPubKey(3)), ] pool.updateDynamicValidators(remoteSignerUrl, keystores, addValidator) pool[].checkPool(expected)