From f38288c22c8a0c6f605730785e38038c4ee922e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Avila=20Gast=C3=B3n?= <72628438+avilagaston9@users.noreply.github.com> Date: Mon, 12 Aug 2024 23:56:04 -0300 Subject: [PATCH] feat: implement KeyManager API (#1246) --- README.md | 26 +- config/runtime.exs | 14 + keymanager-oapi.yaml | 1455 +++++++++++++++++ lib/beacon_api/utils.ex | 3 + lib/key_store_api/api_spec.ex | 13 + .../controllers/error_controller.ex | 33 + .../controllers/v1/key_store_controller.ex | 120 ++ lib/key_store_api/endpoint.ex | 11 + lib/key_store_api/error_json.ex | 10 + lib/key_store_api/key_store_api.ex | 45 + lib/key_store_api/router.ex | 27 + lib/keystore.ex | 40 +- lib/lambda_ethereum_consensus/application.ex | 2 + .../validator/duties.ex | 31 +- .../validator/setup.ex | 18 +- .../validator/validator.ex | 144 +- lib/libp2p_port.ex | 52 + network_params.yaml | 3 +- test/unit/keystore_test.exs | 12 +- 19 files changed, 1957 insertions(+), 102 deletions(-) create mode 100644 keymanager-oapi.yaml create mode 100644 lib/key_store_api/api_spec.ex create mode 100644 lib/key_store_api/controllers/error_controller.ex create mode 100644 lib/key_store_api/controllers/v1/key_store_controller.ex create mode 100644 lib/key_store_api/endpoint.ex create mode 100644 lib/key_store_api/error_json.ex create mode 100644 lib/key_store_api/key_store_api.ex create mode 100644 lib/key_store_api/router.ex diff --git a/README.md b/README.md index 75414b851..baa2d7f33 100644 --- a/README.md +++ b/README.md @@ -90,7 +90,8 @@ Some public endpoints can be found in [eth-clients.github.io/checkpoint-sync-end > The data retrieved from the URL is stored in the DB once the node is initiated (i.e. the iex prompt shows). > Once this happens, following runs of `make iex` will start the node using that data. -### Beacon API +### APIs +#### Beacon API You can start the application with the Beacon API on the default port `4000` running: ```shell @@ -100,7 +101,27 @@ make start You can also specify a port with the "--beacon-api-port" flag: ```shell -iex -S mix run -- --beacon-api --beacon-api-port +iex -S mix run -- --beacon-api-port +``` +> [!WARNING] +> In case checkpoint-sync is needed, following the instructions above will end immediately with an error (see [Checkpoint Sync](#checkpoint-sync)). +> + +#### Key-Manager API + +Implemented following the [Ethereum specification](https://ethereum.github.io/keymanager-APIs/#/). + +You can start the application with the key manager API on the default port `5000` running: + +```shell +iex -S mix run -- --validator-api +``` + + +You can also specify a port with the "--validator-api-port" flag: + +```shell +iex -S mix run -- --validator-api-port ``` > [!WARNING] > In case checkpoint-sync is needed, following the instructions above will end immediately with an error (see [Checkpoint Sync](#checkpoint-sync)). @@ -250,6 +271,7 @@ participants: use_separate_vc: false count: 1 cl_max_mem: 4096 + keymanager_enabled: true ``` ### Kurtosis Execution and Make tasks diff --git a/config/runtime.exs b/config/runtime.exs index e12304012..c187a1329 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -18,6 +18,8 @@ switches = [ log_file: :string, beacon_api: :boolean, beacon_api_port: :integer, + validator_api: :boolean, + validator_api_port: :integer, listen_address: [:string, :keep], discovery_port: :integer, boot_nodes: :string, @@ -47,6 +49,8 @@ metrics_port = Keyword.get(args, :metrics_port, nil) enable_metrics = Keyword.get(args, :metrics, not is_nil(metrics_port)) beacon_api_port = Keyword.get(args, :beacon_api_port, nil) enable_beacon_api = Keyword.get(args, :beacon_api, not is_nil(beacon_api_port)) +validator_api_port = Keyword.get(args, :validator_api_port, nil) +enable_validator_api = Keyword.get(args, :validator_api, not is_nil(validator_api_port)) listen_addresses = Keyword.get_values(args, :listen_address) discovery_port = Keyword.get(args, :discovery_port, 9000) cli_bootnodes = Keyword.get(args, :boot_nodes, "") @@ -153,6 +157,16 @@ config :lambda_ethereum_consensus, BeaconApi.Endpoint, layout: false ] +# KeyStore API +config :lambda_ethereum_consensus, KeyStoreApi.Endpoint, + server: enable_validator_api, + http: [port: validator_api_port || 5000], + url: [host: "localhost"], + render_errors: [ + formats: [json: KeyStoreApi.ErrorJSON], + layout: false + ] + # Validator setup if (keystore_dir != nil and keystore_pass_dir == nil) or diff --git a/keymanager-oapi.yaml b/keymanager-oapi.yaml new file mode 100644 index 000000000..e35176294 --- /dev/null +++ b/keymanager-oapi.yaml @@ -0,0 +1,1455 @@ +openapi: 3.0.3 +info: + title: Eth2 key manager API + description: | + API specification for a key manager client, which enables users to manage keystores. + + The key manager API is served by the binary holding the validator keys. This binary may be a remote signer or a validator client. + + All routes SHOULD be exposed through a secure channel, such as with HTTPs, an SSH tunnel, a VPN, etc. + + All requests by default send and receive JSON, and as such should have either or both of the "Content-Type: application/json" + and "Accept: application/json" headers. + + All sensitive routes are to be authenticated with a token. This token should be provided by the user via a secure channel: + - Log the token to stdout when running the binary with the key manager API enabled + - Read the token from a file available to the binary + version: v1.0.0-alpha + contact: + name: Ethereum Github + url: 'https://github.com/ethereum/keymanager-APIs/issues' + license: + name: Creative Commons Zero v1.0 Universal + url: 'https://creativecommons.org/publicdomain/zero/1.0/' +servers: + - url: '{server_url}' + variables: + server_url: + description: key manager API url + default: 'https://public-mainnet-node.ethereum.org' +tags: + - name: Fee Recipient + description: Set of endpoints for management of fee recipient. + - name: Gas Limit + description: Set of endpoints for management of gas limits. + - name: Local Key Manager + description: Set of endpoints for key management of local keys. + - name: Remote Key Manager + description: Set of endpoints for key management of external keys. +paths: + /eth/v1/keystores: + get: + operationId: listKeys + summary: List Keys. + description: | + List all validating pubkeys known to and decrypted by this keymanager binary + security: + - bearerAuth: [] + tags: + - Local Key Manager + responses: + '200': + description: Success response + content: + application/json: + schema: + title: ListKeysResponse + type: object + required: + - data + properties: + data: + type: array + items: + type: object + required: + - validating_pubkey + properties: + validating_pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + derivation_path: + type: string + description: The derivation path (if present in the imported keystore). + example: m/12381/3600/0/0/0 + readonly: + type: boolean + description: The key associated with this pubkey cannot be deleted from the API + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + post: + operationId: importKeystores + summary: Import Keystores. + description: | + Import keystores generated by the Eth2.0 deposit CLI tooling. `passwords[i]` must unlock `keystores[i]`. + + Users SHOULD send slashing_protection data associated with the imported pubkeys. MUST follow the format defined in + EIP-3076: Slashing Protection Interchange Format. + security: + - bearerAuth: [] + tags: + - Local Key Manager + requestBody: + content: + application/json: + schema: + type: object + required: + - keystores + - passwords + properties: + keystores: + type: array + description: JSON-encoded keystore files generated with the Launchpad. + items: + type: string + description: | + JSON serialized representation of a single keystore in EIP-2335: BLS12-381 Keystore format. + example: '{"version":4,"uuid":"9f75a3fa-1e5a-49f9-be3d-f5a19779c6fa","path":"m/12381/3600/0/0/0","pubkey":"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a","crypto":{"kdf":{"function":"pbkdf2","params":{"dklen":32,"c":262144,"prf":"hmac-sha256","salt":"8ff8f22ef522a40f99c6ce07fdcfc1db489d54dfbc6ec35613edf5d836fa1407"},"message":""},"checksum":{"function":"sha256","params":{},"message":"9678a69833d2576e3461dd5fa80f6ac73935ae30d69d07659a709b3cd3eddbe3"},"cipher":{"function":"aes-128-ctr","params":{"iv":"31b69f0ac97261e44141b26aa0da693f"},"message":"e8228bafec4fcbaca3b827e586daad381d53339155b034e5eaae676b715ab05e"}}}' + passwords: + type: array + description: 'Passwords to unlock imported keystore files. `passwords[i]` must unlock `keystores[i]`.' + items: + type: string + example: ABCDEFGH01234567ABCDEFGH01234567 + slashing_protection: + type: string + description: | + JSON serialized representation of the slash protection data in format defined in EIP-3076: Slashing Protection Interchange Format. + example: '{"metadata":{"interchange_format_version":"5","genesis_validators_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},"data":[{"pubkey":"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a","signed_blocks":[],"signed_attestations":[]}]}' + responses: + '200': + description: Success response + content: + application/json: + schema: + title: ImportKeystoresResponse + type: object + required: + - data + properties: + data: + type: array + description: Status result of each `request.keystores` with same length and order of `request.keystores` + items: + type: object + required: + - status + properties: + status: + type: string + description: | + - imported: Keystore successfully decrypted and imported to keymanager permanent storage + - duplicate: Keystore's pubkey is already known to the keymanager + - error: Any other status different to the above: decrypting error, I/O errors, etc. + enum: + - imported + - duplicate + - error + example: imported + message: + type: string + description: error message if status == error + '400': + description: Bad request. Request was malformed and could not be processed + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + delete: + operationId: deleteKeys + summary: Delete Keys. + description: | + DELETE must delete all keys from `request.pubkeys` that are known to the keymanager and exist in its + persistent storage. Additionally, DELETE must fetch the slashing protection data for the requested keys from + persistent storage, which must be retained (and not deleted) after the response has been sent. Therefore in the + case of two identical delete requests being made, both will have access to slashing protection data. + + In a single atomic sequential operation the keymanager must: + 1. Guarantee that key(s) can not produce any more signature; only then + 2. Delete key(s) and serialize its associated slashing protection data + + DELETE should never return a 404 response, even if all pubkeys from request.pubkeys have no extant keystores + nor slashing protection data. + + Slashing protection data must only be returned for keys from `request.pubkeys` for which a + `deleted` or `not_active` status is returned. + security: + - bearerAuth: [] + tags: + - Local Key Manager + requestBody: + content: + application/json: + schema: + type: object + required: + - pubkeys + properties: + pubkeys: + type: array + description: List of public keys to delete. + items: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + responses: + '200': + description: Success response + content: + application/json: + schema: + title: DeleteKeysResponse + type: object + required: + - data + - slashing_protection + properties: + data: + type: array + description: Deletion status of all keys in `request.pubkeys` in the same order. + items: + type: object + required: + - status + properties: + status: + type: string + description: | + - deleted: key was active and removed + - not_active: slashing protection data returned but key was not active + - not_found: key was not found to be removed, and no slashing data can be returned + - error: unexpected condition meant the key could not be removed (the key was actually found, but we couldn't stop using it) - this would be a sign that making it active elsewhere would almost certainly cause you headaches / slashing conditions etc. + enum: + - deleted + - not_active + - not_found + - error + example: deleted + message: + type: string + description: error message if status == error + slashing_protection: + type: string + description: | + JSON serialized representation of the slash protection data in format defined in EIP-3076: Slashing Protection Interchange Format. + example: '{"metadata":{"interchange_format_version":"5","genesis_validators_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},"data":[{"pubkey":"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a","signed_blocks":[],"signed_attestations":[]}]}' + '400': + description: Bad request. Request was malformed and could not be processed + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + /eth/v1/remotekeys: + get: + operationId: listRemoteKeys + summary: List Remote Keys. + description: | + List all remote validating pubkeys known to this validator client binary + security: + - bearerAuth: [] + tags: + - Remote Key Manager + responses: + '200': + description: Success response + content: + application/json: + schema: + title: ListRemoteKeysResponse + type: object + required: + - data + properties: + data: + type: array + items: + type: object + required: + - pubkey + properties: + pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + url: + description: 'URL to API implementing EIP-3030: BLS Remote Signer HTTP API' + type: string + example: 'https://remote.signer' + readonly: + type: boolean + description: The signer associated with this pubkey cannot be deleted from the API + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + post: + operationId: importRemoteKeys + summary: Import Remote Keys. + description: | + Import remote keys for the validator client to request duties for. + security: + - bearerAuth: [] + tags: + - Remote Key Manager + requestBody: + content: + application/json: + schema: + type: object + required: + - remote_keys + properties: + remote_keys: + type: array + items: + type: object + required: + - pubkey + properties: + pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + url: + description: 'URL to API implementing EIP-3030: BLS Remote Signer HTTP API' + type: string + example: 'https://remote.signer' + responses: + '200': + description: Success response + content: + application/json: + schema: + title: ImportRemoteKeysResponse + type: object + required: + - data + properties: + data: + type: array + description: Status result of each `request.remote_keys` with same length and order of `request.remote_keys` + items: + type: object + required: + - status + properties: + status: + type: string + description: | + - imported: Remote key successfully imported to validator client permanent storage + - duplicate: Remote key's pubkey is already known to the validator client + - error: Any other status different to the above: I/O errors, etc. + enum: + - imported + - duplicate + - error + example: imported + message: + type: string + description: error message if status == error + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + delete: + operationId: deleteRemoteKeys + summary: Delete Remote Keys. + description: | + DELETE must delete all keys from `request.pubkeys` that are known to the validator client and exist in its + persistent storage. + + DELETE should never return a 404 response, even if all pubkeys from request.pubkeys have no existing keystores. + security: + - bearerAuth: [] + tags: + - Remote Key Manager + requestBody: + content: + application/json: + schema: + type: object + required: + - pubkeys + properties: + pubkeys: + type: array + description: List of public keys to delete. + items: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + responses: + '200': + description: Success response + content: + application/json: + schema: + title: DeleteRemoteKeysResponse + type: object + required: + - data + properties: + data: + type: array + description: Deletion status of all keys in `request.pubkeys` in the same order. + items: + type: object + required: + - status + properties: + status: + type: string + description: | + - deleted: key was active and removed + - not_found: key was not found to be removed + - error: unexpected condition meant the key could not be removed (the key was actually found, + but we couldn't stop using it) - this would be a sign that making it active elsewhere would + almost certainly cause you headaches / slashing conditions etc. + enum: + - deleted + - not_found + - error + example: deleted + message: + type: string + description: error message if status == error + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '/eth/v1/validator/{pubkey}/feerecipient': + get: + operationId: listFeeRecipient + summary: List Fee Recipient. + description: | + List the validator public key to eth address mapping for fee recipient feature on a specific public key. + The validator public key will return with the default fee recipient address if a specific one was not found. + + WARNING: The fee_recipient is not used on Phase0 or Altair networks. + security: + - bearerAuth: [] + tags: + - Fee Recipient + parameters: + - in: path + name: pubkey + schema: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + required: true + responses: + '200': + description: success response + content: + application/json: + schema: + title: ListFeeRecipientResponse + type: object + required: + - data + properties: + data: + type: object + required: + - ethaddress + properties: + pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + ethaddress: + type: string + description: An address on the execution (Ethereum 1) network. + example: '0xabcf8e0d4e9587369b2301d0790347320302cc09' + pattern: '^0x[a-fA-F0-9]{40}$' + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '404': + description: Path not found + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + post: + operationId: setFeeRecipient + summary: Set Fee Recipient. + description: | + Sets the validator client fee recipient mapping which will then update the beacon node. + Existing mappings for the same validator public key will be overwritten. + Specific Public keys not mapped will continue to use the default address for fee recipient in accordance to the startup of the validator client and beacon node. + Cannot specify the 0x00 fee recipient address through the API. + + WARNING: The fee_recipient is not used on Phase0 or Altair networks. + security: + - bearerAuth: [] + tags: + - Fee Recipient + parameters: + - in: path + name: pubkey + schema: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + required: true + requestBody: + content: + application/json: + schema: + title: SetFeeRecipientRequest + type: object + required: + - ethaddress + properties: + ethaddress: + type: string + description: An address on the execution (Ethereum 1) network. + example: '0xabcf8e0d4e9587369b2301d0790347320302cc09' + pattern: '^0x[a-fA-F0-9]{40}$' + responses: + '202': + description: successfully updated + '400': + description: Bad request. Request was malformed and could not be processed + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '404': + description: Path not found + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + delete: + operationId: deleteFeeRecipient + summary: Delete Configured Fee Recipient + description: Delete a configured fee recipient mapping for the specified public key. + security: + - bearerAuth: [] + tags: + - Fee Recipient + parameters: + - in: path + name: pubkey + schema: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + required: true + responses: + '204': + description: 'Successfully removed the mapping, or there was no mapping to remove for a key that the server is managing.' + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'A mapping was found, but cannot be removed. This may be because the mapping was in configuration files that cannot be updated.' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '404': + description: 'The key was not found on the server, nothing to delete.' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '/eth/v1/validator/{pubkey}/gas_limit': + get: + operationId: getGasLimit + summary: Get Gas Limit. + description: | + Get the execution gas limit for an individual validator. This gas limit is the one used by the + validator when proposing blocks via an external builder. If no limit has been set explicitly for + a key then the process-wide default will be returned. + + The server may return a 400 status code if no external builder is configured. + + WARNING: The gas_limit is not used on Phase0 or Altair networks. + security: + - bearerAuth: [] + tags: + - Gas Limit + parameters: + - in: path + name: pubkey + schema: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + required: true + responses: + '200': + description: success response + content: + application/json: + schema: + title: ListGasLimitResponse + type: object + required: + - data + properties: + data: + type: object + required: + - gas_limit + properties: + pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + gas_limit: + type: string + pattern: '^[1-9][0-9]{0,19}$' + example: '30000000' + '400': + description: Bad request. Request was malformed and could not be processed + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '404': + description: Path not found + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + post: + operationId: setGasLimit + summary: Set Gas Limit. + description: | + Set the gas limit for an individual validator. This limit will be propagated to the beacon + node for use on future block proposals. The beacon node is responsible for informing external + block builders of the change. + + The server may return a 400 status code if no external builder is configured. + + WARNING: The gas_limit is not used on Phase0 or Altair networks. + security: + - bearerAuth: [] + tags: + - Gas Limit + parameters: + - in: path + name: pubkey + schema: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + required: true + requestBody: + content: + application/json: + schema: + title: SetGasLimitRequest + type: object + required: + - gas_limit + properties: + gas_limit: + type: string + pattern: '^[1-9][0-9]{0,19}$' + example: '30000000' + responses: + '202': + description: successfully updated + '400': + description: Bad request. Request was malformed and could not be processed + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '404': + description: Path not found + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + delete: + operationId: deleteGasLimit + summary: Delete Configured Gas Limit + description: | + Delete a configured gas limit for the specified public key. + + The server may return a 400 status code if no external builder is configured. + security: + - bearerAuth: [] + tags: + - Gas Limit + parameters: + - in: path + name: pubkey + schema: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + required: true + responses: + '204': + description: 'Successfully removed the gas limit, or there was no gas limit set for the requested public key.' + '400': + description: Bad request. Request was malformed and could not be processed + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '401': + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '403': + description: 'A gas limit was found, but cannot be removed. This may be because the gas limit was in configuration files that cannot be updated.' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '404': + description: 'The key was not found on the server, nothing to delete.' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + '500': + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred +components: + securitySchemes: + bearerAuth: + type: http + scheme: bearer + bearerFormat: 'URL safe token, optionally JWT' + schemas: + Pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + EthAddress: + type: string + description: An address on the execution (Ethereum 1) network. + example: '0xabcf8e0d4e9587369b2301d0790347320302cc09' + pattern: '^0x[a-fA-F0-9]{40}$' + Keystore: + type: string + description: | + JSON serialized representation of a single keystore in EIP-2335: BLS12-381 Keystore format. + example: '{"version":4,"uuid":"9f75a3fa-1e5a-49f9-be3d-f5a19779c6fa","path":"m/12381/3600/0/0/0","pubkey":"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a","crypto":{"kdf":{"function":"pbkdf2","params":{"dklen":32,"c":262144,"prf":"hmac-sha256","salt":"8ff8f22ef522a40f99c6ce07fdcfc1db489d54dfbc6ec35613edf5d836fa1407"},"message":""},"checksum":{"function":"sha256","params":{},"message":"9678a69833d2576e3461dd5fa80f6ac73935ae30d69d07659a709b3cd3eddbe3"},"cipher":{"function":"aes-128-ctr","params":{"iv":"31b69f0ac97261e44141b26aa0da693f"},"message":"e8228bafec4fcbaca3b827e586daad381d53339155b034e5eaae676b715ab05e"}}}' + FeeRecipient: + type: object + required: + - ethaddress + properties: + pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + ethaddress: + type: string + description: An address on the execution (Ethereum 1) network. + example: '0xabcf8e0d4e9587369b2301d0790347320302cc09' + pattern: '^0x[a-fA-F0-9]{40}$' + GasLimit: + type: object + required: + - gas_limit + properties: + pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + gas_limit: + type: string + pattern: '^[1-9][0-9]{0,19}$' + example: '30000000' + Uint64: + type: string + pattern: '^[1-9][0-9]{0,19}$' + example: '30000000' + SignerDefinition: + type: object + required: + - pubkey + properties: + pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + url: + description: 'URL to API implementing EIP-3030: BLS Remote Signer HTTP API' + type: string + example: 'https://remote.signer' + readonly: + type: boolean + description: The signer associated with this pubkey cannot be deleted from the API + ImportRemoteSignerDefinition: + type: object + required: + - pubkey + properties: + pubkey: + type: string + pattern: '^0x[a-fA-F0-9]{96}$' + description: | + The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._ + example: '0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a' + url: + description: 'URL to API implementing EIP-3030: BLS Remote Signer HTTP API' + type: string + example: 'https://remote.signer' + SlashingProtectionData: + type: string + description: | + JSON serialized representation of the slash protection data in format defined in EIP-3076: Slashing Protection Interchange Format. + example: '{"metadata":{"interchange_format_version":"5","genesis_validators_root":"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},"data":[{"pubkey":"0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a","signed_blocks":[],"signed_attestations":[]}]}' + ErrorResponse: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + responses: + BadRequest: + description: Bad request. Request was malformed and could not be processed + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + Unauthorized: + description: 'Unauthorized, no token is found' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + Forbidden: + description: 'Forbidden, a token is found but is invalid' + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + NotFound: + description: Path not found + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred + InternalError: + description: | + Internal server error. The server encountered an unexpected error indicative of + a serious fault in the system, or a bug. + content: + application/json: + schema: + type: object + required: + - message + properties: + message: + description: Detailed error message + type: string + example: description of the error that occurred diff --git a/lib/beacon_api/utils.ex b/lib/beacon_api/utils.ex index 4f510eb12..2fce31c6c 100644 --- a/lib/beacon_api/utils.ex +++ b/lib/beacon_api/utils.ex @@ -31,6 +31,9 @@ defmodule BeaconApi.Utils do "0x" <> Base.encode16(binary, case: :lower) end + def hex_decode("0x" <> binary), do: Base.decode16(binary, case: :lower) + def hex_decode(binary), do: {:error, "Not valid pubkey: #{inspect(binary)}"} + defp to_json(attribute, module) when is_struct(attribute) do module.schema() |> Enum.map(fn {k, schema} -> diff --git a/lib/key_store_api/api_spec.ex b/lib/key_store_api/api_spec.ex new file mode 100644 index 000000000..a1ac29c23 --- /dev/null +++ b/lib/key_store_api/api_spec.ex @@ -0,0 +1,13 @@ +defmodule KeyStoreApi.ApiSpec do + @moduledoc false + alias OpenApiSpex.OpenApi + @behaviour OpenApi + + file = "keymanager-oapi.yaml" + @external_resource file + @ethspec YamlElixir.read_from_file!(file) + |> OpenApiSpex.OpenApi.Decode.decode() + + @impl OpenApi + def spec(), do: @ethspec +end diff --git a/lib/key_store_api/controllers/error_controller.ex b/lib/key_store_api/controllers/error_controller.ex new file mode 100644 index 000000000..ad7c10148 --- /dev/null +++ b/lib/key_store_api/controllers/error_controller.ex @@ -0,0 +1,33 @@ +defmodule KeyStoreApi.ErrorController do + use KeyStoreApi, :controller + + @spec bad_request(Plug.Conn.t(), binary()) :: Plug.Conn.t() + def bad_request(conn, message) do + conn + |> put_status(400) + |> json(%{ + code: 400, + message: "#{message}" + }) + end + + @spec not_found(Plug.Conn.t(), any) :: Plug.Conn.t() + def not_found(conn, _params) do + conn + |> put_status(404) + |> json(%{ + code: 404, + message: "Resource not found" + }) + end + + @spec internal_error(Plug.Conn.t(), any) :: Plug.Conn.t() + def internal_error(conn, _params) do + conn + |> put_status(500) + |> json(%{ + code: 500, + message: "Internal server error" + }) + end +end diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex new file mode 100644 index 000000000..2db52b413 --- /dev/null +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -0,0 +1,120 @@ +defmodule KeyStoreApi.V1.KeyStoreController do + use KeyStoreApi, :controller + + alias BeaconApi.Utils + alias KeyStoreApi.ApiSpec + alias LambdaEthereumConsensus.Libp2pPort + + require Logger + + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + @keystore_dir Keystore.get_keystore_dir() + @keystore_pass_dir Keystore.get_keystore_pass_dir() + + # NOTE: this function is required by OpenApiSpex, and should return the information + # of each specific endpoint. We just return the specific entry from the parsed spec. + def open_api_operation(:get_keys), + do: ApiSpec.spec().paths["/eth/v1/keystores"].get + + def open_api_operation(:add_keys), + do: ApiSpec.spec().paths["/eth/v1/keystores"].post + + def open_api_operation(:delete_keys), + do: ApiSpec.spec().paths["/eth/v1/keystores"].delete + + @doc """ + Returns all the keystores associated with the node. + """ + @spec get_keys(Plug.Conn.t(), any) :: Plug.Conn.t() + def get_keys(conn, _params) do + conn + |> json(%{ + "data" => + Libp2pPort.get_keystores() + |> Enum.map( + &%{ + "validatin_pubkey" => &1.pubkey |> Utils.hex_encode(), + "derivation_path" => &1.path, + "readonly" => &1.readonly + } + ) + }) + end + + @doc """ + For each keystore received: + - Creates a keystore_file and keystore_pass_file in their respective directories. + - Creates a new validator in Libp2pPort. + """ + @spec add_keys(Plug.Conn.t(), any) :: Plug.Conn.t() + def add_keys(conn, _params) do + body_params = conn.private.open_api_spex.body_params + + results = + Enum.zip(body_params.keystores, body_params.passwords) + |> Enum.map(fn {keystore_str, password_str} -> + # TODO (#1268): handle bad requests + keystore = Keystore.decode_str!(keystore_str, password_str) + + base_name = keystore.pubkey |> Utils.hex_encode() + + # This overrides any existing credential with the same pubkey. + File.write!(get_keystore_file_path(base_name), keystore_str) + File.write!(get_keystore_pass_file_path(base_name), password_str) + Libp2pPort.add_validator(keystore) + + %{ + status: "imported", + message: "Pubkey: #{inspect(keystore.pubkey)}" + } + end) + + conn + |> json(%{ + "data" => results + }) + end + + @doc """ + For each pubkey received: + - Removes the associated validator from Libp2pPort. + - Removes the keystore_file and keystore_pass_file associated with the key. + """ + @spec delete_keys(Plug.Conn.t(), any) :: Plug.Conn.t() + def delete_keys(conn, _params) do + body_params = conn.private.open_api_spex.body_params + + results = + Enum.map(body_params.pubkeys, fn pubkey -> + with {:ok, pubkey} <- Utils.hex_decode(pubkey), + :ok <- Libp2pPort.delete_validator(pubkey) do + File.rm!(get_keystore_file_path(pubkey)) + File.rm!(get_keystore_pass_file_path(pubkey)) + + %{ + status: "deleted", + message: "Pubkey: #{inspect(pubkey)}" + } + else + {:error, reason} -> + Logger.error("[Keystore] Error removing key. Reason: #{reason}") + + %{ + status: "error", + message: "Error removing key #{inspect(pubkey)}. Reason: #{inspect(reason)}" + } + end + end) + + conn + |> json(%{ + "data" => results + }) + end + + defp get_keystore_file_path(base_name), do: Path.join(@keystore_dir, base_name <> ".json") + + defp get_keystore_pass_file_path(base_name), + do: Path.join(@keystore_pass_dir, base_name <> ".txt") +end diff --git a/lib/key_store_api/endpoint.ex b/lib/key_store_api/endpoint.ex new file mode 100644 index 000000000..f3cf1dac9 --- /dev/null +++ b/lib/key_store_api/endpoint.ex @@ -0,0 +1,11 @@ +defmodule KeyStoreApi.Endpoint do + use Phoenix.Endpoint, otp_app: :lambda_ethereum_consensus + + plug(Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + pass: ["*/*"], + json_decoder: Phoenix.json_library() + ) + + plug(KeyStoreApi.Router) +end diff --git a/lib/key_store_api/error_json.ex b/lib/key_store_api/error_json.ex new file mode 100644 index 000000000..24855f639 --- /dev/null +++ b/lib/key_store_api/error_json.ex @@ -0,0 +1,10 @@ +defmodule KeyStoreApi.ErrorJSON do + use KeyStoreApi, :controller + + @spec render(any, any) :: %{message: String.t()} + def render(_, _) do + %{ + message: "There has been an error" + } + end +end diff --git a/lib/key_store_api/key_store_api.ex b/lib/key_store_api/key_store_api.ex new file mode 100644 index 000000000..b2bb3e00f --- /dev/null +++ b/lib/key_store_api/key_store_api.ex @@ -0,0 +1,45 @@ +defmodule KeyStoreApi do + @moduledoc """ + The entrypoint for defining your web interface, such + as controllers, components, channels, and so on. + + This can be used in your application as: + + use KeyStoreApi, :controller + use KeyStoreApi, :html + + The definitions below will be executed for every controller, + component, etc, so keep them short and clean, focused + on imports, uses and aliases. + + Do NOT define functions inside the quoted expressions + below. Instead, define additional modules and import + those modules here. + """ + + def router() do + quote do + use Phoenix.Router, helpers: false + + # Import common connection and controller functions to use in pipelines + import Plug.Conn + import Phoenix.Controller + end + end + + def controller() do + quote do + use Phoenix.Controller, + formats: [:json] + + import Plug.Conn + end + end + + @doc """ + When used, dispatch to the appropriate controller/view/etc. + """ + defmacro __using__(which) when is_atom(which) do + apply(__MODULE__, which, []) + end +end diff --git a/lib/key_store_api/router.ex b/lib/key_store_api/router.ex new file mode 100644 index 000000000..f840d43f8 --- /dev/null +++ b/lib/key_store_api/router.ex @@ -0,0 +1,27 @@ +defmodule KeyStoreApi.Router do + use KeyStoreApi, :router + + pipeline :api do + plug(:accepts, ["json"]) + plug(OpenApiSpex.Plug.PutApiSpec, module: KeyStoreApi.ApiSpec) + end + + # KeyManager API Version 1 + scope "/eth/v1", KeyStoreApi.V1 do + pipe_through(:api) + + scope "/keystores" do + get("/", KeyStoreController, :get_keys) + post("/", KeyStoreController, :add_keys) + delete("/", KeyStoreController, :delete_keys) + end + end + + scope "/api" do + pipe_through(:api) + get("/openapi", OpenApiSpex.Plug.RenderSpec, []) + end + + # Catch-all route outside of any scope + match(:*, "/*path", KeyStoreApi.ErrorController, :not_found) +end diff --git a/lib/keystore.ex b/lib/keystore.ex index 145a92bdd..cb071838b 100644 --- a/lib/keystore.ex +++ b/lib/keystore.ex @@ -9,18 +9,36 @@ defmodule Keystore do @iv_size 16 @checksum_message_size 32 - @spec decode_from_files!(Path.t(), Path.t()) :: {Types.bls_pubkey(), Bls.privkey()} + fields = [ + :pubkey, + :privkey, + :path, + :readonly + ] + + @enforce_keys fields + defstruct fields + + @type t() :: %__MODULE__{ + pubkey: Bls.pubkey(), + privkey: Bls.privkey(), + path: String.t(), + readonly: boolean() + } + + @spec decode_from_files!(Path.t(), Path.t()) :: t() def decode_from_files!(json, password) do password = File.read!(password) File.read!(json) |> decode_str!(password) end - @spec decode_str!(String.t(), String.t()) :: {Types.bls_pubkey(), Bls.privkey()} + @spec decode_str!(String.t(), String.t()) :: t() def decode_str!(json, password) do decoded_json = Jason.decode!(json) # We only support version 4 (the only one) %{"version" => 4} = decoded_json - validate_empty_path!(decoded_json["path"]) + path = decoded_json["path"] + validate_empty_path!(path) privkey = decrypt!(decoded_json["crypto"], password) @@ -36,7 +54,7 @@ defmodule Keystore do raise("Keystore secret and public keys don't form a valid pair") end - {pubkey, privkey} + %__MODULE__{pubkey: pubkey, privkey: privkey, path: path, readonly: false} end # TODO: support keystore paths @@ -128,4 +146,18 @@ defmodule Keystore do defp sanitize_password(password), do: password |> String.normalize(:nfkd) |> String.replace(~r/[\x00-\x1f\x80-\x9f\x7f]/, "") + + def get_keystore_dir() do + config = + Application.get_env(:lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.Setup, []) + + Keyword.get(config, :keystore_dir) || "keystore_dir" + end + + def get_keystore_pass_dir() do + config = + Application.get_env(:lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.Setup, []) + + Keyword.get(config, :keystore_pass_dir) || "keystore_pass_dir" + end end diff --git a/lib/lambda_ethereum_consensus/application.ex b/lib/lambda_ethereum_consensus/application.ex index b88b82d10..dc89954be 100644 --- a/lib/lambda_ethereum_consensus/application.ex +++ b/lib/lambda_ethereum_consensus/application.ex @@ -29,6 +29,7 @@ defmodule LambdaEthereumConsensus.Application do @impl true def config_change(changed, _new, removed) do BeaconApi.Endpoint.config_change(changed, removed) + KeyStoreApi.Endpoint.config_change(changed, removed) :ok end @@ -46,6 +47,7 @@ defmodule LambdaEthereumConsensus.Application do get_children(:db) ++ [ BeaconApi.Endpoint, + KeyStoreApi.Endpoint, LambdaEthereumConsensus.PromEx, LambdaEthereumConsensus.Beacon.BeaconNode ] diff --git a/lib/lambda_ethereum_consensus/validator/duties.ex b/lib/lambda_ethereum_consensus/validator/duties.ex index ff9b70ff9..5e590fd7b 100644 --- a/lib/lambda_ethereum_consensus/validator/duties.ex +++ b/lib/lambda_ethereum_consensus/validator/duties.ex @@ -4,7 +4,6 @@ defmodule LambdaEthereumConsensus.Validator.Duties do """ alias LambdaEthereumConsensus.StateTransition.Accessors alias LambdaEthereumConsensus.StateTransition.Misc - alias LambdaEthereumConsensus.Validator alias LambdaEthereumConsensus.Validator.Utils alias Types.BeaconState @@ -101,11 +100,11 @@ defmodule LambdaEthereumConsensus.Validator.Duties do end) end - def maybe_update_duties(duties, beacon_state, epoch, validator) do + def maybe_update_duties(duties, beacon_state, epoch, validator_index, privkey) do attester_duties = - maybe_update_attester_duties(duties.attester, beacon_state, epoch, validator) + maybe_update_attester_duties(duties.attester, beacon_state, epoch, validator_index, privkey) - proposer_duties = compute_proposer_duties(beacon_state, epoch, validator.index) + proposer_duties = compute_proposer_duties(beacon_state, epoch, validator_index) # To avoid edge-cases old_duty = case duties.proposer do @@ -116,12 +115,21 @@ defmodule LambdaEthereumConsensus.Validator.Duties do %{duties | attester: attester_duties, proposer: old_duty ++ proposer_duties} end - defp maybe_update_attester_duties([epp, ep0, ep1], beacon_state, epoch, validator) do + defp maybe_update_attester_duties( + [epp, ep0, ep1], + beacon_state, + epoch, + validator_index, + privkey + ) do duties = Stream.with_index([ep0, ep1]) |> Enum.map(fn - {:not_computed, i} -> compute_attester_duties(beacon_state, epoch + i, validator) - {d, _} -> d + {:not_computed, i} -> + compute_attester_duties(beacon_state, epoch + i, validator_index, privkey) + + {d, _} -> + d end) [epp | duties] @@ -138,11 +146,12 @@ defmodule LambdaEthereumConsensus.Validator.Duties do @spec compute_attester_duties( beacon_state :: BeaconState.t(), epoch :: Types.epoch(), - validator :: Validator.validator() + validator_index :: non_neg_integer(), + privkey :: Bls.privkey() ) :: attester_duty() | nil - defp compute_attester_duties(beacon_state, epoch, validator) do + defp compute_attester_duties(beacon_state, epoch, validator_index, privkey) do # Can't fail - {:ok, duty} = get_committee_assignment(beacon_state, epoch, validator.index) + {:ok, duty} = get_committee_assignment(beacon_state, epoch, validator_index) case duty do nil -> @@ -151,7 +160,7 @@ defmodule LambdaEthereumConsensus.Validator.Duties do duty -> duty |> Map.put(:attested?, false) - |> update_with_aggregation_duty(beacon_state, validator.privkey) + |> update_with_aggregation_duty(beacon_state, privkey) |> update_with_subnet_id(beacon_state, epoch) end end diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex index 378e11fc8..7f88ddeb8 100644 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ b/lib/lambda_ethereum_consensus/validator/setup.ex @@ -6,7 +6,7 @@ defmodule LambdaEthereumConsensus.Validator.Setup do require Logger alias LambdaEthereumConsensus.Validator - @spec init(Types.slot(), Types.root()) :: %{Bls.pubkey() => Validator.state()} + @spec init(Types.slot(), Types.root()) :: %{Bls.pubkey() => Validator.t()} def init(slot, head_root) do config = Application.get_env(:lambda_ethereum_consensus, __MODULE__, []) keystore_dir = Keyword.get(config, :keystore_dir) @@ -25,12 +25,12 @@ defmodule LambdaEthereumConsensus.Validator.Setup do end defp setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) do - validator_keys = decode_validator_keys(keystore_dir, keystore_pass_dir) + validator_keystores = decode_validator_keystores(keystore_dir, keystore_pass_dir) validators = - validator_keys - |> Enum.map(fn {pubkey, privkey} -> - {pubkey, Validator.new({slot, head_root, {pubkey, privkey}})} + validator_keystores + |> Enum.map(fn keystore -> + {keystore.pubkey, Validator.new({slot, head_root, keystore})} end) |> Map.new() @@ -40,14 +40,14 @@ defmodule LambdaEthereumConsensus.Validator.Setup do end @doc """ - Get validator keys from the keystore directory. + Get validator keystores from the keystore directory. This function expects two files for each validator: - /.json - /.txt """ - @spec decode_validator_keys(binary(), binary()) :: - list({Bls.pubkey(), Bls.privkey()}) - def decode_validator_keys(keystore_dir, keystore_pass_dir) + @spec decode_validator_keystores(binary(), binary()) :: + list(Keystore.t()) + def decode_validator_keystores(keystore_dir, keystore_pass_dir) when is_binary(keystore_dir) and is_binary(keystore_pass_dir) do File.ls!(keystore_dir) |> Enum.map(fn filename -> diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index fa43825a0..50a468607 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -9,7 +9,8 @@ defmodule LambdaEthereumConsensus.Validator do :root, :epoch, :duties, - :validator, + :index, + :keystore, :payload_builder ] @@ -31,35 +32,27 @@ defmodule LambdaEthereumConsensus.Validator do @default_graffiti_message "Lambda, so gentle, so good" - @type validator :: %{ - index: non_neg_integer() | nil, - pubkey: Bls.pubkey(), - privkey: Bls.privkey() - } - # TODO: Slot and Root are redundant, we should also have the duties separated and calculated # just at the begining of every epoch, and then just update them as needed. - @type state :: %__MODULE__{ + @type t :: %__MODULE__{ slot: Types.slot(), epoch: Types.epoch(), root: Types.root(), duties: Duties.duties(), - validator: validator(), + index: non_neg_integer() | nil, + keystore: Keystore.t(), payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } - @spec new({Types.slot(), Types.root(), {Bls.pubkey(), Bls.privkey()}}) :: state - def new({head_slot, head_root, {pubkey, privkey}}) do + @spec new({Types.slot(), Types.root(), Keystore.t()}) :: t() + def new({head_slot, head_root, keystore}) do state = %__MODULE__{ slot: head_slot, epoch: Misc.compute_epoch_at_slot(head_slot), root: head_root, duties: Duties.empty_duties(), - validator: %{ - pubkey: pubkey, - privkey: privkey, - index: nil - }, + index: nil, + keystore: keystore, payload_builder: nil } @@ -76,27 +69,35 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec try_setup_validator(state, Types.slot(), Types.root()) :: state | nil + @spec try_setup_validator(t(), Types.slot(), Types.root()) :: t() | nil defp try_setup_validator(state, slot, root) do epoch = Misc.compute_epoch_at_slot(slot) beacon = fetch_target_state(epoch, root) - case fetch_validator_index(beacon, state.validator) do + case fetch_validator_index(beacon, state.keystore.pubkey) do nil -> nil validator_index -> log_info(validator_index, "setup validator", slot: slot, root: root) - validator = %{state.validator | index: validator_index} - duties = Duties.maybe_update_duties(state.duties, beacon, epoch, validator) + + duties = + Duties.maybe_update_duties( + state.duties, + beacon, + epoch, + validator_index, + state.keystore.privkey + ) + join_subnets_for_duties(duties) Duties.log_duties(duties, validator_index) - %{state | duties: duties, validator: validator} + %{state | duties: duties, index: validator_index} end end - @spec handle_new_head(Types.slot(), Types.root(), state) :: state - def handle_new_head(slot, head_root, %{validator: %{index: nil}} = state) do + @spec handle_new_head(Types.slot(), Types.root(), t()) :: t() + def handle_new_head(slot, head_root, %{index: nil} = state) do log_error("-1", "setup validator", "index not present handle block", slot: slot, root: head_root @@ -106,7 +107,7 @@ defmodule LambdaEthereumConsensus.Validator do end def handle_new_head(slot, head_root, state) do - log_debug(state.validator.index, "recieved new head", slot: slot, root: head_root) + log_debug(state.index, "recieved new head", slot: slot, root: head_root) # TODO: this doesn't take into account reorgs state @@ -115,14 +116,14 @@ defmodule LambdaEthereumConsensus.Validator do |> maybe_build_payload(slot + 1) end - @spec handle_tick({Types.slot(), atom()}, state) :: state - def handle_tick(_logical_time, %{validator: %{index: nil}} = state) do + @spec handle_tick({Types.slot(), atom()}, t()) :: t() + def handle_tick(_logical_time, %{index: nil} = state) do log_error("-1", "setup validator", "index not present for handle tick") state end def handle_tick({slot, :first_third}, state) do - log_debug(state.validator.index, "started first third", slot: slot) + log_debug(state.index, "started first third", slot: slot) # Here we may: # 1. propose our blocks # 2. (TODO) start collecting attestations for aggregation @@ -131,7 +132,7 @@ defmodule LambdaEthereumConsensus.Validator do end def handle_tick({slot, :second_third}, state) do - log_debug(state.validator.index, "started second third", slot: slot) + log_debug(state.index, "started second third", slot: slot) # Here we may: # 1. send our attestation for an empty slot # 2. start building a payload @@ -141,7 +142,7 @@ defmodule LambdaEthereumConsensus.Validator do end def handle_tick({slot, :last_third}, state) do - log_debug(state.validator.index, "started last third", slot: slot) + log_debug(state.index, "started last third", slot: slot) # Here we may publish our attestation aggregate maybe_publish_aggregate(state, slot) end @@ -150,7 +151,7 @@ defmodule LambdaEthereumConsensus.Validator do ### Private Functions ########################## - @spec update_state(state, Types.slot(), Types.root()) :: state + @spec update_state(t(), Types.slot(), Types.root()) :: t() defp update_state(%{slot: slot, root: root} = state, slot, root), do: state @@ -165,7 +166,7 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec recompute_duties(state, Types.epoch(), Types.epoch(), Types.slot(), Types.root()) :: state + @spec recompute_duties(t(), Types.epoch(), Types.epoch(), Types.slot(), Types.root()) :: t() defp recompute_duties(%{root: last_root} = state, last_epoch, epoch, slot, head_root) do start_slot = Misc.compute_start_slot_at_epoch(epoch) target_root = if slot == start_slot, do: head_root, else: last_root @@ -175,10 +176,10 @@ defmodule LambdaEthereumConsensus.Validator do new_duties = Duties.shift_duties(state.duties, epoch, last_epoch) - |> Duties.maybe_update_duties(new_beacon, epoch, state.validator) + |> Duties.maybe_update_duties(new_beacon, epoch, state.index, state.keystore.privkey) move_subnets(state.duties, new_duties) - Duties.log_duties(new_duties, state.validator.index) + Duties.log_duties(new_duties, state.index) %{state | slot: slot, root: head_root, duties: new_duties, epoch: epoch} end @@ -221,7 +222,7 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec maybe_attest(state, Types.slot()) :: state + @spec maybe_attest(t(), Types.slot()) :: t() defp maybe_attest(state, slot) do case Duties.get_current_attester_duty(state.duties, slot) do %{attested?: false} = duty -> @@ -237,36 +238,36 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec attest(state, Duties.attester_duty()) :: :ok - defp attest(%{validator: validator} = state, current_duty) do + @spec attest(t(), Duties.attester_duty()) :: :ok + defp attest(%{index: validator_index, keystore: keystore} = state, current_duty) do subnet_id = current_duty.subnet_id - log_debug(validator.index, "attesting", slot: current_duty.slot, subnet_id: subnet_id) + log_debug(validator_index, "attesting", slot: current_duty.slot, subnet_id: subnet_id) - attestation = produce_attestation(current_duty, state.root, state.validator.privkey) + attestation = produce_attestation(current_duty, state.root, keystore.privkey) log_md = [slot: attestation.data.slot, attestation: attestation, subnet_id: subnet_id] debug_log_msg = - "publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(validator.pubkey)}" + "publishing attestation on committee index: #{current_duty.committee_index} | as #{current_duty.index_in_committee}/#{current_duty.committee_length - 1} and pubkey: #{LambdaEthereumConsensus.Utils.format_shorten_binary(keystore.pubkey)}" - log_debug(validator.index, debug_log_msg, log_md) + log_debug(validator_index, debug_log_msg, log_md) Gossip.Attestation.publish(subnet_id, attestation) - |> log_info_result(validator.index, "published attestation", log_md) + |> log_info_result(validator_index, "published attestation", log_md) if current_duty.should_aggregate? do - log_debug(validator.index, "collecting for future aggregation", log_md) + log_debug(validator_index, "collecting for future aggregation", log_md) Gossip.Attestation.collect(subnet_id, attestation) - |> log_debug_result(validator.index, "collected attestation", log_md) + |> log_debug_result(validator_index, "collected attestation", log_md) end end # We publish our aggregate on the next slot, and when we're an aggregator - defp maybe_publish_aggregate(%{validator: validator} = state, slot) do + defp maybe_publish_aggregate(%{index: validator_index, keystore: keystore} = state, slot) do case Duties.get_current_attester_duty(state.duties, slot) do %{should_aggregate?: true} = duty -> - publish_aggregate(duty, validator) + publish_aggregate(duty, validator_index, keystore) new_duties = Duties.replace_attester_duty(state.duties, duty, %{duty | should_aggregate?: false}) @@ -278,20 +279,20 @@ defmodule LambdaEthereumConsensus.Validator do end end - defp publish_aggregate(duty, validator) do + defp publish_aggregate(duty, validator_index, keystore) do case Gossip.Attestation.stop_collecting(duty.subnet_id) do {:ok, attestations} -> log_md = [slot: duty.slot, attestations: attestations] - log_debug(validator.index, "publishing aggregate", log_md) + log_debug(validator_index, "publishing aggregate", log_md) aggregate_attestations(attestations) - |> append_proof(duty.selection_proof, validator) - |> append_signature(duty.signing_domain, validator) + |> append_proof(duty.selection_proof, validator_index) + |> append_signature(duty.signing_domain, keystore) |> Gossip.Attestation.publish_aggregate() - |> log_info_result(validator.index, "published aggregate", log_md) + |> log_info_result(validator_index, "published aggregate", log_md) {:error, reason} -> - log_error(validator.index, "stop collecting attestations", reason) + log_error(validator_index, "stop collecting attestations", reason) :ok end end @@ -311,9 +312,9 @@ defmodule LambdaEthereumConsensus.Validator do %{List.first(attestations) | aggregation_bits: aggregation_bits, signature: signature} end - defp append_proof(aggregate, proof, validator) do + defp append_proof(aggregate, proof, validator_index) do %Types.AggregateAndProof{ - aggregator_index: validator.index, + aggregator_index: validator_index, aggregate: aggregate, selection_proof: proof } @@ -378,15 +379,15 @@ defmodule LambdaEthereumConsensus.Validator do BlockStates.get_state_info!(parent_root).beacon_state |> go_to_slot(slot) end - @spec fetch_validator_index(Types.BeaconState.t(), validator()) :: + @spec fetch_validator_index(Types.BeaconState.t(), Bls.pubkey()) :: non_neg_integer() | nil - defp fetch_validator_index(beacon, %{index: nil, pubkey: pk}) do - Enum.find_index(beacon.validators, &(&1.pubkey == pk)) + defp fetch_validator_index(beacon, pubkey) do + Enum.find_index(beacon.validators, &(&1.pubkey == pubkey)) end defp proposer?(%{duties: %{proposer: slots}}, slot), do: Enum.member?(slots, slot) - @spec maybe_build_payload(state, Types.slot()) :: state + @spec maybe_build_payload(t(), Types.slot()) :: t() defp maybe_build_payload(%{root: head_root} = state, proposed_slot) do if proposer?(state, proposed_slot) do start_payload_builder(state, proposed_slot, head_root) @@ -395,22 +396,22 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec start_payload_builder(state, Types.slot(), Types.root()) :: state + @spec start_payload_builder(t(), Types.slot(), Types.root()) :: t() defp start_payload_builder(%{payload_builder: {slot, root, _}} = state, slot, root), do: state - defp start_payload_builder(%{validator: validator} = state, proposed_slot, head_root) do + defp start_payload_builder(%{index: validator_index} = state, proposed_slot, head_root) do # TODO: handle reorgs and late blocks - log_debug(validator.index, "starting building payload for slot #{proposed_slot}") + log_debug(validator_index, "starting building payload for slot #{proposed_slot}") case BlockBuilder.start_building_payload(proposed_slot, head_root) do {:ok, payload_id} -> - log_info(validator.index, "payload built for slot #{proposed_slot}") + log_info(validator_index, "payload built for slot #{proposed_slot}") %{state | payload_builder: {proposed_slot, head_root, payload_id}} {:error, reason} -> - log_error(validator.index, "start building payload for slot #{proposed_slot}", reason) + log_error(validator_index, "start building payload for slot #{proposed_slot}", reason) %{state | payload_builder: nil} end @@ -427,32 +428,33 @@ defmodule LambdaEthereumConsensus.Validator do defp propose( %{ root: head_root, - validator: validator, - payload_builder: {proposed_slot, head_root, payload_id} + index: validator_index, + payload_builder: {proposed_slot, head_root, payload_id}, + keystore: keystore } = state, proposed_slot ) do - log_debug(validator.index, "building block", slot: proposed_slot) + log_debug(validator_index, "building block", slot: proposed_slot) build_result = BlockBuilder.build_block( %BuildBlockRequest{ slot: proposed_slot, parent_root: head_root, - proposer_index: validator.index, + proposer_index: validator_index, graffiti_message: @default_graffiti_message, - privkey: validator.privkey + privkey: keystore.privkey }, payload_id ) case build_result do {:ok, {signed_block, blob_sidecars}} -> - publish_block(validator.index, signed_block) - Enum.each(blob_sidecars, &publish_sidecar(validator.index, &1)) + publish_block(validator_index, signed_block) + Enum.each(blob_sidecars, &publish_sidecar(validator_index, &1)) {:error, reason} -> - log_error(validator.index, "build block", reason, slot: proposed_slot) + log_error(validator_index, "build block", reason, slot: proposed_slot) end %{state | payload_builder: nil} @@ -460,7 +462,7 @@ defmodule LambdaEthereumConsensus.Validator do # TODO: at least in kurtosis there are blocks that are proposed without a payload apparently, must investigate. defp propose(%{payload_builder: nil} = state, _proposed_slot) do - log_error(state.validator.index, "propose block", "lack of execution payload") + log_error(state.index, "propose block", "lack of execution payload") state end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 47f86eaa8..afc7d12dd 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -334,6 +334,15 @@ defmodule LambdaEthereumConsensus.Libp2pPort do cast_command(pid, {:update_enr, enr}) end + @spec get_keystores() :: list(Keystore.t()) + def get_keystores(), do: GenServer.call(__MODULE__, :get_keystores) + + @spec delete_validator(Bls.pubkey()) :: :ok | {:error, String.t()} + def delete_validator(pubkey), do: GenServer.call(__MODULE__, {:delete_validator, pubkey}) + + @spec add_validator(Keystore.t()) :: :ok + def add_validator(keystore), do: GenServer.call(__MODULE__, {:add_validator, keystore}) + @spec join_init_topics(port()) :: :ok | {:error, String.t()} defp join_init_topics(port) do topics = [BeaconBlock.topic()] ++ BlobSideCar.topics() @@ -530,6 +539,49 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, state} end + @impl GenServer + def handle_call(:get_keystores, _from, %{validators: validators} = state), + do: {:reply, Enum.map(validators, fn {_pubkey, validator} -> validator.keystore end), state} + + @impl GenServer + def handle_call({:delete_validator, pubkey}, _from, %{validators: validators} = state) do + case Map.fetch(validators, pubkey) do + {:ok, validator} -> + Logger.warning("[Libp2pPort] Deleting validator with index #{inspect(validator.index)}.") + + {:reply, :ok, %{state | validators: Map.delete(validators, pubkey)}} + + :error -> + {:error, "Pubkey #{inspect(pubkey)} not found."} + end + end + + @impl GenServer + def handle_call( + {:add_validator, %Keystore{pubkey: pubkey} = keystore}, + _from, + %{validators: validators} = state + ) do + # TODO (#1263): handle 0 validators + first_validator = validators |> Map.values() |> List.first() + validator = Validator.new({first_validator.slot, first_validator.root, keystore}) + + Logger.warning( + "[Libp2pPort] Adding validator with index #{inspect(validator.index)}. head_slot: #{inspect(validator.slot)}." + ) + + {:reply, :ok, + %{ + state + | validators: + Map.put( + validators, + pubkey, + validator + ) + }} + end + ###################### ### PRIVATE FUNCTIONS ###################### diff --git a/network_params.yaml b/network_params.yaml index e995d03fa..102796866 100644 --- a/network_params.yaml +++ b/network_params.yaml @@ -9,4 +9,5 @@ participants: use_separate_vc: false count: 1 validator_count: 32 - cl_max_mem: 4096 \ No newline at end of file + cl_max_mem: 4096 + keymanager_enabled: true diff --git a/test/unit/keystore_test.exs b/test/unit/keystore_test.exs index 639776366..1d4f40c92 100644 --- a/test/unit/keystore_test.exs +++ b/test/unit/keystore_test.exs @@ -72,7 +72,8 @@ defmodule Unit.KeystoreTest do }) test "eip scrypt test vector" do - {pubkey, privkey} = Keystore.decode_str!(@scrypt_json, @eip_password) + %Keystore{pubkey: pubkey, privkey: privkey, path: _path} = + Keystore.decode_str!(@scrypt_json, @eip_password) assert privkey == @eip_secret assert pubkey == @pubkey @@ -83,7 +84,8 @@ defmodule Unit.KeystoreTest do end test "eip pbkdf2 test vector" do - {pubkey, privkey} = Keystore.decode_str!(@pbkdf2_json, @eip_password) + %Keystore{pubkey: pubkey, privkey: privkey, path: _path} = + Keystore.decode_str!(@pbkdf2_json, @eip_password) assert privkey == @eip_secret assert pubkey == @pubkey @@ -99,7 +101,8 @@ defmodule Unit.KeystoreTest do |> Map.delete("pubkey") |> Jason.encode!() - {pubkey, privkey} = Keystore.decode_str!(scrypt_json, @eip_password) + %Keystore{pubkey: pubkey, privkey: privkey, path: _path} = + Keystore.decode_str!(scrypt_json, @eip_password) assert privkey == @eip_secret assert pubkey == @pubkey @@ -115,7 +118,8 @@ defmodule Unit.KeystoreTest do |> Map.delete("pubkey") |> Jason.encode!() - {pubkey, privkey} = Keystore.decode_str!(pbkdf2_json, @eip_password) + %Keystore{pubkey: pubkey, privkey: privkey, path: _path} = + Keystore.decode_str!(pbkdf2_json, @eip_password) assert privkey == @eip_secret assert pubkey == @pubkey