From 28fef70b01fae690c4f266c5edc3299324025ae9 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Tue, 9 May 2023 00:14:27 +0300 Subject: [PATCH] Initial public version of the Verifying Web3Signer functionality * Allow the list of proved properties for web3signer to be configured * Document the Web3Signer setups (regular, distributed and verified) --- AllTests-mainnet.md | 30 +-- beacon_chain/nimbus_signing_node.nim | 7 +- beacon_chain/spec/eth2_apis/rest_types.nim | 2 +- beacon_chain/spec/keystore.nim | 176 +++++++++++---- beacon_chain/spec/mev/bellatrix_mev.nim | 2 +- beacon_chain/spec/mev/capella_mev.nim | 2 +- .../validators/keystore_management.nim | 32 ++- beacon_chain/validators/validator_pool.nim | 145 ++++++------- docs/the_nimbus_book/mkdocs.yml | 8 + docs/the_nimbus_book/src/data-dir.md | 10 +- docs/the_nimbus_book/src/web3signer.md | 148 +++++++++++++ tests/test_remote_keystore.nim | 204 +++++++++++++----- tests/test_signing_node.nim | 81 ++++--- 13 files changed, 598 insertions(+), 249 deletions(-) create mode 100644 docs/the_nimbus_book/src/web3signer.md diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 1212685959..2dcd766d08 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -356,6 +356,16 @@ OK: 4/4 Fail: 0/4 Skip: 0/4 + Voluntary exit signatures OK ``` OK: 8/8 Fail: 0/8 Skip: 0/8 +## Nimbus remote signer/signing test (verifying-web3signer) +```diff ++ Signing BeaconBlock (getBlockSignature(altair)) OK ++ Signing BeaconBlock (getBlockSignature(bellatrix)) OK ++ Signing BeaconBlock (getBlockSignature(capella)) OK ++ Signing BeaconBlock (getBlockSignature(deneb)) OK ++ Signing BeaconBlock (getBlockSignature(phase0)) OK ++ Waiting for signing node (/upcheck) test OK +``` +OK: 6/6 Fail: 0/6 Skip: 0/6 ## Nimbus remote signer/signing test (web3signer) ```diff + Connection timeout test OK @@ -382,16 +392,6 @@ OK: 8/8 Fail: 0/8 Skip: 0/8 + Waiting for signing node (/upcheck) test OK ``` OK: 22/22 Fail: 0/22 Skip: 0/22 -## Nimbus remote signer/signing test (web3signer-diva) -```diff -+ Signing BeaconBlock (getBlockSignature(altair)) OK -+ Signing BeaconBlock (getBlockSignature(bellatrix)) OK -+ Signing BeaconBlock (getBlockSignature(capella)) OK -+ Signing BeaconBlock (getBlockSignature(deneb)) OK -+ Signing BeaconBlock (getBlockSignature(phase0)) OK -+ Waiting for signing node (/upcheck) test OK -``` -OK: 6/6 Fail: 0/6 Skip: 0/6 ## Old database versions [Preset: mainnet] ```diff + pre-1.1.0 OK @@ -420,11 +420,13 @@ OK: 12/12 Fail: 0/12 Skip: 0/12 OK: 1/1 Fail: 0/1 Skip: 0/1 ## Remove keystore testing suite ```diff ++ Many remotes OK ++ Single remote OK ++ Verifying Signer / Many remotes OK ++ Verifying Signer / Single remote OK + vesion 1 OK -+ vesion 2 many remotes OK -+ vesion 2 single remote OK ``` -OK: 3/3 Fail: 0/3 Skip: 0/3 +OK: 5/5 Fail: 0/5 Skip: 0/5 ## Serialization/deserialization [Beacon Node] [Preset: mainnet] ```diff + Deserialization test vectors OK @@ -667,4 +669,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2 OK: 9/9 Fail: 0/9 Skip: 0/9 ---TOTAL--- -OK: 380/385 Fail: 0/385 Skip: 5/385 +OK: 382/387 Fail: 0/387 Skip: 5/387 diff --git a/beacon_chain/nimbus_signing_node.nim b/beacon_chain/nimbus_signing_node.nim index 5009f53591..118475dbeb 100644 --- a/beacon_chain/nimbus_signing_node.nim +++ b/beacon_chain/nimbus_signing_node.nim @@ -1,9 +1,10 @@ -# nimbus_sign_node -# Copyright (c) 2018-2022 Status Research & Development GmbH +# nimbus_signing_node +# Copyright (c) 2018-2023 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. + import std/[tables, os, strutils] import serialization, json_serialization, json_serialization/std/[options, net], @@ -256,7 +257,7 @@ proc installApiHandlers*(node: SigningNodeRef) = let feeRecipientRoot = hash_tree_root(distinctBase( node.config.expectedFeeRecipient.get())) - if not(is_valid_merkle_branch(feeRecipientRoot, proof.merkleProofs, + if not(is_valid_merkle_branch(feeRecipientRoot, proof.proof, log2trunc(proof.index), get_subtree_index(proof.index), blockHeader.body_root)): diff --git a/beacon_chain/spec/eth2_apis/rest_types.nim b/beacon_chain/spec/eth2_apis/rest_types.nim index 368eefee7e..08a75429c0 100644 --- a/beacon_chain/spec/eth2_apis/rest_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_types.nim @@ -568,7 +568,7 @@ type Web3SignerMerkleProof* = object index*: GeneralizedIndex - merkleProofs* {.serializedFieldName: "merkle_proofs".}: seq[Eth2Digest] + proof*: seq[Eth2Digest] Web3SignerRequestKind* {.pure.} = enum AggregationSlot, AggregateAndProof, Attestation, Block, BlockV2, diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index fd3b2b6d6e..e5bf9a328e 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -143,6 +143,18 @@ type ioHandle*: IoLockHandle opened*: bool + RemoteSignerType* {.pure.} = enum + Web3Signer, VerifyingWeb3Signer + + ProvenProperty* = object + path*: string + description*: Option[string] + phase0Index*: Option[GeneralizedIndex] + altairIndex*: Option[GeneralizedIndex] + bellatrixIndex*: Option[GeneralizedIndex] + capellaIndex*: Option[GeneralizedIndex] + denebIndex*: Option[GeneralizedIndex] + KeystoreData* = object version*: uint64 pubkey*: ValidatorPubKey @@ -157,7 +169,11 @@ type flags*: set[RemoteKeystoreFlag] remotes*: seq[RemoteSignerInfo] threshold*: uint32 - remoteType*: RemoteSignerType + case remoteType*: RemoteSignerType + of RemoteSignerType.Web3Signer: + discard + of RemoteSignerType.VerifyingWeb3Signer: + provenBlockProperties*: seq[ProvenProperty] NetKeystore* = object crypto*: Crypto @@ -166,13 +182,14 @@ type uuid*: string version*: int - RemoteSignerType* {.pure.} = enum - Web3Signer, Web3SignerDiva - RemoteKeystore* = object version*: uint64 description*: Option[string] - remoteType*: RemoteSignerType + case remoteType*: RemoteSignerType + of RemoteSignerType.Web3Signer: + discard + of RemoteSignerType.VerifyingWeb3Signer: + provenBlockProperties*: seq[ProvenProperty] pubkey*: ValidatorPubKey flags*: set[RemoteKeystoreFlag] remotes*: seq[RemoteSignerInfo] @@ -599,8 +616,9 @@ proc writeValue*(writer: var JsonWriter, value: RemoteKeystore) case value.remoteType of RemoteSignerType.Web3Signer: writer.writeField("type", "web3signer") - of RemoteSignerType.Web3SignerDiva: - writer.writeField("type", "web3signer-diva") + of RemoteSignerType.VerifyingWeb3Signer: + writer.writeField("type", "verifying-web3signer") + writer.writeField("proven_block_properties", value.provenBlockProperties) if value.description.isSome(): writer.writeField("description", value.description.get()) if RemoteKeystoreFlag.IgnoreSSLVerification in value.flags: @@ -618,7 +636,8 @@ proc readValue*(reader: var JsonReader, value: var RemoteKeystore) description: Option[string] remote: Option[HttpHostUri] remotes: Option[seq[RemoteSignerInfo]] - remoteType: Option[string] + remoteType: Option[RemoteSignerType] + provenBlockProperties: Option[seq[ProvenProperty]] ignoreSslVerification: Option[bool] pubkey: Option[ValidatorPubKey] threshold: Option[uint32] @@ -630,58 +649,118 @@ proc readValue*(reader: var JsonReader, value: var RemoteKeystore) for fieldName in readObjectFields(reader): case fieldName: of "pubkey": - if pubkey.isSome(): + if pubkey.isSome: reader.raiseUnexpectedField("Multiple `pubkey` fields found", "RemoteKeystore") pubkey = some(reader.readValue(ValidatorPubKey)) of "remote": + if remote.isSome: + reader.raiseUnexpectedField("Multiple `remote` fields found", + "RemoteKeystore") if version.isSome and version.get > 1: reader.raiseUnexpectedField( "The `remote` field is valid only in version 1 of the remote keystore format", "RemoteKeystore") - - if remote.isSome(): - reader.raiseUnexpectedField("Multiple `remote` fields found", - "RemoteKeystore") remote = some(reader.readValue(HttpHostUri)) implicitVersion1 = true of "remotes": - if remotes.isSome(): + if remotes.isSome: reader.raiseUnexpectedField("Multiple `remote` fields found", "RemoteKeystore") + if version.isNone: + reader.raiseUnexpectedField( + "The `remotes` field should be specified after the `version` field of the keystore", + "RemoteKeystore") + if version.get < 2: + reader.raiseUnexpectedField( + "The `remotes` field is valid only past version 2 of the remote keystore format", + "RemoteKeystore") remotes = some(reader.readValue(seq[RemoteSignerInfo])) of "version": - if version.isSome(): + if version.isSome: reader.raiseUnexpectedField("Multiple `version` fields found", "RemoteKeystore") version = some(reader.readValue(uint64)) if implicitVersion1 and version.get > 1'u64: reader.raiseUnexpectedValue( "Remote keystore format doesn't match the specified version number") - if version.get > 2'u64: + if version.get > 3'u64: reader.raiseUnexpectedValue( "Remote keystore version " & $version.get & " requires a more recent version of Nimbus") of "description": - let res = reader.readValue(string) - if description.isSome(): - description = some(description.get() & "\n" & res) - else: - description = some(res) + if description.isSome: + reader.raiseUnexpectedField("Multiple `description` fields found", + "RemoteKeystore") + description = some(reader.readValue(string)) of "ignore_ssl_verification": - if ignoreSslVerification.isSome(): + if ignoreSslVerification.isSome: reader.raiseUnexpectedField("Multiple conflicting options found", "RemoteKeystore") ignoreSslVerification = some(reader.readValue(bool)) of "type": - if remoteType.isSome(): + if remoteType.isSome: reader.raiseUnexpectedField("Multiple `type` fields found", "RemoteKeystore") - remoteType = some(reader.readValue(string)) + if version.isNone: + reader.raiseUnexpectedField( + "The `type` field should be specified after the `version` field of the keystore", + "RemoteKeystore") + if version.get < 2: + reader.raiseUnexpectedField( + "The `type` field is valid only past version 2 of the remote keystore format", + "RemoteKeystore") + let remoteTypeValue = case reader.readValue(string).toLowerAscii() + of "web3signer": + RemoteSignerType.Web3Signer + of "verifying-web3signer": + RemoteSignerType.VerifyingWeb3Signer + else: + reader.raiseUnexpectedValue("Unsupported remote signer `type` value") + remoteType = some remoteTypeValue + of "proven_block_properties": + if provenBlockProperties.isSome: + reader.raiseUnexpectedField("Multiple `proven_block_properties` fields found", + "RemoteKeystore") + if version.isNone: + reader.raiseUnexpectedField( + "The `proven_block_properties` field should be specified after the `version` field of the keystore", + "RemoteKeystore") + if version.get < 3: + reader.raiseUnexpectedField( + "The `proven_block_properties` field is valid only past version 3 of the remote keystore format", + "RemoteKeystore") + if remoteType.isNone: + reader.raiseUnexpectedField( + "The `proven_block_properties` field should be specified after the `type` field of the keystore", + "RemoteKeystore") + if remoteType.get != RemoteSignerType.VerifyingWeb3Signer: + reader.raiseUnexpectedField( + "The `proven_block_properties` field can be specified only when the remote signer type is 'verifying-web3signer'", + "RemoteKeystore") + var provenProperties = reader.readValue(seq[ProvenProperty]) + for prop in provenProperties.mitems: + if prop.path == ".execution_payload.fee_recipient": + prop.bellatrixIndex = some GeneralizedIndex(401) + prop.capellaIndex = some GeneralizedIndex(401) + prop.denebIndex = some GeneralizedIndex(401) + else: + reader.raiseUnexpectedValue("Keystores with proved properties different than " & + "`.execution_payload.fee_recipient` require a " & + "more recent version of Nimbus") + provenBlockProperties = some provenProperties of "threshold": - if threshold.isSome(): + if threshold.isSome: reader.raiseUnexpectedField("Multiple `threshold` fields found", "RemoteKeystore") + if version.isNone: + reader.raiseUnexpectedField( + "The `threshold` field should be specified after the `version` field of the keystore", + "RemoteKeystore") + if version.get < 2: + reader.raiseUnexpectedField( + "The `threshold` field is valid only past version 2 of the remote keystore format", + "RemoteKeystore") threshold = some(reader.readValue(uint32)) else: # Ignore unknown field names. @@ -701,18 +780,15 @@ proc readValue*(reader: var JsonReader, value: var RemoteKeystore) if pubkey.isNone(): reader.raiseUnexpectedValue("Field `pubkey` is missing") - let keystoreType = - if remoteType.isSome(): - let res = remoteType.get() - case res.toLowerAscii() - of "web3signer": - RemoteSignerType.Web3Signer - of "web3signer-diva": - RemoteSignerType.Web3SignerDiva - else: - reader.raiseUnexpectedValue("Unsupported remote signer `type` value") - else: - RemoteSignerType.Web3Signer + if version.get >= 3: + if remoteType.isNone: + reader.raiseUnexpectedValue("The required `type` is missing") + case remoteType.get + of RemoteSignerType.Web3Signer: + discard + of RemoteSignerType.VerifyingWeb3Signer: + if provenBlockProperties.isNone: + reader.raiseUnexpectedValue("The required `proven_block_properties` is missing") let keystoreFlags = block: @@ -721,14 +797,24 @@ proc readValue*(reader: var JsonReader, value: var RemoteKeystore) res.incl(RemoteKeystoreFlag.IgnoreSSLVerification) res - value = RemoteKeystore( - version: 2'u64, - pubkey: pubkey.get, - description: description, - remoteType: keystoreType, - remotes: remotes.get, - threshold: threshold.get(1), - ) + value = case remoteType.get(RemoteSignerType.Web3Signer) + of RemoteSignerType.Web3Signer: + RemoteKeystore( + version: 2'u64, + pubkey: pubkey.get, + description: description, + remoteType: RemoteSignerType.Web3Signer, + remotes: remotes.get, + threshold: threshold.get(1)) + of RemoteSignerType.VerifyingWeb3Signer: + RemoteKeystore( + version: 2'u64, + pubkey: pubkey.get, + description: description, + remoteType: RemoteSignerType.VerifyingWeb3Signer, + provenBlockProperties: provenBlockProperties.get, + remotes: remotes.get, + threshold: threshold.get(1)) template writeValue*(w: var JsonWriter, value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) = diff --git a/beacon_chain/spec/mev/bellatrix_mev.nim b/beacon_chain/spec/mev/bellatrix_mev.nim index 171ef2786f..9b86c677dd 100644 --- a/beacon_chain/spec/mev/bellatrix_mev.nim +++ b/beacon_chain/spec/mev/bellatrix_mev.nim @@ -35,7 +35,7 @@ type signature*: ValidatorSig # https://github.com/ethereum/builder-specs/blob/v0.3.0/specs/bellatrix/builder.md#blindedbeaconblockbody - BlindedBeaconBlockBody = object + BlindedBeaconBlockBody* = object randao_reveal*: ValidatorSig eth1_data*: Eth1Data graffiti*: GraffitiBytes diff --git a/beacon_chain/spec/mev/capella_mev.nim b/beacon_chain/spec/mev/capella_mev.nim index 65236101c6..56601dad5e 100644 --- a/beacon_chain/spec/mev/capella_mev.nim +++ b/beacon_chain/spec/mev/capella_mev.nim @@ -25,7 +25,7 @@ type signature*: ValidatorSig # https://github.com/ethereum/builder-specs/blob/v0.3.0/specs/capella/builder.md#blindedbeaconblockbody - BlindedBeaconBlockBody = object + BlindedBeaconBlockBody* = object randao_reveal*: ValidatorSig eth1_data*: Eth1Data graffiti*: GraffitiBytes diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index e03a0f8b74..dad746826d 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -139,16 +139,28 @@ func init*(T: type KeystoreData, keystore: RemoteKeystore, let cookedKey = keystore.pubkey.load().valueOr: return err("Invalid validator's public key") - ok(KeystoreData( - kind: KeystoreKind.Remote, - handle: handle, - pubkey: cookedKey.toPubKey, - description: keystore.description, - version: keystore.version, - remotes: keystore.remotes, - threshold: keystore.threshold, - remoteType: keystore.remoteType - )) + ok case keystore.remoteType + of RemoteSignerType.Web3Signer: + KeystoreData( + kind: KeystoreKind.Remote, + handle: handle, + pubkey: cookedKey.toPubKey, + description: keystore.description, + version: keystore.version, + remotes: keystore.remotes, + threshold: keystore.threshold, + remoteType: RemoteSignerType.Web3Signer) + of RemoteSignerType.VerifyingWeb3Signer: + KeystoreData( + kind: KeystoreKind.Remote, + handle: handle, + pubkey: cookedKey.toPubKey, + description: keystore.description, + version: keystore.version, + remotes: keystore.remotes, + threshold: keystore.threshold, + remoteType: RemoteSignerType.VerifyingWeb3Signer, + provenBlockProperties: keystore.provenBlockProperties) func init*(T: type KeystoreData, cookedKey: CookedPubKey, remotes: seq[RemoteSignerInfo], threshold: uint32, diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index 4e7c6e7679..de166b65cb 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -443,55 +443,6 @@ proc signData(v: AttachedValidator, else: v.signWithDistributedKey(request) -proc getFeeRecipientProof(blck: ForkedBeaconBlock | ForkedBlindedBeaconBlock | - bellatrix_mev.BlindedBeaconBlock | - capella_mev.BlindedBeaconBlock - ): Result[Web3SignerMerkleProof, string] = - when blck is ForkedBlindedBeaconBlock: - case blck.kind - of ConsensusFork.Phase0: - err("Invalid block fork: phase0") - of ConsensusFork.Altair: - err("Invalid block fork: altair") - of ConsensusFork.Bellatrix: - const FeeRecipientIndex = GeneralizedIndex(401) - let res = ? build_proof(blck.bellatrixData.body, FeeRecipientIndex) - ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) - of ConsensusFork.Capella: - const FeeRecipientIndex = GeneralizedIndex(401) - let res = ? build_proof(blck.capellaData.body, FeeRecipientIndex) - ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) - of ConsensusFork.Deneb: - const FeeRecipientIndex = GeneralizedIndex(401) - let res = ? build_proof(blck.denebData.body, FeeRecipientIndex) - ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) - elif blck is bellatrix_mev.BlindedBeaconBlock: - const FeeRecipientIndex = GeneralizedIndex(401) - let res = ? build_proof(blck.body, FeeRecipientIndex) - ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) - elif blck is capella_mev.BlindedBeaconBlock: - const FeeRecipientIndex = GeneralizedIndex(401) - let res = ? build_proof(blck.body, FeeRecipientIndex) - ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) - else: - case blck.kind - of ConsensusFork.Phase0: - err("Invalid block fork: phase0") - of ConsensusFork.Altair: - err("Invalid block fork: altair") - of ConsensusFork.Bellatrix: - const FeeRecipientIndex = GeneralizedIndex(401) - let res = ? build_proof(blck.bellatrixData.body, FeeRecipientIndex) - ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) - of ConsensusFork.Capella: - const FeeRecipientIndex = GeneralizedIndex(401) - let res = ? build_proof(blck.capellaData.body, FeeRecipientIndex) - ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) - of ConsensusFork.Deneb: - const FeeRecipientIndex = GeneralizedIndex(401) - let res = ? build_proof(blck.denebData.body, FeeRecipientIndex) - ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) - # https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/phase0/validator.md#signature proc getBlockSignature*(v: AttachedValidator, fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, @@ -500,6 +451,28 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, bellatrix_mev.BlindedBeaconBlock | capella_mev.BlindedBeaconBlock ): Future[SignatureResult] {.async.} = + type SomeBlockBody = + bellatrix.BeaconBlockBody | + capella.BeaconBlockBody | + deneb.BeaconBlockBody | + bellatrix_mev.BlindedBeaconBlockBody | + capella_mev.BlindedBeaconBlockBody + + template blockPropertiesProofs(blockBody: SomeBlockBody, + forkIndexField: untyped): seq[Web3SignerMerkleProof] = + var proofs: seq[Web3SignerMerkleProof] + for prop in v.data.provenBlockProperties: + if prop.forkIndexField.isSome: + let + idx = prop.forkIndexField.get + proofRes = build_proof(blockBody, idx) + if proofRes.isErr: + return err proofRes.error + proofs.add Web3SignerMerkleProof( + index: idx, + proof: proofRes.get) + proofs + return case v.kind of ValidatorKind.Local: @@ -517,7 +490,7 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Phase0, phase0Data: blck.phase0Data)) - of RemoteSignerType.Web3SignerDiva: + of RemoteSignerType.VerifyingWeb3Signer: return SignatureResult.err("Invalid beacon block fork version") of ConsensusFork.Altair: case v.data.remoteType @@ -525,7 +498,7 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Altair, altairData: blck.altairData)) - of RemoteSignerType.Web3SignerDiva: + of RemoteSignerType.VerifyingWeb3Signer: return SignatureResult.err("Invalid beacon block fork version") of ConsensusFork.Bellatrix: case v.data.remoteType @@ -533,39 +506,39 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, bellatrixData: blck.bellatrixData.toBeaconBlockHeader)) - of RemoteSignerType.Web3SignerDiva: - let res = getFeeRecipientProof(blck) - if res.isErr(): return SignatureResult.err(res.error) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.bellatrixData.body, bellatrixIndex) Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, bellatrixData: blck.bellatrixData.toBeaconBlockHeader), - [res.get()]) + proofs) of ConsensusFork.Capella: case v.data.remoteType of RemoteSignerType.Web3Signer: Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, capellaData: blck.capellaData.toBeaconBlockHeader)) - of RemoteSignerType.Web3SignerDiva: - let res = getFeeRecipientProof(blck) - if res.isErr(): return SignatureResult.err(res.error) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.capellaData.body, capellaIndex) Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, capellaData: blck.capellaData.toBeaconBlockHeader), - [res.get()]) + proofs) of ConsensusFork.Deneb: case v.data.remoteType of RemoteSignerType.Web3Signer: Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, denebData: blck.denebData.toBeaconBlockHeader)) - of RemoteSignerType.Web3SignerDiva: - let res = getFeeRecipientProof(blck) - if res.isErr(): return SignatureResult.err(res.error) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.denebData.body, denebIndex) Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, denebData: blck.denebData.toBeaconBlockHeader), - [res.get()]) + proofs) elif blck is bellatrix_mev.BlindedBeaconBlock: case v.data.remoteType of RemoteSignerType.Web3Signer: @@ -573,27 +546,29 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, bellatrixData: blck.toBeaconBlockHeader) ) - of RemoteSignerType.Web3SignerDiva: - let res = getFeeRecipientProof(blck) - if res.isErr(): return SignatureResult.err(res.error) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.body, bellatrixIndex) Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, bellatrixData: blck.toBeaconBlockHeader), - [res.get()]) + proofs) elif blck is capella_mev.BlindedBeaconBlock: case v.data.remoteType of RemoteSignerType.Web3Signer: Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, capellaData: blck.toBeaconBlockHeader)) - of RemoteSignerType.Web3SignerDiva: - let res = getFeeRecipientProof(blck) - if res.isErr(): return SignatureResult.err(res.error) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.body, capellaIndex) Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, capellaData: blck.toBeaconBlockHeader), - [res.get()]) + proofs) else: + # There should be a deneb_mev module just like the ones above + discard denebImplementationMissing case blck.kind of ConsensusFork.Phase0: # In case of `phase0` block we did not send merkle proof. @@ -602,7 +577,7 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Phase0, phase0Data: blck.phase0Data)) - of RemoteSignerType.Web3SignerDiva: + of RemoteSignerType.VerifyingWeb3Signer: return SignatureResult.err("Invalid beacon block fork version") of ConsensusFork.Altair: # In case of `altair` block we did not send merkle proof. @@ -611,7 +586,7 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Altair, altairData: blck.altairData)) - of RemoteSignerType.Web3SignerDiva: + of RemoteSignerType.VerifyingWeb3Signer: return SignatureResult.err("Invalid beacon block fork version") of ConsensusFork.Bellatrix: case v.data.remoteType @@ -619,39 +594,39 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, bellatrixData: blck.bellatrixData.toBeaconBlockHeader)) - of RemoteSignerType.Web3SignerDiva: - let res = getFeeRecipientProof(blck) - if res.isErr(): return SignatureResult.err(res.error) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.bellatrixData.body, bellatrixIndex) Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, bellatrixData: blck.bellatrixData.toBeaconBlockHeader), - [res.get()]) + proofs) of ConsensusFork.Capella: case v.data.remoteType of RemoteSignerType.Web3Signer: Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, capellaData: blck.capellaData.toBeaconBlockHeader)) - of RemoteSignerType.Web3SignerDiva: - let res = getFeeRecipientProof(blck) - if res.isErr(): return SignatureResult.err(res.error) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.capellaData.body, capellaIndex) Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, capellaData: blck.capellaData.toBeaconBlockHeader), - [res.get()]) + proofs) of ConsensusFork.Deneb: case v.data.remoteType of RemoteSignerType.Web3Signer: Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, denebData: blck.denebData.toBeaconBlockHeader)) - of RemoteSignerType.Web3SignerDiva: - let res = getFeeRecipientProof(blck) - if res.isErr(): return SignatureResult.err(res.error) + of RemoteSignerType.VerifyingWeb3Signer: + let proofs = blockPropertiesProofs( + blck.denebData.body, denebIndex) Web3SignerRequest.init(fork, genesis_validators_root, Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, denebData: blck.denebData.toBeaconBlockHeader), - [res.get()]) + proofs) await v.signData(web3SignerRequest) # https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/phase0/validator.md#aggregate-signature diff --git a/docs/the_nimbus_book/mkdocs.yml b/docs/the_nimbus_book/mkdocs.yml index b45a4ea244..9423d68fa3 100644 --- a/docs/the_nimbus_book/mkdocs.yml +++ b/docs/the_nimbus_book/mkdocs.yml @@ -1,5 +1,12 @@ # yaml-language-server: $schema=https://squidfunk.github.io/mkdocs-material/schema.json +# nimbus_guide +# Copyright (c) 2022-2023 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + site_name: The Nimbus Guide theme: name: material @@ -104,6 +111,7 @@ nav: - 'data-dir.md' - 'era-store.md' - 'history.md' + - 'web3signer.md' - 'migration-options.md' - 'attestation-performance.md' - 'troubleshooting.md' diff --git a/docs/the_nimbus_book/src/data-dir.md b/docs/the_nimbus_book/src/data-dir.md index 8b24cae4c2..b76a096615 100644 --- a/docs/the_nimbus_book/src/data-dir.md +++ b/docs/the_nimbus_book/src/data-dir.md @@ -35,13 +35,21 @@ The growth of the database depends on the [history mode](./history.md). ### `secrets` and `validators` -These two folders contain your validator keys, as well as the passwords needed to unlock them when starting the beacon node. +These two folders contain your validator keys, as well as the passwords needed to unlock them when starting the beacon node. By default, the folders are nested directly under the selected data directory, but you can alter the location through the options `--validators-dir` and `--secrets-dir`. !!! warning Be careful not to copy the `secrets` and `validator` folders, leaving them in two locations! Instead, always _move_ them to the new location. Using the same validators with two nodes poses a significant slashing risk! +For each imported validator, the validators directory includes a sub-folder named after the 0x-prefixed hex-encoded public key of the validator. The per-validator directory contains either a [local keystore file](https://eips.ethereum.org/EIPS/eip-2335) with the name `keystore.json` or [remote keystore file](./web3signer.md) with the name `remote_keystore.json`. It may also contain the following additional configuration files: + +* `suggested_fee_recipient.hex` - a hex-encoded execution layer address that will receive the transaction fees from blocks produced by the particular validator. + +* `suggested_gas_limit.json` - the suggested gas limit of the blocks produced by the particular validator. + +For each imported validator with a local keystore, the secrets directory includes a file named after the 0x-prefixed hex-encoded public key of the validator. The contents of the file will be used as the password for unlocking the keystore. If a password file for a particular validator is missing, Nimbus obtains the password interactively from the user on start-up. If the `--non-interactive` option is specified, Nimbus considers a missing password file to be a fatal error and it will terminate with a non-zero exit code. + ## Moving the data directory You can move the data directory to another location or computer simply by moving its contents and updating the `--data-dir` option when starting the node. diff --git a/docs/the_nimbus_book/src/web3signer.md b/docs/the_nimbus_book/src/web3signer.md new file mode 100644 index 0000000000..8c82779f33 --- /dev/null +++ b/docs/the_nimbus_book/src/web3signer.md @@ -0,0 +1,148 @@ +# Web3Signer + +[Web3Signer](https://docs.web3signer.consensys.net/en/latest/) is a remote signing server developed by Consensys. +It offers a [standardized REST API](https://consensys.github.io/web3signer/web3signer-eth2.html) allowing the Nimbus beacon node or validator client to operate without storing any validator keys locally. + +Remote validators can be permanently added to a Nimbus installation (or more precisely to a particular [data directory](./data-dir.md)) either on-the-fly through the [`POST /eth/v1/remotekeys`](https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/ImportRemoteKeys) request when the [Keymanager API](./keymanager-api.md) is enabled or by manually creating a remote keystore file within the [validators directory](./data-dir.md#secrets-and-validators) of the client which will be loaded upon the next restart. + +Here is an example `remote_keystore.json` file: + +``` +{ + "version": 3, + "description": "This is simple remote keystore file", + "type": "verifying-web3signer", + "pubkey": "0x8107ff6a5cfd1993f0dc19a6a9ec7dc742a528dd6f2e3e10189a4a6fc489ae6c7ba9070ea4e2e328f0d20b91cc129733", + "remote": "http://127.0.0.1:15052", + "ignore_ssl_verification": true, + "proven_block_properties": [ + { "path": ".execution_payload.fee_recipient" } + ] +} +``` + +The fields have the following semantics: + +1. `version` - A decimal version number of the keystore format. This should be the first field. +2. `description` - An optional description of the keystore that can be set to any value by the user. +3. `type` - The type of the remote signer. The currently supported values are `web3signer` and `verifying-web3signer` (see below). Future versions may also support the protocol used by the [Dirk](https://www.attestant.io/posts/introducing-dirk/) signer. +4. `pubkey` - The validator's public key encoded in hexadecimal form. +5. `remote` - An URL of a remote signing server. +6. `remotes` - A [distributed keystore](#distributed-keystores) configuration including two or more remote signing servers. +7. `ignore_ssl_verification` - An optional boolean flag allowing the use of self-signed certificates by the signing server. +8. `proven_block_properties` - When the `verifying-web3signer` type is used, this is a list of locations withing the SSZ block body for which the block signing requests will contain additional merkle proofs, allowing the signer to verify certain details about the signed blocks (e.g. the `fee_recipient` value). + +!!! info + The current version of the remote keystore format is `3` which adds support for the experimental [verifying web3signer setups](#verifying-web3signer). + Version `2` introduced the support for distributed keystores. + +## Distributed Keystores + +!!! warn + This functionality is not currently recommended for production use. + All details described below are subject to change after a planned security audit of the implementation. + Please refer to the [Nimbus SSV Roadmap](https://github.com/status-im/nimbus-eth2/issues/3416) for more details. + +The distributed keystores offer a mechanism for spreading the work of signing validator messages over multiple signing servers in order to gain higher resilience (safety, liveness, or both) when compared to running a validator client on a single machine. +When properly deployed, they can ensure that the validator key cannot be leaked to unauthorized third parties even when they have physical access to the machines where the signers are running. +Furthermore, the scheme supports M-out-of-N threshold signing configurations that can remain active even when some of the signing servers are taken offline. +For more information, please refer to the [Distributed Validator Specification](https://github.com/ethereum/distributed-validator-specs) published by the EF. + +Currently, the distributed keystore support allows pairing a single Nimbus instance with multiple Web3Signer servers. +Future versions may allow creating a highly available cluster of Nimbus instances that mutually act as signers for each other. +Please refer to the [Nimbus SSV Roadmap](https://github.com/status-im/nimbus-eth2/issues/3416) for more details. + +You can migrate any existing validator to a distributed keystore by splitting the key in multiple shares through the `ncli_split_keystore` program. + +!!! info + Since this is a preview feature, the `ncli_split_keystore` program is currently available only when compiling from source. + To build it, clone the [nimbus-eth2 repository](https://github.com/status-im/nimbus-eth2) and run the `make ncli_split_keystore` command within its root. + The resulting binary will be placed in the `build` folder sub-directory. + +Here is an example invocation of the command: + +``` +build/ncli_split_keystore \ + --data-dir=$NIMBUS_DATA_DIR \ + --key=$VALIDATOR_PUBLIC_KEY \ + --threshold=2 \ + --remote-signer=http://signer-1-url \ + --remote-signer=http://signer-2-url \ + --remote-signer=http://signer-3-url \ + --out-dir=$OUT_DIR +``` + +The specified output directory will contain the following files: + +``` +$OUT_DIR/$VALIDATOR_PUBLIC_KEY/remote_keystore.json +$OUT_DIR/shares/secrets/1/$SHARE_1_PUBLIC_KEY +$OUT_DIR/shares/secrets/2/$SHARE_2_PUBLIC_KEY +$OUT_DIR/shares/secrets/3/$SHARE_3_PUBLIC_KEY +$OUT_DIR/shares/validators/1/$SHARE_1_PUBLIC_KEY/keystore.json +$OUT_DIR/shares/validators/2/$SHARE_2_PUBLIC_KEY/keystore.json +$OUT_DIR/shares/validators/3/$SHARE_3_PUBLIC_KEY/keystore.json +``` + +The keystores under the created `shares` directory must be moved to the server where the respective remote signer will be running, while the directory containing the `remote_keystore.json` file must be placed in the validators directory of the Nimbus. + +The specified `threshold` value specifies the minimum number of signers that must remain online in order to create a signature. +Naturally, this value must be lower than the total number of specified remote signers. + +If you are already using a threshold signing setup (e.g. based on Vouch and Dirk), you can migrate your partial keystores to any Web3Signer-compatible server and then manually create the `remote_keystore.json` file which must have the following structure: + +``` +{ + "version": 3, + "pubkey": "0x8107ff6a5cfd1993f0dc19a6a9ec7dc742a528dd6f2e3e10189a4a6fc489ae6c7ba9070ea4e2e328f0d20b91cc129733", + "remotes": [ + { + "url": "http://signer-1-url", + "id": 1, + "pubkey": "83b26b1466f001d723e516b9a4f2ca13c01d9541b17a51a62ee8651d223dcc2dead9ce212e499815f43f7f96dddd4f5a" + }, + { + "url": "http://signer-2-url", + "id": 2, + "pubkey": "897727ba999519a55ac96b617a39cbba543fcd061a99fa4bcac8340dd19126a1130a8b6c2574add4debd4ec4c0c29faf" + }, + { + "url": "http://signer-3-url", + "id": 3, + ` "pubkey": "a68f3ac58974d993908a2e5796d04222411bcdfbb7e5b8c7a10df6717792f9b968772495c554d1b508d4a738014c49b4" + } + ], + "threshold": 2, + "type": "web3signer" +} +``` + +## Verifying Web3Signer + +!!! warn + This functionality is currently considered experimental. + The described implementation may be incomplete and is subject to change in future releases. + +The verifying Web3Signer is an experimental extension to the [Web3Signer protocol](https://consensys.github.io/web3signer/web3signer-eth2.html#tag/Signing/operation/ETH2_SIGN) which allows the remote signer to verify certain details of the signed blocks before creating a signature (for example, the signer may require the signed block to have a particular fee recipient value). + +To enable this use case, the `BLOCK_V2` request type of the `/api/v1/eth2/sign/{identifier}` endpoint is extended with an additional array field named `proofs`. The array consists of objects with the properties `index`, `proof` and `value`, where `index` is an arbitrary [generalized index](https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md#generalized-merkle-tree-index) of any property nested under the block body and `proof` is its corresponding merkle proof against the block body root included in the request. The `value` property is optional and it is included only when the SSZ hash of the field included in the merkle proof doesn't match its value. + +Since the generalized index of a particular field may change in a hard-fork, in the remote keystore format the proven fields are usually specified by their name: + +``` +{ + "version": 3, + "description": "This is simple remote keystore file", + "type": "verifying-web3signer", + "pubkey": "0x8107ff6a5cfd1993f0dc19a6a9ec7dc742a528dd6f2e3e10189a4a6fc489ae6c7ba9070ea4e2e328f0d20b91cc129733", + "remote": "http://127.0.0.1:15052", + "ignore_ssl_verification": true, + "proven_block_properties": [ + { "path": ".execution_payload.fee_recipient" }, + { "path": ".graffiti" } + ] +} +``` + +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/dev/specs/phase0/beacon-chain.md#is_valid_merkle_branch) function by utilizing a similar automatic mapping mechanism for the generalized index. diff --git a/tests/test_remote_keystore.nim b/tests/test_remote_keystore.nim index 8e5dda0aba..e6d2d15b3b 100644 --- a/tests/test_remote_keystore.nim +++ b/tests/test_remote_keystore.nim @@ -1,5 +1,5 @@ # beacon_chain -# Copyright (c) 2022 Status Research & Development GmbH +# Copyright (c) 2022-2023 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). @@ -14,74 +14,160 @@ import ../beacon_chain/spec/[crypto, keystore], ./testutil +template parse(keystore: string): auto = + try: + parseRemoteKeystore(keystore) + except SerializationError as err: + checkpoint "Serialization Error: " & err.formatMsg("") + raise err + suite "Remove keystore testing suite": test "vesion 1" : let remoteKeyStores = """{ "version": 1, "pubkey": "0x8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c", - "remote": "http://127.0.0.1:6000", - "type": "web3signer" + "remote": "http://127.0.0.1:6000" }""" - let keystore = parseRemoteKeystore(remoteKeyStores) + let keystore = parse(remoteKeyStores) check keystore.pubkey.toHex == "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" check keystore.remotes.len == 1 check $keystore.remotes[0].url == "http://127.0.0.1:6000" check keystore.remotes[0].id == 0 check keystore.remotes[0].pubkey.toHex == "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" - test "vesion 2 single remote": - let remoteKeyStores = """{ - "version": 2, - "pubkey": "0x8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c", - "remotes": [ - { - "url": "http://127.0.0.1:6000", - "pubkey": "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" - } - ], - "type": "web3signer" - }""" - let keystore = parseRemoteKeystore(remoteKeyStores) - check keystore.pubkey.toHex == "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" - check keystore.remotes.len == 1 - check $keystore.remotes[0].url == "http://127.0.0.1:6000" - check keystore.remotes[0].id == 0 - check keystore.remotes[0].pubkey.toHex == "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" + test "Single remote": + for version in [2, 3]: + let remoteKeyStores = """{ + "version": """ & $version & """, + "type": "web3signer", + "pubkey": "0x8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c", + "remotes": [ + { + "url": "http://127.0.0.1:6000", + "pubkey": "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" + } + ] + }""" + let keystore = parse(remoteKeyStores) + check keystore.pubkey.toHex == "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" + check keystore.remotes.len == 1 + check $keystore.remotes[0].url == "http://127.0.0.1:6000" + check keystore.remotes[0].id == 0 + check keystore.remotes[0].pubkey.toHex == "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" - test "vesion 2 many remotes" : - let remoteKeyStores = """{ - "version": 2, - "pubkey": "0x8ebc7291df2a671326de83471a4feeb759cc842caa59aa92065e3508baa7e50513bc49a79ff4387c8ef747764f364b6f", - "remotes": [ - { - "url": "http://127.0.0.1:6000", - "id": 1, - "pubkey": "95313b967bcd761175dbc2a5685c16b1a73000e66f9622eca080cb0428dd3db61f7377b32b1fd27f3bdbdf2b554e7f87" - }, - { - "url": "http://127.0.0.1:6001", - "id": 2, - "pubkey": "8b8c115d19a9bdacfc7af9c8e8fc1353af54b63b0e772a641499cac9b6ea5cb1b3479cfa52ebc98ba5afe07a06c06238" - }, - { - "url": "http://127.0.0.1:6002", - "id": 3, - "pubkey": "8f5f9e305e7fcbde94182747f5ecec573d1786e8320a920347a74c0ff5e70f12ca22607c98fdc8dbe71161db59e0ac9d" - } - ], - "threshold": 2, - "type": "web3signer" - }""" - let keystore = Json.decode(remoteKeyStores, RemoteKeystore) - check keystore.pubkey.toHex == "8ebc7291df2a671326de83471a4feeb759cc842caa59aa92065e3508baa7e50513bc49a79ff4387c8ef747764f364b6f" - check keystore.remotes.len == 3 - check $keystore.remotes[0].url == "http://127.0.0.1:6000" - check $keystore.remotes[1].url == "http://127.0.0.1:6001" - check $keystore.remotes[2].url == "http://127.0.0.1:6002" - check keystore.remotes[0].id == 1 - check keystore.remotes[1].id == 2 - check keystore.remotes[2].id == 3 - check keystore.remotes[0].pubkey.toHex == "95313b967bcd761175dbc2a5685c16b1a73000e66f9622eca080cb0428dd3db61f7377b32b1fd27f3bdbdf2b554e7f87" - check keystore.remotes[1].pubkey.toHex == "8b8c115d19a9bdacfc7af9c8e8fc1353af54b63b0e772a641499cac9b6ea5cb1b3479cfa52ebc98ba5afe07a06c06238" - check keystore.remotes[2].pubkey.toHex == "8f5f9e305e7fcbde94182747f5ecec573d1786e8320a920347a74c0ff5e70f12ca22607c98fdc8dbe71161db59e0ac9d" - check keystore.threshold == 2 + test "Many remotes" : + for version in [2, 3]: + let remoteKeyStores = """{ + "version": """ & $version & """, + "type": "web3signer", + "pubkey": "0x8ebc7291df2a671326de83471a4feeb759cc842caa59aa92065e3508baa7e50513bc49a79ff4387c8ef747764f364b6f", + "remotes": [ + { + "url": "http://127.0.0.1:6000", + "id": 1, + "pubkey": "95313b967bcd761175dbc2a5685c16b1a73000e66f9622eca080cb0428dd3db61f7377b32b1fd27f3bdbdf2b554e7f87" + }, + { + "url": "http://127.0.0.1:6001", + "id": 2, + "pubkey": "8b8c115d19a9bdacfc7af9c8e8fc1353af54b63b0e772a641499cac9b6ea5cb1b3479cfa52ebc98ba5afe07a06c06238" + }, + { + "url": "http://127.0.0.1:6002", + "id": 3, + "pubkey": "8f5f9e305e7fcbde94182747f5ecec573d1786e8320a920347a74c0ff5e70f12ca22607c98fdc8dbe71161db59e0ac9d" + } + ], + "threshold": 2 + }""" + let keystore = Json.decode(remoteKeyStores, RemoteKeystore) + check keystore.pubkey.toHex == "8ebc7291df2a671326de83471a4feeb759cc842caa59aa92065e3508baa7e50513bc49a79ff4387c8ef747764f364b6f" + check keystore.remotes.len == 3 + check $keystore.remotes[0].url == "http://127.0.0.1:6000" + check $keystore.remotes[1].url == "http://127.0.0.1:6001" + check $keystore.remotes[2].url == "http://127.0.0.1:6002" + check keystore.remotes[0].id == 1 + check keystore.remotes[1].id == 2 + check keystore.remotes[2].id == 3 + check keystore.remotes[0].pubkey.toHex == "95313b967bcd761175dbc2a5685c16b1a73000e66f9622eca080cb0428dd3db61f7377b32b1fd27f3bdbdf2b554e7f87" + check keystore.remotes[1].pubkey.toHex == "8b8c115d19a9bdacfc7af9c8e8fc1353af54b63b0e772a641499cac9b6ea5cb1b3479cfa52ebc98ba5afe07a06c06238" + check keystore.remotes[2].pubkey.toHex == "8f5f9e305e7fcbde94182747f5ecec573d1786e8320a920347a74c0ff5e70f12ca22607c98fdc8dbe71161db59e0ac9d" + check keystore.threshold == 2 + + test "Verifying Signer / Single remote": + for version in [3]: + let remoteKeyStores = """{ + "version": """ & $version & """, + "type": "verifying-web3signer", + "proven_block_properties": [ + { + "path": ".execution_payload.fee_recipient" + } + ], + "pubkey": "0x8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c", + "remotes": [ + { + "url": "http://127.0.0.1:6000", + "pubkey": "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" + } + ] + }""" + let keystore = parse(remoteKeyStores) + check keystore.pubkey.toHex == "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" + check keystore.remotes.len == 1 + check $keystore.remotes[0].url == "http://127.0.0.1:6000" + check keystore.remotes[0].id == 0 + check keystore.remotes[0].pubkey.toHex == "8b9c875fbe539c6429c4fc304675062579ce47fb6b2ac6b6a1ba1188ca123a80affbfe381dbbc8e7f2437709a4c3325c" + check keystore.provenBlockProperties.len == 1 + check keystore.provenBlockProperties[0].bellatrixIndex == some GeneralizedIndex(401) + check keystore.provenBlockProperties[0].capellaIndex == some GeneralizedIndex(401) + check keystore.provenBlockProperties[0].denebIndex == some GeneralizedIndex(401) + + test "Verifying Signer / Many remotes": + for version in [3]: + let remoteKeyStores = """{ + "version": """ & $version & """, + "type": "verifying-web3signer", + "provenBlockProperties": [ + { + "description": "The fee recipient field of the execution payload", + "path": ".execution_payload.fee_recipient" + } + ], + "pubkey": "0x8ebc7291df2a671326de83471a4feeb759cc842caa59aa92065e3508baa7e50513bc49a79ff4387c8ef747764f364b6f", + "remotes": [ + { + "url": "http://127.0.0.1:6000", + "id": 1, + "pubkey": "95313b967bcd761175dbc2a5685c16b1a73000e66f9622eca080cb0428dd3db61f7377b32b1fd27f3bdbdf2b554e7f87" + }, + { + "url": "http://127.0.0.1:6001", + "id": 2, + "pubkey": "8b8c115d19a9bdacfc7af9c8e8fc1353af54b63b0e772a641499cac9b6ea5cb1b3479cfa52ebc98ba5afe07a06c06238" + }, + { + "url": "http://127.0.0.1:6002", + "id": 3, + "pubkey": "8f5f9e305e7fcbde94182747f5ecec573d1786e8320a920347a74c0ff5e70f12ca22607c98fdc8dbe71161db59e0ac9d" + } + ], + "threshold": 2 + }""" + let keystore = Json.decode(remoteKeyStores, RemoteKeystore) + check keystore.pubkey.toHex == "8ebc7291df2a671326de83471a4feeb759cc842caa59aa92065e3508baa7e50513bc49a79ff4387c8ef747764f364b6f" + check keystore.remotes.len == 3 + check $keystore.remotes[0].url == "http://127.0.0.1:6000" + check $keystore.remotes[1].url == "http://127.0.0.1:6001" + check $keystore.remotes[2].url == "http://127.0.0.1:6002" + check keystore.remotes[0].id == 1 + check keystore.remotes[1].id == 2 + check keystore.remotes[2].id == 3 + check keystore.remotes[0].pubkey.toHex == "95313b967bcd761175dbc2a5685c16b1a73000e66f9622eca080cb0428dd3db61f7377b32b1fd27f3bdbdf2b554e7f87" + check keystore.remotes[1].pubkey.toHex == "8b8c115d19a9bdacfc7af9c8e8fc1353af54b63b0e772a641499cac9b6ea5cb1b3479cfa52ebc98ba5afe07a06c06238" + check keystore.remotes[2].pubkey.toHex == "8f5f9e305e7fcbde94182747f5ecec573d1786e8320a920347a74c0ff5e70f12ca22607c98fdc8dbe71161db59e0ac9d" + check keystore.threshold == 2 + check keystore.provenBlockProperties.len == 1 + check keystore.provenBlockProperties[0].bellatrixIndex == some GeneralizedIndex(401) + check keystore.provenBlockProperties[0].capellaIndex == some GeneralizedIndex(401) + check keystore.provenBlockProperties[0].denebIndex == some GeneralizedIndex(401) diff --git a/tests/test_signing_node.nim b/tests/test_signing_node.nim index 6ef02d10d5..9042c5e2f2 100644 --- a/tests/test_signing_node.nim +++ b/tests/test_signing_node.nim @@ -1,3 +1,10 @@ +# nimbus_signing_node +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + import std/[osproc, algorithm], presto, unittest2, chronicles, stew/[results, byteutils, io2], @@ -11,7 +18,7 @@ import const TestDirectoryName = "test-signing-node" - TestDirectoryNameDiva = "test-signing-node-diva" + TestDirectoryNameVerifyingWeb3Signer = "test-signing-node-verifying-web3signer" ValidatorKeystore1 = "{\"crypto\":{\"kdf\":{\"function\":\"pbkdf2\",\"params\":{\"dklen\":32,\"c\":1,\"prf\":\"hmac-sha256\",\"salt\":\"040f3f4b9dfc4bdeb37de870cbaa83582f981f358e370f271c2945f2e6430aab\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"8b98b30b4e144dbbcc724e502ffecc67c33651aa49600e745e41f959e12abf37\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"04f91a7eb3d6430a598255ea83621e78\"},\"message\":\"5c652e6cdd1215eb9203281e2446abc4d3e1bd50cb822583ce5c74570e9cab18\"}},\"pubkey\":\"99a8df087e253a874c3ca31e0d1115500a671ed8714800d503e99c2c887331a968a7fa7f0290c3a0698675eee138b407\",\"path\":\"m/12381/3600/161/0/0\",\"uuid\":\"81bec933-d928-4e7e-83da-54bbe37a4715\",\"version\":4}" ValidatorKeystore2 = "{\"crypto\":{\"kdf\":{\"function\":\"pbkdf2\",\"params\":{\"dklen\":32,\"c\":1,\"prf\":\"hmac-sha256\",\"salt\":\"040f3f4b9dfc4bdeb37de870cbaa83582f981f358e370f271c2945f2e6430aab\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"2ecda276340c04cb92ce003db9cface0727905f0ba1aa9c60b101f478fca9a5e\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"9d9d73af0031fd19e6833983557b2e30\"},\"message\":\"16d5f87e0675c95cb1e4fc209eea738d45c19b3c0f14088c9e140c573bce0253\"}},\"pubkey\":\"aa19751eb240a04a17b8720e2334acf1d78182ab496e77c51b3bb9e887d50295a478d499abcf6434efbc1aa4c4c4f352\",\"path\":\"m/12381/3600/232/0/0\",\"uuid\":\"291e837b-d8ff-494c-8c7b-7e6bab23b8bf\",\"version\":4}" ValidatorKeystore3 = "{\"crypto\":{\"kdf\":{\"function\":\"pbkdf2\",\"params\":{\"dklen\":32,\"c\":1,\"prf\":\"hmac-sha256\",\"salt\":\"040f3f4b9dfc4bdeb37de870cbaa83582f981f358e370f271c2945f2e6430aab\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"a8c2333e787d65415a02d607c0ec774b654e5a67066e4bc379e2f3b7cf4c826a\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"161171cb21c1c6ec20b15798f545fffc\"},\"message\":\"8ecb326d14dece099d4ba4800a5326324ccf3a8df38fd4aa37af02e8f0617da0\"}},\"pubkey\":\"acf31f9b1ecf65dbb198e380599b6c81fc1a1f5db4457482cc697d81b1fdfb6e49cf8eff4980477f6e32749eef61dc4d\",\"path\":\"m/12381/3600/36/0/0\",\"uuid\":\"420578fd-6832-4e79-a3db-ac0662ace13c\",\"version\":4}" @@ -66,7 +73,7 @@ proc getNodePort(rt: RemoteSignerType): int = case rt of RemoteSignerType.Web3Signer: SigningNodePort - of RemoteSignerType.Web3SignerDiva: + of RemoteSignerType.VerifyingWeb3Signer: SigningNodePort + 1 proc getBlock(fork: ConsensusFork, @@ -170,8 +177,8 @@ proc getTestDir(rt: RemoteSignerType): string = case rt of RemoteSignerType.Web3Signer: TestDirectoryName - of RemoteSignerType.Web3SignerDiva: - TestDirectoryNameDiva + of RemoteSignerType.VerifyingWeb3Signer: + TestDirectoryNameVerifyingWeb3Signer proc createTestDir(rt: RemoteSignerType): Result[void, string] = let @@ -214,12 +221,11 @@ proc getLocalKeystoreData(data: string): Result[KeystoreData, string] = return err("Unable to initialize private key") ValidatorPrivKey(key) - ok(KeystoreData( + ok KeystoreData( kind: KeystoreKind.Local, privateKey: privateKey, version: uint64(4), - pubkey: privateKey.toPubKey().toPubKey() - )) + pubkey: privateKey.toPubKey().toPubKey()) proc getRemoteKeystoreData(data: string, rt: RemoteSignerType): Result[KeystoreData, string] = @@ -232,13 +238,30 @@ proc getRemoteKeystoreData(data: string, $getNodePort(rt))), pubkey: publicKey ) - ok(KeystoreData( - kind: KeystoreKind.Remote, - remoteType: rt, - version: uint64(4), - pubkey: publicKey, - remotes: @[info] - )) + + ok case rt + of RemoteSignerType.Web3Signer: + KeystoreData( + kind: KeystoreKind.Remote, + remoteType: RemoteSignerType.Web3Signer, + version: uint64(4), + pubkey: publicKey, + remotes: @[info]) + of RemoteSignerType.VerifyingWeb3Signer: + KeystoreData( + kind: KeystoreKind.Remote, + remoteType: RemoteSignerType.VerifyingWeb3Signer, + provenBlockProperties: @[ + ProvenProperty( + path: ".execution_payload.fee_recipient", + denebIndex: some GeneralizedIndex(401), + capellaIndex: some GeneralizedIndex(401), + bellatrixIndex: some GeneralizedIndex(401) + ) + ], + version: uint64(4), + pubkey: publicKey, + remotes: @[info]) proc spawnSigningNodeProcess(rt: RemoteSignerType): Result[Process, string] = let process = @@ -254,7 +277,7 @@ proc spawnSigningNodeProcess(rt: RemoteSignerType): Result[Process, string] = "--request-timeout=" & $SigningRequestTimeoutSeconds # we make so low `timeout` to test connection pool. ] - of RemoteSignerType.Web3SignerDiva: + of RemoteSignerType.VerifyingWeb3Signer: @[ "--non-interactive=true", "--data-dir=" & getTestDir(rt) & DirSep & "signing-node", @@ -264,7 +287,7 @@ proc spawnSigningNodeProcess(rt: RemoteSignerType): Result[Process, string] = "--request-timeout=" & $SigningRequestTimeoutSeconds # we make so low `timeout` to test connection pool. ] - osproc.startProcess("build/nimbus_signing_node", "", arguments) + osproc.startProcess("build/nimbus_signing_node", "", arguments, options = {poParentStreams}) except CatchableError as exc: echo "Error while spawning `nimbus_signing_node` process [", $exc.name, "] " & $exc.msg @@ -1073,11 +1096,11 @@ suite "Nimbus remote signer/signing test (web3signer)": shutdownSigningNodeProcess(process) removeTestDir(RemoteSignerType.Web3Signer) -suite "Nimbus remote signer/signing test (web3signer-diva)": - let res = createTestDir(RemoteSignerType.Web3SignerDiva) +suite "Nimbus remote signer/signing test (verifying-web3signer)": + let res = createTestDir(RemoteSignerType.VerifyingWeb3Signer) doAssert(res.isOk()) - let pres = spawnSigningNodeProcess(RemoteSignerType.Web3SignerDiva) + let pres = spawnSigningNodeProcess(RemoteSignerType.VerifyingWeb3Signer) doAssert(pres.isOk()) let process = pres.get() @@ -1104,17 +1127,17 @@ suite "Nimbus remote signer/signing test (web3signer-diva)": let pool2 = newClone(default(ValidatorPool)) let validator4 = pool2[].addValidator( getRemoteKeystoreData(ValidatorPubKey1, - RemoteSignerType.Web3SignerDiva).get(), + RemoteSignerType.VerifyingWeb3Signer).get(), default(Eth1Address), 300_000_000'u64 ) let validator5 = pool2[].addValidator( getRemoteKeystoreData(ValidatorPubKey2, - RemoteSignerType.Web3SignerDiva).get(), + RemoteSignerType.VerifyingWeb3Signer).get(), default(Eth1Address), 300_000_000'u64 ) let validator6 = pool2[].addValidator( getRemoteKeystoreData(ValidatorPubKey3, - RemoteSignerType.Web3SignerDiva).get(), + RemoteSignerType.VerifyingWeb3Signer).get(), default(Eth1Address), 300_000_000'u64 ) @@ -1125,7 +1148,7 @@ suite "Nimbus remote signer/signing test (web3signer-diva)": asyncTest "Waiting for signing node (/upcheck) test": let remoteUrl = "http://" & SigningNodeAddress & ":" & - $getNodePort(RemoteSignerType.Web3SignerDiva) + $getNodePort(RemoteSignerType.VerifyingWeb3Signer) prestoFlags = {RestClientFlag.CommaSeparatedArray} rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) @@ -1162,7 +1185,7 @@ suite "Nimbus remote signer/signing test (web3signer-diva)": request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, Web3SignerForkedBeaconBlock.init(forked1), @[]) remoteUrl = "http://" & SigningNodeAddress & ":" & - $getNodePort(RemoteSignerType.Web3SignerDiva) + $getNodePort(RemoteSignerType.VerifyingWeb3Signer) prestoFlags = {RestClientFlag.CommaSeparatedArray} rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() @@ -1249,7 +1272,7 @@ suite "Nimbus remote signer/signing test (web3signer-diva)": request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, Web3SignerForkedBeaconBlock.init(forked1), @[]) remoteUrl = "http://" & SigningNodeAddress & ":" & - $getNodePort(RemoteSignerType.Web3SignerDiva) + $getNodePort(RemoteSignerType.VerifyingWeb3Signer) prestoFlags = {RestClientFlag.CommaSeparatedArray} rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() @@ -1336,7 +1359,7 @@ suite "Nimbus remote signer/signing test (web3signer-diva)": request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, Web3SignerForkedBeaconBlock.init(forked1), @[]) remoteUrl = "http://" & SigningNodeAddress & ":" & - $getNodePort(RemoteSignerType.Web3SignerDiva) + $getNodePort(RemoteSignerType.VerifyingWeb3Signer) prestoFlags = {RestClientFlag.CommaSeparatedArray} rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() @@ -1425,7 +1448,7 @@ suite "Nimbus remote signer/signing test (web3signer-diva)": request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, Web3SignerForkedBeaconBlock.init(forked1), @[]) remoteUrl = "http://" & SigningNodeAddress & ":" & - $getNodePort(RemoteSignerType.Web3SignerDiva) + $getNodePort(RemoteSignerType.VerifyingWeb3Signer) prestoFlags = {RestClientFlag.CommaSeparatedArray} rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() @@ -1514,7 +1537,7 @@ suite "Nimbus remote signer/signing test (web3signer-diva)": request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, Web3SignerForkedBeaconBlock.init(forked1), @[]) remoteUrl = "http://" & SigningNodeAddress & ":" & - $getNodePort(RemoteSignerType.Web3SignerDiva) + $getNodePort(RemoteSignerType.VerifyingWeb3Signer) prestoFlags = {RestClientFlag.CommaSeparatedArray} rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() @@ -1592,4 +1615,4 @@ suite "Nimbus remote signer/signing test (web3signer-diva)": await client.closeWait() shutdownSigningNodeProcess(process) - removeTestDir(RemoteSignerType.Web3SignerDiva) + removeTestDir(RemoteSignerType.VerifyingWeb3Signer)