From 013fec20b2e6d4c0f25f01d84565436bd0354304 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 31 Jul 2024 18:02:36 -0300 Subject: [PATCH 01/26] feat: add keystore API phoenix endpoint --- Makefile | 2 +- config/runtime.exs | 14 + keymanager-oapi.yaml | 1455 +++++++++++++++++ lib/key_store_api/api_spec.ex | 13 + .../controllers/error_controller.ex | 33 + 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 | 16 + lib/lambda_ethereum_consensus/application.ex | 2 + 10 files changed, 1600 insertions(+), 1 deletion(-) 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/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/Makefile b/Makefile index 34485f3ca..afa1d8739 100644 --- a/Makefile +++ b/Makefile @@ -168,7 +168,7 @@ checkpoint-sync: compile-all #▶️ sepolia: @ Run an interactive terminal using sepolia network sepolia: compile-all - iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics + iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics --keystore-api #▶️ holesky: @ Run an interactive terminal using holesky network holesky: compile-all diff --git a/config/runtime.exs b/config/runtime.exs index 7ecc40018..87b4446db 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -18,6 +18,8 @@ switches = [ log_file: :string, beacon_api: :boolean, beacon_api_port: :integer, + keystore_api: :boolean, + keystore_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)) +keystore_api_port = Keyword.get(args, :keystore_api_port, nil) +enable_keystore_api = Keyword.get(args, :keystore_api, not is_nil(keystore_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_keystore_api, + http: [port: keystore_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/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/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..862c796a2 --- /dev/null +++ b/lib/key_store_api/router.ex @@ -0,0 +1,16 @@ +defmodule KeyStoreApi.Router do + use KeyStoreApi, :router + + pipeline :api do + plug(:accepts, ["json"]) + plug(OpenApiSpex.Plug.PutApiSpec, module: KeyStoreApi.ApiSpec) + 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/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 ] From 6f74cd696af4a9ca3da662afb99d230ea4a53719 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 31 Jul 2024 22:05:14 -0300 Subject: [PATCH 02/26] feat: add GET /keystores endpoint --- .../controllers/v1/key_store_controller.ex | 32 +++++++++++++++++++ lib/key_store_api/router.ex | 9 ++++++ .../validator/validator_manager.ex | 5 +++ 3 files changed, 46 insertions(+) create mode 100644 lib/key_store_api/controllers/v1/key_store_controller.ex 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..e65c05ac8 --- /dev/null +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -0,0 +1,32 @@ +defmodule KeyStoreApi.V1.KeyStoreController do + use KeyStoreApi, :controller + + alias BeaconApi.Utils + alias KeyStoreApi.ApiSpec + alias LambdaEthereumConsensus.Validator.ValidatorManager + + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + + # 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 + + @spec get_keys(Plug.Conn.t(), any) :: Plug.Conn.t() + def get_keys(conn, _params) do + pubkeys_info = + ValidatorManager.get_pubkeys() + |> Enum.map( + &%{ + "validatin_pubkey" => &1 |> Utils.hex_encode(), + "derivation_path" => "m/12381/3600/0/0/0", + "readonly" => true + } + ) + + conn + |> json(%{ + "data" => pubkeys_info + }) + end +end diff --git a/lib/key_store_api/router.ex b/lib/key_store_api/router.ex index 862c796a2..9761be944 100644 --- a/lib/key_store_api/router.ex +++ b/lib/key_store_api/router.ex @@ -6,6 +6,15 @@ defmodule KeyStoreApi.Router do 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) + end + end + scope "/api" do pipe_through(:api) get("/openapi", OpenApiSpex.Plug.RenderSpec, []) diff --git a/lib/lambda_ethereum_consensus/validator/validator_manager.ex b/lib/lambda_ethereum_consensus/validator/validator_manager.ex index 099a18a75..820b44ab4 100644 --- a/lib/lambda_ethereum_consensus/validator/validator_manager.ex +++ b/lib/lambda_ethereum_consensus/validator/validator_manager.ex @@ -23,6 +23,11 @@ defmodule LambdaEthereumConsensus.Validator.ValidatorManager do setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) end + def get_pubkeys(), do: GenServer.call(__MODULE__, :get_pubkeys) + + def handle_call(:get_pubkeys, _from, [] = validators), do: {:reply, validators, validators} + def handle_call(:get_pubkeys, _from, validators), do: {:reply, Map.keys(validators), validators} + defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do Logger.warning( From 72cbba975e7de7f9897643b06c35bb725aa9b3ea Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Thu, 1 Aug 2024 17:27:55 -0300 Subject: [PATCH 03/26] fix: update port flag --- Makefile | 2 +- config/runtime.exs | 8 ++++---- network_params.yaml | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index afa1d8739..54e4eedc0 100644 --- a/Makefile +++ b/Makefile @@ -168,7 +168,7 @@ checkpoint-sync: compile-all #▶️ sepolia: @ Run an interactive terminal using sepolia network sepolia: compile-all - iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics --keystore-api + iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics --validator-api-port 5056 #▶️ holesky: @ Run an interactive terminal using holesky network holesky: compile-all diff --git a/config/runtime.exs b/config/runtime.exs index 87b4446db..1ebd1c7d1 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -19,7 +19,7 @@ switches = [ beacon_api: :boolean, beacon_api_port: :integer, keystore_api: :boolean, - keystore_api_port: :integer, + validator_api_port: :integer, listen_address: [:string, :keep], discovery_port: :integer, boot_nodes: :string, @@ -49,8 +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)) -keystore_api_port = Keyword.get(args, :keystore_api_port, nil) -enable_keystore_api = Keyword.get(args, :keystore_api, not is_nil(keystore_api_port)) +validator_api_port = Keyword.get(args, :validator_api_port, nil) +enable_keystore_api = Keyword.get(args, :keystore_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, "") @@ -160,7 +160,7 @@ config :lambda_ethereum_consensus, BeaconApi.Endpoint, # KeyStore API config :lambda_ethereum_consensus, KeyStoreApi.Endpoint, server: enable_keystore_api, - http: [port: keystore_api_port || 5000], + http: [port: validator_api_port || 5000], url: [host: "localhost"], render_errors: [ formats: [json: KeyStoreApi.ErrorJSON], 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 From deaddeee92351b25767698b53e21946b891e1b39 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Tue, 6 Aug 2024 13:09:57 -0300 Subject: [PATCH 04/26] feat: add post method --- Makefile | 2 +- .../controllers/v1/key_store_controller.ex | 46 +++++++++++++++++++ lib/key_store_api/router.ex | 1 + 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 54e4eedc0..d0c3042d3 100644 --- a/Makefile +++ b/Makefile @@ -168,7 +168,7 @@ checkpoint-sync: compile-all #▶️ sepolia: @ Run an interactive terminal using sepolia network sepolia: compile-all - iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics --validator-api-port 5056 + iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics --validator-api-port 5056 --keystore-dir "keystore_dir" --keystore-pass-dir "keystore_pass_dir" #▶️ holesky: @ Run an interactive terminal using holesky network holesky: compile-all diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index e65c05ac8..3c12075d1 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -7,11 +7,17 @@ defmodule KeyStoreApi.V1.KeyStoreController do plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) + @default_keystore_dir "keystore_dir" + @default_keystore_pass_dir "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 + @spec get_keys(Plug.Conn.t(), any) :: Plug.Conn.t() def get_keys(conn, _params) do pubkeys_info = @@ -29,4 +35,44 @@ defmodule KeyStoreApi.V1.KeyStoreController do "data" => pubkeys_info }) end + + @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 + config = Application.get_env(:lambda_ethereum_consensus, ValidatorManager, []) + keystore_dir = Keyword.get(config, :keystore_dir) || @default_keystore_dir + keystore_pass_dir = Keyword.get(config, :keystore_pass_dir) || @default_keystore_pass_dir + + results = + Enum.zip(body_params.keystores, body_params.passwords) + |> Enum.map(fn {keystore, password} -> + {pubkey, _privkey} = Keystore.decode_str!(keystore, password) + + File.write!( + Path.join( + keystore_dir, + "#{inspect(pubkey |> Utils.hex_encode())}.json" + ), + keystore + ) + + File.write!( + Path.join( + keystore_pass_dir, + "#{inspect(pubkey |> Utils.hex_encode())}.txt" + ), + password + ) + + %{ + status: "imported", + message: "Pubkey: #{inspect(pubkey)}" + } + end) + + conn + |> json(%{ + "data" => results + }) + end end diff --git a/lib/key_store_api/router.ex b/lib/key_store_api/router.ex index 9761be944..b4d3c02de 100644 --- a/lib/key_store_api/router.ex +++ b/lib/key_store_api/router.ex @@ -12,6 +12,7 @@ defmodule KeyStoreApi.Router do scope "/keystores" do get("/", KeyStoreController, :get_keys) + post("/", KeyStoreController, :add_keys) end end From 4ecf6db24cf4b717a1bdcc467c404e86d731a2c5 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Tue, 6 Aug 2024 22:12:00 -0300 Subject: [PATCH 05/26] refactor: save keystores into validators --- Makefile | 3 +- .../controllers/v1/key_store_controller.ex | 41 +++---- lib/keystore.ex | 26 ++++- .../validator/duties.ex | 31 ++++-- .../validator/setup.ex | 19 ++-- .../validator/validator.ex | 101 +++++++++--------- lib/libp2p_port.ex | 10 ++ 7 files changed, 132 insertions(+), 99 deletions(-) diff --git a/Makefile b/Makefile index bfff0b1e0..1823ebbae 100644 --- a/Makefile +++ b/Makefile @@ -168,8 +168,7 @@ checkpoint-sync: compile-all #▶️ sepolia: @ Run an interactive terminal using sepolia network sepolia: compile-all - iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics --validator-api-port 5056 --keystore-dir "keystore_dir" --keystore-pass-dir "keystore_pass_dir" - + iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics --validator-api-port 5056 #▶️ holesky: @ Run an interactive terminal using holesky network holesky: compile-all iex -S mix run -- --checkpoint-sync-url https://checkpoint-sync.holesky.ethpandaops.io --network holesky diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index 3c12075d1..bc3883e52 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -3,7 +3,7 @@ defmodule KeyStoreApi.V1.KeyStoreController do alias BeaconApi.Utils alias KeyStoreApi.ApiSpec - alias LambdaEthereumConsensus.Validator.ValidatorManager + alias LambdaEthereumConsensus.Libp2pPort plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) @@ -20,53 +20,54 @@ defmodule KeyStoreApi.V1.KeyStoreController do @spec get_keys(Plug.Conn.t(), any) :: Plug.Conn.t() def get_keys(conn, _params) do - pubkeys_info = - ValidatorManager.get_pubkeys() - |> Enum.map( - &%{ - "validatin_pubkey" => &1 |> Utils.hex_encode(), - "derivation_path" => "m/12381/3600/0/0/0", - "readonly" => true - } - ) - conn |> json(%{ - "data" => pubkeys_info + "data" => + Libp2pPort.get_keystores() + |> Enum.map( + &%{ + "validatin_pubkey" => &1.pubkey |> Utils.hex_encode(), + "derivation_path" => &1.path, + "readonly" => &1.readonly + } + ) }) end @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 - config = Application.get_env(:lambda_ethereum_consensus, ValidatorManager, []) + + config = + Application.get_env(:lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.Setup, []) + keystore_dir = Keyword.get(config, :keystore_dir) || @default_keystore_dir keystore_pass_dir = Keyword.get(config, :keystore_pass_dir) || @default_keystore_pass_dir results = Enum.zip(body_params.keystores, body_params.passwords) - |> Enum.map(fn {keystore, password} -> - {pubkey, _privkey} = Keystore.decode_str!(keystore, password) + |> Enum.map(fn {keystore_file, password_file} -> + keystore = Keystore.decode_str!(keystore_file, password_file) File.write!( Path.join( keystore_dir, - "#{inspect(pubkey |> Utils.hex_encode())}.json" + "#{inspect(keystore.pubkey |> Utils.hex_encode())}.json" ), - keystore + keystore_file ) File.write!( Path.join( keystore_pass_dir, - "#{inspect(pubkey |> Utils.hex_encode())}.txt" + "#{inspect(keystore.pubkey |> Utils.hex_encode())}.txt" ), - password + password_file ) %{ status: "imported", - message: "Pubkey: #{inspect(pubkey)}" + message: "Pubkey: #{inspect(keystore.pubkey)}" } end) diff --git a/lib/keystore.ex b/lib/keystore.ex index 145a92bdd..f51334b8f 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: Types.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 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 d07469dcf..00206e312 100644 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ b/lib/lambda_ethereum_consensus/validator/setup.ex @@ -15,11 +15,6 @@ defmodule LambdaEthereumConsensus.Validator.Setup do setup_validators(slot, head_root, keystore_dir, keystore_pass_dir) end - def get_pubkeys(), do: GenServer.call(__MODULE__, :get_pubkeys) - - def handle_call(:get_pubkeys, _from, [] = validators), do: {:reply, validators, validators} - def handle_call(:get_pubkeys, _from, validators), do: {:reply, Map.keys(validators), validators} - defp setup_validators(_s, _r, keystore_dir, keystore_pass_dir) when is_nil(keystore_dir) or is_nil(keystore_pass_dir) do Logger.warning( @@ -30,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() @@ -45,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()) :: + @spec decode_validator_keystores(binary(), binary()) :: list({Bls.pubkey(), Bls.privkey()}) - def decode_validator_keys(keystore_dir, keystore_pass_dir) + 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..af9ed737f 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,12 +32,6 @@ 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__{ @@ -44,22 +39,20 @@ defmodule LambdaEthereumConsensus.Validator do 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()}) :: state + 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 } @@ -81,17 +74,25 @@ defmodule LambdaEthereumConsensus.Validator 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 @@ -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 @@ -122,7 +123,7 @@ defmodule LambdaEthereumConsensus.Validator do 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 @@ -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 @@ -238,35 +239,35 @@ defmodule LambdaEthereumConsensus.Validator do end @spec attest(state, Duties.attester_duty()) :: :ok - defp attest(%{validator: validator} = state, current_duty) do + 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,9 +379,9 @@ 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.privkey()) :: non_neg_integer() | nil - defp fetch_validator_index(beacon, %{index: nil, pubkey: pk}) do + defp fetch_validator_index(beacon, pk) do Enum.find_index(beacon.validators, &(&1.pubkey == pk)) end @@ -399,18 +400,18 @@ defmodule LambdaEthereumConsensus.Validator do 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 @@ -460,7 +461,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..184881bfd 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -334,6 +334,8 @@ defmodule LambdaEthereumConsensus.Libp2pPort do cast_command(pid, {:update_enr, enr}) end + def get_keystores(), do: GenServer.call(__MODULE__, :get_keystores) + @spec join_init_topics(port()) :: :ok | {:error, String.t()} defp join_init_topics(port) do topics = [BeaconBlock.topic()] ++ BlobSideCar.topics() @@ -530,6 +532,14 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, state} end + @impl GenServer + def handle_call(:get_keystores, _from, %{validators: []} = state), + do: {:reply, [], state} + + @impl GenServer + def handle_call(:get_keystores, _from, %{validators: validators} = state), + do: {:reply, Enum.map(validators, fn {_pk, validator} -> validator.keystore end), state} + ###################### ### PRIVATE FUNCTIONS ###################### From b597427cd9afbcfd4cd24e300afd7a416e493517 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 7 Aug 2024 10:50:58 -0300 Subject: [PATCH 06/26] feat: add delete endpoint --- .../controllers/v1/key_store_controller.ex | 45 +++++++++++++++++++ lib/key_store_api/router.ex | 1 + lib/libp2p_port.ex | 24 +++++++++- test/unit/keystore_test.exs | 12 +++-- 4 files changed, 77 insertions(+), 5 deletions(-) diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index bc3883e52..50da08064 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -18,6 +18,9 @@ defmodule KeyStoreApi.V1.KeyStoreController do 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 + @spec get_keys(Plug.Conn.t(), any) :: Plug.Conn.t() def get_keys(conn, _params) do conn @@ -65,6 +68,8 @@ defmodule KeyStoreApi.V1.KeyStoreController do password_file ) + Libp2pPort.add_validator(keystore) + %{ status: "imported", message: "Pubkey: #{inspect(keystore.pubkey)}" @@ -76,4 +81,44 @@ defmodule KeyStoreApi.V1.KeyStoreController do "data" => results }) end + + @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 + + config = + Application.get_env(:lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.Setup, []) + + keystore_dir = Keyword.get(config, :keystore_dir) || @default_keystore_dir + keystore_pass_dir = Keyword.get(config, :keystore_pass_dir) || @default_keystore_pass_dir + + results = + Enum.map(body_params.pubkeys, fn pubkey -> + :ok = Libp2pPort.delete_validator(pubkey) + + File.rm!( + Path.join( + keystore_dir, + "#{inspect(pubkey |> Utils.hex_encode())}.json" + ) + ) + + File.rm!( + Path.join( + keystore_pass_dir, + "#{inspect(pubkey |> Utils.hex_encode())}.txt" + ) + ) + + %{ + status: "deleted", + message: "Pubkey: #{inspect(pubkey)}" + } + end) + + conn + |> json(%{ + "data" => results + }) + end end diff --git a/lib/key_store_api/router.ex b/lib/key_store_api/router.ex index b4d3c02de..f840d43f8 100644 --- a/lib/key_store_api/router.ex +++ b/lib/key_store_api/router.ex @@ -13,6 +13,7 @@ defmodule KeyStoreApi.Router do scope "/keystores" do get("/", KeyStoreController, :get_keys) post("/", KeyStoreController, :add_keys) + delete("/", KeyStoreController, :delete_keys) end end diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 184881bfd..c8947b8f1 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -84,7 +84,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do discovery_addresses: [String.t()] } - @sync_delay_millis 10_000 + @sync_delay_millis 20_000 ###################### ### API @@ -334,8 +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 + 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() @@ -540,6 +547,21 @@ defmodule LambdaEthereumConsensus.Libp2pPort do def handle_call(:get_keystores, _from, %{validators: validators} = state), do: {:reply, Enum.map(validators, fn {_pk, validator} -> validator.keystore end), state} + @impl GenServer + def handle_call({:delete_validator, pk}, _from, %{validators: validators} = state), + do: {:reply, :ok, %{state | validators: Map.delete(validators, pk)}} + + @impl GenServer + def handle_call({:add_validator, keystore}, _from, %{validators: validators} = state) do + # TODO: HANDLE REPEATED VALIDATORS + {:reply, :ok, + %{ + state + | validators: + Map.put(validators, keystore.pubkey, Validator.new({0, <<0::256>>, keystore})) + }} + end + ###################### ### PRIVATE FUNCTIONS ###################### 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 From 52003fb9256c4fc28539bc44d6694faa685abd60 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 7 Aug 2024 17:00:42 -0300 Subject: [PATCH 07/26] fix: delete/add endpoints --- lib/beacon_api/utils.ex | 6 +++ .../controllers/v1/key_store_controller.ex | 52 +++++++++++-------- .../validator/setup.ex | 2 + lib/libp2p_port.ex | 23 ++++++-- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/lib/beacon_api/utils.ex b/lib/beacon_api/utils.ex index 4f510eb12..df0f3fde0 100644 --- a/lib/beacon_api/utils.ex +++ b/lib/beacon_api/utils.ex @@ -31,6 +31,12 @@ defmodule BeaconApi.Utils do "0x" <> Base.encode16(binary, case: :lower) end + def hex_decode("0x" <> binary) do + with {:ok, decoded} <- Base.decode16(binary, case: :lower) do + decoded + end + end + defp to_json(attribute, module) when is_struct(attribute) do module.schema() |> Enum.map(fn {k, schema} -> diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index 50da08064..3a3a92992 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -5,6 +5,8 @@ defmodule KeyStoreApi.V1.KeyStoreController do alias KeyStoreApi.ApiSpec alias LambdaEthereumConsensus.Libp2pPort + require Logger + plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) @default_keystore_dir "keystore_dir" @@ -52,10 +54,12 @@ defmodule KeyStoreApi.V1.KeyStoreController do |> Enum.map(fn {keystore_file, password_file} -> keystore = Keystore.decode_str!(keystore_file, password_file) + base_name = keystore.pubkey |> Utils.hex_encode() + File.write!( Path.join( keystore_dir, - "#{inspect(keystore.pubkey |> Utils.hex_encode())}.json" + base_name <> ".json" ), keystore_file ) @@ -63,7 +67,7 @@ defmodule KeyStoreApi.V1.KeyStoreController do File.write!( Path.join( keystore_pass_dir, - "#{inspect(keystore.pubkey |> Utils.hex_encode())}.txt" + base_name <> ".txt" ), password_file ) @@ -94,26 +98,30 @@ defmodule KeyStoreApi.V1.KeyStoreController do results = Enum.map(body_params.pubkeys, fn pubkey -> - :ok = Libp2pPort.delete_validator(pubkey) - - File.rm!( - Path.join( - keystore_dir, - "#{inspect(pubkey |> Utils.hex_encode())}.json" - ) - ) - - File.rm!( - Path.join( - keystore_pass_dir, - "#{inspect(pubkey |> Utils.hex_encode())}.txt" - ) - ) - - %{ - status: "deleted", - message: "Pubkey: #{inspect(pubkey)}" - } + case Libp2pPort.delete_validator(pubkey |> Utils.hex_decode()) do + :ok -> + File.rm!( + Path.join( + keystore_dir, + pubkey <> ".json" + ) + ) + + File.rm!( + Path.join( + keystore_pass_dir, + pubkey <> ".txt" + ) + ) + + %{ + status: "deleted", + message: "Pubkey: #{inspect(pubkey)}" + } + + {:error, reason} -> + Logger.error("[Keystore] Error removing key. Reason: #{reason}") + end end) conn diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex index 00206e312..630bc96b7 100644 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ b/lib/lambda_ethereum_consensus/validator/setup.ex @@ -57,6 +57,8 @@ defmodule LambdaEthereumConsensus.Validator.Setup do keystore_file = Path.join(keystore_dir, "#{base_name}.json") keystore_pass_file = Path.join(keystore_pass_dir, "#{base_name}.txt") + IO.inspect("KEYSTORE_FILE: #{inspect(keystore_file)}") + IO.inspect("KEYSTORE_PASS_FILE: #{inspect(keystore_pass_file)}") {keystore_file, keystore_pass_file} else Logger.warning("[Validator] Skipping file: #{filename}. Not a keystore file.") diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index c8947b8f1..1c2ecbf4c 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -337,7 +337,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @spec get_keystores() :: list(Keystore.t()) def get_keystores(), do: GenServer.call(__MODULE__, :get_keystores) - @spec delete_validator(Bls.pubkey()) :: :ok + @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 @@ -548,17 +548,32 @@ defmodule LambdaEthereumConsensus.Libp2pPort do do: {:reply, Enum.map(validators, fn {_pk, validator} -> validator.keystore end), state} @impl GenServer - def handle_call({:delete_validator, pk}, _from, %{validators: validators} = state), - do: {:reply, :ok, %{state | validators: Map.delete(validators, pk)}} + def handle_call({:delete_validator, pk}, _from, %{validators: validators} = state) do + case Map.fetch(validators, pk) do + {:ok, validator} -> + Logger.warning("[Libp2pPort] Deleting validator with index #{inspect(validator.index)}.") + + {:reply, :ok, %{state | validators: Map.delete(validators, pk)}} + + :error -> + {:error, "Pubkey #{inspect(pk)} not found."} + end + end @impl GenServer def handle_call({:add_validator, keystore}, _from, %{validators: validators} = state) do # TODO: HANDLE REPEATED VALIDATORS + current_status = ForkChoice.get_current_status_message() + {:reply, :ok, %{ state | validators: - Map.put(validators, keystore.pubkey, Validator.new({0, <<0::256>>, keystore})) + Map.put( + validators, + keystore.pubkey, + Validator.new({current_status.head_slot, current_status.head_root, keystore}) + ) }} end From c45cc5648d8dd9bcf2bd7b67ea9c72ca9a419589 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 7 Aug 2024 18:19:28 -0300 Subject: [PATCH 08/26] fix: try fix ci --- lib/keystore.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/keystore.ex b/lib/keystore.ex index f51334b8f..92095ce21 100644 --- a/lib/keystore.ex +++ b/lib/keystore.ex @@ -20,7 +20,7 @@ defmodule Keystore do defstruct fields @type t() :: %__MODULE__{ - pubkey: Types.bls_pubkey(), + pubkey: Bls.pubkey(), privkey: Bls.privkey(), path: String.t(), readonly: boolean() From 31edacb3eda39ae68068ff15432d1a7940683c58 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Wed, 7 Aug 2024 23:22:20 -0300 Subject: [PATCH 09/26] fix: test --- .../fork_choice/fork_choice.ex | 46 +++++++++---------- .../validator/validator.ex | 17 +++---- network_params.yaml | 4 +- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index 6e8ccd844..103fb98a5 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -11,11 +11,11 @@ defmodule LambdaEthereumConsensus.ForkChoice do alias LambdaEthereumConsensus.Metrics alias LambdaEthereumConsensus.P2P.Gossip.OperationsCollector alias LambdaEthereumConsensus.StateTransition.Misc - alias LambdaEthereumConsensus.Store.BlobDb - alias LambdaEthereumConsensus.Store.BlockDb + # alias LambdaEthereumConsensus.Store.BlobDb + # alias LambdaEthereumConsensus.Store.BlockDb alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.CheckpointStates - alias LambdaEthereumConsensus.Store.StateDb + # alias LambdaEthereumConsensus.Store.StateDb alias LambdaEthereumConsensus.Store.StoreDb alias Types.Attestation alias Types.BlockInfo @@ -174,26 +174,26 @@ defmodule LambdaEthereumConsensus.ForkChoice do ### Private Functions ########################## - defp prune_old_states(last_finalized_epoch, new_finalized_epoch) do - if last_finalized_epoch < new_finalized_epoch do - new_finalized_slot = - new_finalized_epoch * ChainSpec.get("SLOTS_PER_EPOCH") - - Task.Supervisor.start_child( - PruneStatesSupervisor, - fn -> StateDb.prune_states_older_than(new_finalized_slot) end - ) - - Task.Supervisor.start_child( - PruneBlocksSupervisor, - fn -> BlockDb.prune_blocks_older_than(new_finalized_slot) end - ) - - Task.Supervisor.start_child( - PruneBlobsSupervisor, - fn -> BlobDb.prune_old_blobs(new_finalized_slot) end - ) - end + defp prune_old_states(_last_finalized_epoch, _new_finalized_epoch) do + # if last_finalized_epoch < new_finalized_epoch do + # new_finalized_slot = + # new_finalized_epoch * ChainSpec.get("SLOTS_PER_EPOCH") + + # Task.Supervisor.start_child( + # PruneStatesSupervisor, + # fn -> StateDb.prune_states_older_than(new_finalized_slot) end + # ) + + # Task.Supervisor.start_child( + # PruneBlocksSupervisor, + # fn -> BlockDb.prune_blocks_older_than(new_finalized_slot) end + # ) + + # Task.Supervisor.start_child( + # PruneBlobsSupervisor, + # fn -> BlobDb.prune_old_blobs(new_finalized_slot) end + # ) + # end end def apply_handler(iter, state, handler) do diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index af9ed737f..9aeee3385 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -428,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} diff --git a/network_params.yaml b/network_params.yaml index 102796866..5c564f285 100644 --- a/network_params.yaml +++ b/network_params.yaml @@ -2,12 +2,12 @@ participants: - el_type: geth cl_type: lighthouse count: 2 - validator_count: 32 + validator_count: 1 - el_type: geth cl_type: lambda cl_image: lambda_ethereum_consensus:latest use_separate_vc: false count: 1 - validator_count: 32 + validator_count: 63 cl_max_mem: 4096 keymanager_enabled: true From f0f352432a164d9b88328776af51cba7d8e6e630 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Thu, 8 Aug 2024 00:14:27 -0300 Subject: [PATCH 10/26] refactor: enhace readability --- .../controllers/v1/key_store_controller.ex | 51 ++++--------------- lib/keystore.ex | 14 +++++ lib/libp2p_port.ex | 6 ++- 3 files changed, 27 insertions(+), 44 deletions(-) diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index 3a3a92992..e979d70b3 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -9,8 +9,8 @@ defmodule KeyStoreApi.V1.KeyStoreController do plug(OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true) - @default_keystore_dir "keystore_dir" - @default_keystore_pass_dir "keystore_pass_dir" + @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. @@ -43,12 +43,6 @@ defmodule KeyStoreApi.V1.KeyStoreController do def add_keys(conn, _params) do body_params = conn.private.open_api_spex.body_params - config = - Application.get_env(:lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.Setup, []) - - keystore_dir = Keyword.get(config, :keystore_dir) || @default_keystore_dir - keystore_pass_dir = Keyword.get(config, :keystore_pass_dir) || @default_keystore_pass_dir - results = Enum.zip(body_params.keystores, body_params.passwords) |> Enum.map(fn {keystore_file, password_file} -> @@ -56,21 +50,8 @@ defmodule KeyStoreApi.V1.KeyStoreController do base_name = keystore.pubkey |> Utils.hex_encode() - File.write!( - Path.join( - keystore_dir, - base_name <> ".json" - ), - keystore_file - ) - - File.write!( - Path.join( - keystore_pass_dir, - base_name <> ".txt" - ), - password_file - ) + File.write!(get_keystore_file(base_name), keystore_file) + File.write!(get_keystore_pass_file(base_name), password_file) Libp2pPort.add_validator(keystore) @@ -90,29 +71,12 @@ defmodule KeyStoreApi.V1.KeyStoreController do def delete_keys(conn, _params) do body_params = conn.private.open_api_spex.body_params - config = - Application.get_env(:lambda_ethereum_consensus, LambdaEthereumConsensus.Validator.Setup, []) - - keystore_dir = Keyword.get(config, :keystore_dir) || @default_keystore_dir - keystore_pass_dir = Keyword.get(config, :keystore_pass_dir) || @default_keystore_pass_dir - results = Enum.map(body_params.pubkeys, fn pubkey -> case Libp2pPort.delete_validator(pubkey |> Utils.hex_decode()) do :ok -> - File.rm!( - Path.join( - keystore_dir, - pubkey <> ".json" - ) - ) - - File.rm!( - Path.join( - keystore_pass_dir, - pubkey <> ".txt" - ) - ) + File.rm!(get_keystore_file(pubkey)) + File.rm!(get_keystore_pass_file(pubkey)) %{ status: "deleted", @@ -129,4 +93,7 @@ defmodule KeyStoreApi.V1.KeyStoreController do "data" => results }) end + + defp get_keystore_file(base_name), do: Path.join(@keystore_dir, base_name <> ".json") + defp get_keystore_pass_file(base_name), do: Path.join(@keystore_pass_dir, base_name <> ".txt") end diff --git a/lib/keystore.ex b/lib/keystore.ex index 92095ce21..cb071838b 100644 --- a/lib/keystore.ex +++ b/lib/keystore.ex @@ -146,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/libp2p_port.ex b/lib/libp2p_port.ex index 1c2ecbf4c..131715ed0 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -562,8 +562,10 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_call({:add_validator, keystore}, _from, %{validators: validators} = state) do - # TODO: HANDLE REPEATED VALIDATORS + # TODO: handle repeated validators current_status = ForkChoice.get_current_status_message() + validator = Validator.new({current_status.head_slot, current_status.head_root, keystore}) + Logger.warning("[Libp2pPort] Adding validator with index #{inspect(validator.index)}.") {:reply, :ok, %{ @@ -572,7 +574,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do Map.put( validators, keystore.pubkey, - Validator.new({current_status.head_slot, current_status.head_root, keystore}) + validator ) }} end From b49b168b4d46d4fed1d1af73d0831e205b4b8730 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Thu, 8 Aug 2024 21:29:46 -0300 Subject: [PATCH 11/26] fix: use other validators slot when adding a new validator --- lib/lambda_ethereum_consensus/validator/setup.ex | 2 -- lib/libp2p_port.ex | 10 +++++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex index 630bc96b7..00206e312 100644 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ b/lib/lambda_ethereum_consensus/validator/setup.ex @@ -57,8 +57,6 @@ defmodule LambdaEthereumConsensus.Validator.Setup do keystore_file = Path.join(keystore_dir, "#{base_name}.json") keystore_pass_file = Path.join(keystore_pass_dir, "#{base_name}.txt") - IO.inspect("KEYSTORE_FILE: #{inspect(keystore_file)}") - IO.inspect("KEYSTORE_PASS_FILE: #{inspect(keystore_pass_file)}") {keystore_file, keystore_pass_file} else Logger.warning("[Validator] Skipping file: #{filename}. Not a keystore file.") diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 131715ed0..7a2953de3 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -563,9 +563,13 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_call({:add_validator, keystore}, _from, %{validators: validators} = state) do # TODO: handle repeated validators - current_status = ForkChoice.get_current_status_message() - validator = Validator.new({current_status.head_slot, current_status.head_root, keystore}) - Logger.warning("[Libp2pPort] Adding validator with index #{inspect(validator.index)}.") + # TODO: 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, %{ From 2d2e77d9797b5035e5a4b195bc9bcc96baa8b29b Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Fri, 9 Aug 2024 10:13:41 -0300 Subject: [PATCH 12/26] refactor: restore pruning --- Makefile | 2 +- config/runtime.exs | 6 +-- .../fork_choice/fork_choice.ex | 46 +++++++++---------- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Makefile b/Makefile index 1823ebbae..8b66dda22 100644 --- a/Makefile +++ b/Makefile @@ -168,7 +168,7 @@ checkpoint-sync: compile-all #▶️ sepolia: @ Run an interactive terminal using sepolia network sepolia: compile-all - iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics --validator-api-port 5056 + iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics #▶️ holesky: @ Run an interactive terminal using holesky network holesky: compile-all iex -S mix run -- --checkpoint-sync-url https://checkpoint-sync.holesky.ethpandaops.io --network holesky diff --git a/config/runtime.exs b/config/runtime.exs index 0eb75ffb6..c187a1329 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -18,7 +18,7 @@ switches = [ log_file: :string, beacon_api: :boolean, beacon_api_port: :integer, - keystore_api: :boolean, + validator_api: :boolean, validator_api_port: :integer, listen_address: [:string, :keep], discovery_port: :integer, @@ -50,7 +50,7 @@ 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_keystore_api = Keyword.get(args, :keystore_api, not is_nil(validator_api_port)) +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, "") @@ -159,7 +159,7 @@ config :lambda_ethereum_consensus, BeaconApi.Endpoint, # KeyStore API config :lambda_ethereum_consensus, KeyStoreApi.Endpoint, - server: enable_keystore_api, + server: enable_validator_api, http: [port: validator_api_port || 5000], url: [host: "localhost"], render_errors: [ diff --git a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex index 6c1a6bfd7..7f2b19123 100644 --- a/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex +++ b/lib/lambda_ethereum_consensus/fork_choice/fork_choice.ex @@ -12,11 +12,11 @@ defmodule LambdaEthereumConsensus.ForkChoice do alias LambdaEthereumConsensus.P2P.Gossip.OperationsCollector alias LambdaEthereumConsensus.StateTransition.Accessors alias LambdaEthereumConsensus.StateTransition.Misc - # alias LambdaEthereumConsensus.Store.BlobDb - # alias LambdaEthereumConsensus.Store.BlockDb + alias LambdaEthereumConsensus.Store.BlobDb + alias LambdaEthereumConsensus.Store.BlockDb alias LambdaEthereumConsensus.Store.Blocks alias LambdaEthereumConsensus.Store.CheckpointStates - # alias LambdaEthereumConsensus.Store.StateDb + alias LambdaEthereumConsensus.Store.StateDb alias LambdaEthereumConsensus.Store.StoreDb alias Types.Attestation alias Types.BlockInfo @@ -175,26 +175,26 @@ defmodule LambdaEthereumConsensus.ForkChoice do ### Private Functions ########################## - defp prune_old_states(_last_finalized_epoch, _new_finalized_epoch) do - # if last_finalized_epoch < new_finalized_epoch do - # new_finalized_slot = - # new_finalized_epoch * ChainSpec.get("SLOTS_PER_EPOCH") - - # Task.Supervisor.start_child( - # PruneStatesSupervisor, - # fn -> StateDb.prune_states_older_than(new_finalized_slot) end - # ) - - # Task.Supervisor.start_child( - # PruneBlocksSupervisor, - # fn -> BlockDb.prune_blocks_older_than(new_finalized_slot) end - # ) - - # Task.Supervisor.start_child( - # PruneBlobsSupervisor, - # fn -> BlobDb.prune_old_blobs(new_finalized_slot) end - # ) - # end + defp prune_old_states(last_finalized_epoch, new_finalized_epoch) do + if last_finalized_epoch < new_finalized_epoch do + new_finalized_slot = + new_finalized_epoch * ChainSpec.get("SLOTS_PER_EPOCH") + + Task.Supervisor.start_child( + PruneStatesSupervisor, + fn -> StateDb.prune_states_older_than(new_finalized_slot) end + ) + + Task.Supervisor.start_child( + PruneBlocksSupervisor, + fn -> BlockDb.prune_blocks_older_than(new_finalized_slot) end + ) + + Task.Supervisor.start_child( + PruneBlobsSupervisor, + fn -> BlobDb.prune_old_blobs(new_finalized_slot) end + ) + end end def apply_handler(iter, state, handler) do From 206dee6ccf4ca8565dd76b947d1e8e5b71e04a87 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Fri, 9 Aug 2024 10:51:00 -0300 Subject: [PATCH 13/26] Update README.md --- README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 75414b851..601003924 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)). From bee3af166abeeb56a38ecd475f440582b47a8727 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Fri, 9 Aug 2024 13:19:16 -0300 Subject: [PATCH 14/26] refactor: nit changes --- Makefile | 1 + README.md | 1 + .../controllers/v1/key_store_controller.ex | 14 ++++++++------ lib/libp2p_port.ex | 6 +++--- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 8b66dda22..a6f62a7b3 100644 --- a/Makefile +++ b/Makefile @@ -169,6 +169,7 @@ checkpoint-sync: compile-all #▶️ sepolia: @ Run an interactive terminal using sepolia network sepolia: compile-all iex -S mix run -- --checkpoint-sync-url https://sepolia.beaconstate.info --network sepolia --metrics + #▶️ holesky: @ Run an interactive terminal using holesky network holesky: compile-all iex -S mix run -- --checkpoint-sync-url https://checkpoint-sync.holesky.ethpandaops.io --network holesky diff --git a/README.md b/README.md index 601003924..baa2d7f33 100644 --- a/README.md +++ b/README.md @@ -271,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/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index e979d70b3..440000765 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -50,8 +50,8 @@ defmodule KeyStoreApi.V1.KeyStoreController do base_name = keystore.pubkey |> Utils.hex_encode() - File.write!(get_keystore_file(base_name), keystore_file) - File.write!(get_keystore_pass_file(base_name), password_file) + File.write!(get_keystore_file_path(base_name), keystore_file) + File.write!(get_keystore_pass_file_path(base_name), password_file) Libp2pPort.add_validator(keystore) @@ -75,8 +75,8 @@ defmodule KeyStoreApi.V1.KeyStoreController do Enum.map(body_params.pubkeys, fn pubkey -> case Libp2pPort.delete_validator(pubkey |> Utils.hex_decode()) do :ok -> - File.rm!(get_keystore_file(pubkey)) - File.rm!(get_keystore_pass_file(pubkey)) + File.rm!(get_keystore_file_path(pubkey)) + File.rm!(get_keystore_pass_file_path(pubkey)) %{ status: "deleted", @@ -94,6 +94,8 @@ defmodule KeyStoreApi.V1.KeyStoreController do }) end - defp get_keystore_file(base_name), do: Path.join(@keystore_dir, base_name <> ".json") - defp get_keystore_pass_file(base_name), do: Path.join(@keystore_pass_dir, base_name <> ".txt") + 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/libp2p_port.ex b/lib/libp2p_port.ex index 7a2953de3..919b41baf 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -84,7 +84,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do discovery_addresses: [String.t()] } - @sync_delay_millis 20_000 + @sync_delay_millis 10_000 ###################### ### API @@ -562,8 +562,8 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_call({:add_validator, keystore}, _from, %{validators: validators} = state) do - # TODO: handle repeated validators - # TODO: handle 0 validators + # TODO (#1263): handle 0 validators + # TODO (#1264): handle repeated validators first_validator = validators |> Map.values() |> List.first() validator = Validator.new({first_validator.slot, first_validator.root, keystore}) From 01a7371c23bd04c666e5f27a9491beed8bad3dcf Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Fri, 9 Aug 2024 23:11:11 -0300 Subject: [PATCH 15/26] doc: add doc to the controller functions --- .../controllers/v1/key_store_controller.ex | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index 440000765..dec1bbe49 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -23,6 +23,9 @@ defmodule KeyStoreApi.V1.KeyStoreController do 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 @@ -39,19 +42,25 @@ defmodule KeyStoreApi.V1.KeyStoreController do }) 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_file, password_file} -> - keystore = Keystore.decode_str!(keystore_file, password_file) + |> 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() - File.write!(get_keystore_file_path(base_name), keystore_file) - File.write!(get_keystore_pass_file_path(base_name), password_file) + 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) @@ -67,6 +76,11 @@ defmodule KeyStoreApi.V1.KeyStoreController do }) 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 From 6056eaa4b0fe318dce10a4fdb9c0fd0faa1374b0 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Sat, 10 Aug 2024 20:31:24 -0300 Subject: [PATCH 16/26] overrides repeated credentials --- lib/key_store_api/controllers/v1/key_store_controller.ex | 2 +- lib/libp2p_port.ex | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index dec1bbe49..ce91e1dc2 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -59,9 +59,9 @@ defmodule KeyStoreApi.V1.KeyStoreController do 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) %{ diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 919b41baf..18a2baf34 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -563,7 +563,6 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_call({:add_validator, keystore}, _from, %{validators: validators} = state) do # TODO (#1263): handle 0 validators - # TODO (#1264): handle repeated validators first_validator = validators |> Map.values() |> List.first() validator = Validator.new({first_validator.slot, first_validator.root, keystore}) From d3b1d20ee6a6a62613141c71205aabbf04962238 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Sat, 10 Aug 2024 21:00:13 -0300 Subject: [PATCH 17/26] fix: try fix dializer --- lib/lambda_ethereum_consensus/validator/validator.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 9aeee3385..262992fd7 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -44,7 +44,7 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } - @spec new({Types.slot(), Types.root(), Keystore.t()}) :: state + @spec new({Types.slot(), Types.root(), Keystore.t()}) :: state() def new({head_slot, head_root, keystore}) do state = %__MODULE__{ slot: head_slot, @@ -69,7 +69,7 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec try_setup_validator(state, Types.slot(), Types.root()) :: state | nil + @spec try_setup_validator(state(), Types.slot(), Types.root()) :: state() | nil defp try_setup_validator(state, slot, root) do epoch = Misc.compute_epoch_at_slot(slot) beacon = fetch_target_state(epoch, root) From 51b80a1be66e4e303e56853f4a4b3e7888436666 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 12:10:46 -0300 Subject: [PATCH 18/26] refactor: use pubkey instead of pk for readability --- lib/libp2p_port.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 18a2baf34..f9496cc9e 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -545,18 +545,18 @@ defmodule LambdaEthereumConsensus.Libp2pPort do @impl GenServer def handle_call(:get_keystores, _from, %{validators: validators} = state), - do: {:reply, Enum.map(validators, fn {_pk, validator} -> validator.keystore end), state} + do: {:reply, Enum.map(validators, fn {_pubkey, validator} -> validator.keystore end), state} @impl GenServer - def handle_call({:delete_validator, pk}, _from, %{validators: validators} = state) do - case Map.fetch(validators, pk) do + 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, pk)}} + {:reply, :ok, %{state | validators: Map.delete(validators, pubkey)}} :error -> - {:error, "Pubkey #{inspect(pk)} not found."} + {:error, "Pubkey #{inspect(pubkey)} not found."} end end From 9f62d43e45f093de679ae053bc663833a5cccefd Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 12:25:51 -0300 Subject: [PATCH 19/26] refactor: remove unnecesary call in Libp2pPort --- lib/libp2p_port.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index f9496cc9e..a0ad485bb 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -539,10 +539,6 @@ defmodule LambdaEthereumConsensus.Libp2pPort do {:noreply, state} end - @impl GenServer - def handle_call(:get_keystores, _from, %{validators: []} = state), - do: {:reply, [], state} - @impl GenServer def handle_call(:get_keystores, _from, %{validators: validators} = state), do: {:reply, Enum.map(validators, fn {_pubkey, validator} -> validator.keystore end), state} From b96a2e991aba37f0c2999eb3ae7228381ec6b95d Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 14:51:59 -0300 Subject: [PATCH 20/26] refactor: handle malformed pubkeys --- lib/beacon_api/utils.ex | 7 ++---- .../controllers/v1/key_store_controller.ex | 23 +++++++++++-------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/beacon_api/utils.ex b/lib/beacon_api/utils.ex index df0f3fde0..2fce31c6c 100644 --- a/lib/beacon_api/utils.ex +++ b/lib/beacon_api/utils.ex @@ -31,11 +31,8 @@ defmodule BeaconApi.Utils do "0x" <> Base.encode16(binary, case: :lower) end - def hex_decode("0x" <> binary) do - with {:ok, decoded} <- Base.decode16(binary, case: :lower) do - decoded - end - 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() diff --git a/lib/key_store_api/controllers/v1/key_store_controller.ex b/lib/key_store_api/controllers/v1/key_store_controller.ex index ce91e1dc2..2db52b413 100644 --- a/lib/key_store_api/controllers/v1/key_store_controller.ex +++ b/lib/key_store_api/controllers/v1/key_store_controller.ex @@ -87,18 +87,23 @@ defmodule KeyStoreApi.V1.KeyStoreController do results = Enum.map(body_params.pubkeys, fn pubkey -> - case Libp2pPort.delete_validator(pubkey |> Utils.hex_decode()) do - :ok -> - File.rm!(get_keystore_file_path(pubkey)) - File.rm!(get_keystore_pass_file_path(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: "deleted", - message: "Pubkey: #{inspect(pubkey)}" + status: "error", + message: "Error removing key #{inspect(pubkey)}. Reason: #{inspect(reason)}" } - - {:error, reason} -> - Logger.error("[Keystore] Error removing key. Reason: #{reason}") end end) From e2487426dce94362074a65be389a4918c6575187 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 14:52:41 -0300 Subject: [PATCH 21/26] fix: dialyzer --- lib/lambda_ethereum_consensus/validator/setup.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex index 00206e312..6c09f9670 100644 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ b/lib/lambda_ethereum_consensus/validator/setup.ex @@ -46,7 +46,7 @@ defmodule LambdaEthereumConsensus.Validator.Setup do - /.txt """ @spec decode_validator_keystores(binary(), binary()) :: - list({Bls.pubkey(), Bls.privkey()}) + 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) From d3d2eb28adae7bd87e8eedb28e1be06331179a40 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 15:04:49 -0300 Subject: [PATCH 22/26] refactor: restore network params --- network_params.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network_params.yaml b/network_params.yaml index 5c564f285..102796866 100644 --- a/network_params.yaml +++ b/network_params.yaml @@ -2,12 +2,12 @@ participants: - el_type: geth cl_type: lighthouse count: 2 - validator_count: 1 + validator_count: 32 - el_type: geth cl_type: lambda cl_image: lambda_ethereum_consensus:latest use_separate_vc: false count: 1 - validator_count: 63 + validator_count: 32 cl_max_mem: 4096 keymanager_enabled: true From c0478abf8055c6a38e4a3997e4cdaf27e059167c Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 16:39:38 -0300 Subject: [PATCH 23/26] fix: update handle_tick and handle_head params --- lib/lambda_ethereum_consensus/validator/validator.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 262992fd7..d4e14b183 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -97,7 +97,7 @@ defmodule LambdaEthereumConsensus.Validator do end @spec handle_new_head(Types.slot(), Types.root(), state) :: state - def handle_new_head(slot, head_root, %{validator: %{index: nil}} = state) do + 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 @@ -117,7 +117,7 @@ defmodule LambdaEthereumConsensus.Validator do end @spec handle_tick({Types.slot(), atom()}, state) :: state - def handle_tick(_logical_time, %{validator: %{index: nil}} = state) do + def handle_tick(_logical_time, %{index: nil} = state) do log_error("-1", "setup validator", "index not present for handle tick") state end From 9ba09252d75d5cae5c10774222e06cd3f50c6437 Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 17:07:40 -0300 Subject: [PATCH 24/26] refactor: remove tuple in Validator.new() --- lib/lambda_ethereum_consensus/validator/setup.ex | 2 +- lib/lambda_ethereum_consensus/validator/validator.ex | 4 ++-- lib/libp2p_port.ex | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex index 6c09f9670..1260f2379 100644 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ b/lib/lambda_ethereum_consensus/validator/setup.ex @@ -30,7 +30,7 @@ defmodule LambdaEthereumConsensus.Validator.Setup do validators = validator_keystores |> Enum.map(fn keystore -> - {keystore.pubkey, Validator.new({slot, head_root, keystore})} + {keystore.pubkey, Validator.new(slot, head_root, keystore)} end) |> Map.new() diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index d4e14b183..4884121d5 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -44,8 +44,8 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } - @spec new({Types.slot(), Types.root(), Keystore.t()}) :: state() - def new({head_slot, head_root, keystore}) do + @spec new(Types.slot(), Types.root(), Keystore.t()) :: state() + def new(head_slot, head_root, keystore) do state = %__MODULE__{ slot: head_slot, epoch: Misc.compute_epoch_at_slot(head_slot), diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index a0ad485bb..5bb7a8743 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -560,7 +560,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do def handle_call({:add_validator, 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}) + 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)}." From 691374f4bc5c336daf843c08410704e9fd353b7a Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 17:25:35 -0300 Subject: [PATCH 25/26] refactor: enforce the Keystore struct in Libp2pPort.add_validator --- lib/lambda_ethereum_consensus/validator/setup.ex | 2 +- lib/lambda_ethereum_consensus/validator/validator.ex | 4 ++-- lib/libp2p_port.ex | 10 +++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex index 1260f2379..6c09f9670 100644 --- a/lib/lambda_ethereum_consensus/validator/setup.ex +++ b/lib/lambda_ethereum_consensus/validator/setup.ex @@ -30,7 +30,7 @@ defmodule LambdaEthereumConsensus.Validator.Setup do validators = validator_keystores |> Enum.map(fn keystore -> - {keystore.pubkey, Validator.new(slot, head_root, keystore)} + {keystore.pubkey, Validator.new({slot, head_root, keystore})} end) |> Map.new() diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index 4884121d5..d4e14b183 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -44,8 +44,8 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } - @spec new(Types.slot(), Types.root(), Keystore.t()) :: state() - def new(head_slot, head_root, keystore) do + @spec new({Types.slot(), Types.root(), Keystore.t()}) :: state() + def new({head_slot, head_root, keystore}) do state = %__MODULE__{ slot: head_slot, epoch: Misc.compute_epoch_at_slot(head_slot), diff --git a/lib/libp2p_port.ex b/lib/libp2p_port.ex index 5bb7a8743..afc7d12dd 100644 --- a/lib/libp2p_port.ex +++ b/lib/libp2p_port.ex @@ -557,10 +557,14 @@ defmodule LambdaEthereumConsensus.Libp2pPort do end @impl GenServer - def handle_call({:add_validator, keystore}, _from, %{validators: validators} = state) do + 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) + 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)}." @@ -572,7 +576,7 @@ defmodule LambdaEthereumConsensus.Libp2pPort do | validators: Map.put( validators, - keystore.pubkey, + pubkey, validator ) }} From e72ea94ff8776947d0002f598eed5c718a5eb87c Mon Sep 17 00:00:00 2001 From: avilagaston9 Date: Mon, 12 Aug 2024 23:46:48 -0300 Subject: [PATCH 26/26] fix: fetch_validator_index uses pubkeys instead of privkeys --- .../validator/setup.ex | 2 +- .../validator/validator.ex | 28 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/lambda_ethereum_consensus/validator/setup.ex b/lib/lambda_ethereum_consensus/validator/setup.ex index 6c09f9670..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) diff --git a/lib/lambda_ethereum_consensus/validator/validator.ex b/lib/lambda_ethereum_consensus/validator/validator.ex index d4e14b183..50a468607 100644 --- a/lib/lambda_ethereum_consensus/validator/validator.ex +++ b/lib/lambda_ethereum_consensus/validator/validator.ex @@ -34,7 +34,7 @@ defmodule LambdaEthereumConsensus.Validator do # 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(), @@ -44,7 +44,7 @@ defmodule LambdaEthereumConsensus.Validator do payload_builder: {Types.slot(), Types.root(), BlockBuilder.payload_id()} | nil } - @spec new({Types.slot(), Types.root(), Keystore.t()}) :: state() + @spec new({Types.slot(), Types.root(), Keystore.t()}) :: t() def new({head_slot, head_root, keystore}) do state = %__MODULE__{ slot: head_slot, @@ -69,7 +69,7 @@ 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) @@ -96,7 +96,7 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec handle_new_head(Types.slot(), Types.root(), state) :: state + @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, @@ -116,7 +116,7 @@ defmodule LambdaEthereumConsensus.Validator do |> maybe_build_payload(slot + 1) end - @spec handle_tick({Types.slot(), atom()}, state) :: state + @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 @@ -151,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 @@ -166,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 @@ -222,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 -> @@ -238,7 +238,7 @@ defmodule LambdaEthereumConsensus.Validator do end end - @spec attest(state, Duties.attester_duty()) :: :ok + @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) @@ -379,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(), Bls.privkey()) :: + @spec fetch_validator_index(Types.BeaconState.t(), Bls.pubkey()) :: non_neg_integer() | nil - defp fetch_validator_index(beacon, 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) @@ -396,7 +396,7 @@ 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