From 7a880dd23c4a95cc2cb74ae329116406d5c0ae9e Mon Sep 17 00:00:00 2001 From: Pawan Dhananjay Date: Fri, 14 Feb 2020 13:47:12 +0530 Subject: [PATCH] Update docs for api endpoints (#852) * Add docs for /beacon endpoints * Add docs for /network endpoints * Add docs for /spec endpoint * Add docs for /advanced endpoint * Add docs for /validator endpoint * Minor fixes * Address reviewer comments --- book/src/SUMMARY.md | 2 + book/src/http.md | 2 + book/src/http_advanced.md | 115 ++++++++++++++++++++ book/src/http_beacon.md | 93 ++++++++++++++++ book/src/http_network.md | 73 +++++++++++++ book/src/http_spec.md | 154 +++++++++++++++++++++++++++ book/src/http_validator.md | 212 +++++++++++++++++++++++++++++++++++++ 7 files changed, 651 insertions(+) create mode 100644 book/src/http_advanced.md create mode 100644 book/src/http_spec.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 0126fe714..f9a2745e8 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -15,6 +15,8 @@ * [/validator](./http_validator.md) * [/consensus](./http_consensus.md) * [/network](./http_network.md) + * [/spec](./http_spec.md) + * [/advanced](./http_advanced.md) * [WebSocket](./websockets.md) * [Advanced Usage](./advanced.md) * [Database Configuration](./advanced_database.md) diff --git a/book/src/http.md b/book/src/http.md index deef0088c..042881af0 100644 --- a/book/src/http.md +++ b/book/src/http.md @@ -18,6 +18,8 @@ Endpoint | Description | [`/validator`](./http_validator.md) | Provides functionality to validator clients. [`/consensus`](./http_consensus.md) | Proof-of-stake voting statistics. [`/network`](./http_network.md) | Information about the p2p network. +[`/spec`](./http_spec.md) | Information about the specs that the client is running. +[`/advanced`](./http_advanced.md) | Provides endpoints for advanced inspection of Lighthouse specific objects. _Please note: The OpenAPI format at [SwaggerHub: Lighthouse REST diff --git a/book/src/http_advanced.md b/book/src/http_advanced.md new file mode 100644 index 000000000..01e20b446 --- /dev/null +++ b/book/src/http_advanced.md @@ -0,0 +1,115 @@ +# Lighthouse REST API: `/spec` + +The `/advanced` endpoints provide information Lighthouse specific data structures for advanced debugging. + +## Endpoints + +HTTP Path | Description | +| --- | -- | +[`/advanced/fork_choice`](#advancedfork_choice) | Get the `proto_array` fork choice object. +[`/advanced/operation_pool`](#advancedoperation_pool) | Get the Lighthouse `PersistedOperationPool` object. + + +## `/advanced/fork_choice` + +Requests the `proto_array` fork choice object as represented in Lighthouse. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/advanced/fork_choice` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +{ + "prune_threshold": 256, + "justified_epoch": 25, + "finalized_epoch": 24, + "nodes": [ + { + "slot": 544, + "root": "0x27103c56d4427cb4309dd202920ead6381d54d43277c29cf0572ddf0d528e6ea", + "parent": null, + "justified_epoch": 16, + "finalized_epoch": 15, + "weight": 256000000000, + "best_child": 1, + "best_descendant": 296 + }, + { + "slot": 545, + "root": "0x09af0e8d4e781ea4280c9c969d168839c564fab3a03942e7db0bfbede7d4c745", + "parent": 0, + "justified_epoch": 16, + "finalized_epoch": 15, + "weight": 256000000000, + "best_child": 2, + "best_descendant": 296 + }, + ], + "indices": { + "0xb935bb3651eeddcb2d2961bf307156850de982021087062033f02576d5df00a3": 59, + "0x8f4ec47a34c6c1d69ede64d27165d195f7e2a97c711808ce51f1071a6e12d5b9": 189, + "0xf675eba701ef77ee2803a130dda89c3c5673a604d2782c9e25ea2be300d7d2da": 173, + "0x488a483c8d5083faaf5f9535c051b9f373ba60d5a16e77ddb1775f248245b281": 37 + } +} +``` +_Truncated for brevity._ + +## `/advanced/operation_pool` + +Requests the `PersistedOperationPool` object as represented in Lighthouse. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/advanced/operation_pool` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +{ + "attestations": [ + [ + { + "v": [39, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 112, 118, 215, 252, 51, 186, 76, 156, 157, 99, 91, 4, 137, 195, 209, 224, 26, 233, 233, 184, 38, 89, 215, 177, 247, 97, 243, 119, 229, 69, 50, 90, 24, 0, 0, 0, 0, 0, 0, 0, 79, 37, 38, 210, 96, 235, 121, 142, 129, 136, 206, 214, 179, 132, 22, 19, 222, 213, 203, 46, 112, 192, 26, 5, 254, 26, 103, 170, 158, 205, 72, 3, 25, 0, 0, 0, 0, 0, 0, 0, 164, 50, 214, 67, 98, 13, 50, 180, 108, 232, 248, 109, 128, 45, 177, 23, 221, 24, 218, 211, 8, 152, 172, 120, 24, 86, 198, 103, 68, 164, 67, 202, 1, 0, 0, 0, 0, 0, 0, 0] + }, + [ + { + "aggregation_bits": "0x03", + "data": { + "slot": 807, + "index": 0, + "beacon_block_root": "0x7076d7fc33ba4c9c9d635b0489c3d1e01ae9e9b82659d7b1f761f377e545325a", + "source": { + "epoch": 24, + "root": "0x4f2526d260eb798e8188ced6b3841613ded5cb2e70c01a05fe1a67aa9ecd4803" + }, + "target": { + "epoch": 25, + "root": "0xa432d643620d32b46ce8f86d802db117dd18dad30898ac781856c66744a443ca" + } + }, + "signature": "0x8b1d624b0cd5a7a0e13944e90826878a230e3901db34ea87dbef5b145ade2fedbc830b6752a38a0937a1594211ab85b615d65f9eef0baccd270acca945786036695f4db969d9ff1693c505c0fe568b2fe9831ea78a74cbf7c945122231f04026" + } + ] + ] + ], + "attester_slashings": [], + "proposer_slashings": [], + "voluntary_exits": [] +} +``` +_Truncated for brevity._ \ No newline at end of file diff --git a/book/src/http_beacon.md b/book/src/http_beacon.md index ca994bc4f..309ed6437 100644 --- a/book/src/http_beacon.md +++ b/book/src/http_beacon.md @@ -13,6 +13,9 @@ HTTP Path | Description | [`/beacon/block`](#beaconblock) | Get a `SignedBeaconBlock` by slot or root. [`/beacon/state_root`](#beaconstate_root) | Resolve a slot to a state root. [`/beacon/state`](#beaconstate) | Get a `BeaconState` by slot or root. +[`/beacon/state/genesis`](#beaconstategenesis) | Get a `BeaconState` at genesis. +[`/beacon/genesis_time`](#beacongenesis_time) | Get the genesis time from the beacon state. +[`/beacon/fork`](#beaconfork) | Get the fork of the head of the chain. [`/beacon/validators`](#beaconvalidators) | Query for one or more validators. [`/beacon/validators/all`](#beaconvalidatorsall) | Get all validators. [`/beacon/validators/active`](#beaconvalidatorsactive) | Get all active validators. @@ -233,6 +236,96 @@ and its tree hash root. _Truncated for brevity._ +## `/beacon/state/genesis` + +Request that the node return a beacon chain state at genesis (slot 0). + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/state/genesis` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + + +### Returns + +Returns an object containing the genesis +[`BeaconState`](https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/beacon-chain.md#beaconstate). + +### Example Response + +```json +{ + "genesis_time": 1581576353, + "slot": 0, + "fork": { + "previous_version": "0x00000000", + "current_version": "0x00000000", + "epoch": 0 + }, +} +``` + +_Truncated for brevity._ + +## `/beacon/genesis_time` + +Request that the node return the genesis time from the beacon state. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/genesis_time` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + + +### Returns + +Returns an object containing the genesis time. + +### Example Response + +```json +1581576353 +``` + +## `/beacon/fork` + +Request that the node return the `fork` of the current head. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/beacon/fork` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + + +### Returns + +Returns an object containing the [`Fork`](https://github.com/ethereum/eth2.0-specs/blob/v0.10.1/specs/phase0/beacon-chain.md#fork) of the current head. + +### Example Response + +```json +{ + "previous_version": "0x00000000", + "current_version": "0x00000000", + "epoch": 0 +} +``` + ## `/beacon/validators` Request that the node returns information about one or more validator public diff --git a/book/src/http_network.md b/book/src/http_network.md index 2f1d6e9b2..8b0e73257 100644 --- a/book/src/http_network.md +++ b/book/src/http_network.md @@ -8,8 +8,11 @@ Lighthouse uses to communicate with other beacon nodes. HTTP Path | Description | | --- | -- | [`/network/peer_id`](#networkpeer_id) | Get a node's libp2p `PeerId`. +[`/network/peer_count`](#networkpeer_count) | Get the count of connected peers. [`/network/peers`](#networkpeers) | List a node's libp2p peers (as `PeerIds`). [`/network/enr`](#networkenr) | Get a node's discovery `ENR` address. +[`/network/listen_port`](#networklisten_port) | Get a node's libp2p listening port. +[`/network/listen_addresses`](#networklisten_addresses) | Get a list of libp2p multiaddr the node is listening on. ## `/network/peer_id` @@ -31,6 +34,26 @@ Typical Responses | 200 "QmVFcULBYZecPdCKgGmpEYDqJLqvMecfhJadVBtB371Avd" ``` +## `/network/peer_count` + +Requests the count of peers connected to the client. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/network/peer_id` +Method | GET +JSON Encoding | Number +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +5 +``` + ## `/network/peers` Requests one `MultiAddr` for each peer connected to the beacon node. @@ -73,3 +96,53 @@ Typical Responses | 200 ```json "-IW4QPYyGkXJSuJ2Eji8b-m4PTNrW4YMdBsNOBrYAdCk8NLMJcddAiQlpcv6G_hdNjiLACOPTkqTBhUjnC0wtIIhyQkEgmlwhKwqAPqDdGNwgiMog3VkcIIjKIlzZWNwMjU2azGhA1sBKo0yCfw4Z_jbggwflNfftjwKACu-a-CoFAQHJnrm" ``` + +## `/network/listen_port` + +Requests the TCP port that the client's libp2p service is listening on. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/network/listen_port` +Method | GET +JSON Encoding | Number +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +9000 +``` + +## `/network/listen_addresses` + +Requests the list of multiaddr that the client's libp2p service is listening on. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/network/listen_addresses` +Method | GET +JSON Encoding | Array +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +[ + "/ip4/127.0.0.1/tcp/9000", + "/ip4/192.168.31.115/tcp/9000", + "/ip4/172.24.0.1/tcp/9000", + "/ip4/172.21.0.1/tcp/9000", + "/ip4/172.17.0.1/tcp/9000", + "/ip4/172.18.0.1/tcp/9000", + "/ip4/172.19.0.1/tcp/9000", + "/ip4/172.42.0.1/tcp/9000", + "/ip6/::1/tcp/9000" +] +``` \ No newline at end of file diff --git a/book/src/http_spec.md b/book/src/http_spec.md new file mode 100644 index 000000000..9fb8c0c98 --- /dev/null +++ b/book/src/http_spec.md @@ -0,0 +1,154 @@ +# Lighthouse REST API: `/spec` + +The `/spec` endpoints provide information about Eth2.0 specifications that the node is running. + +## Endpoints + +HTTP Path | Description | +| --- | -- | +[`/spec`](#spec) | Get the full spec object that a node's running. +[`/spec/slots_per_epoch`](#specslots_per_epoch) | Get the number of slots per epoch. +[`/spec/eth2_config`](#specseth2_config) | Get the full Eth2 config object. + +## `/spec` + +Requests the full spec object that a node's running. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/spec` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +{ + "genesis_slot": 0, + "base_rewards_per_epoch": 4, + "deposit_contract_tree_depth": 32, + "max_committees_per_slot": 64, + "target_committee_size": 128, + "min_per_epoch_churn_limit": 4, + "churn_limit_quotient": 65536, + "shuffle_round_count": 90, + "min_genesis_active_validator_count": 16384, + "min_genesis_time": 1578009600, + "min_deposit_amount": 1000000000, + "max_effective_balance": 32000000000, + "ejection_balance": 16000000000, + "effective_balance_increment": 1000000000, + "genesis_fork_version": "0x00000000", + "bls_withdrawal_prefix_byte": "0x00", + "min_genesis_delay": 86400, + "milliseconds_per_slot": 12000, + "min_attestation_inclusion_delay": 1, + "min_seed_lookahead": 1, + "max_seed_lookahead": 4, + "min_epochs_to_inactivity_penalty": 4, + "min_validator_withdrawability_delay": 256, + "persistent_committee_period": 2048, + "base_reward_factor": 64, + "whistleblower_reward_quotient": 512, + "proposer_reward_quotient": 8, + "inactivity_penalty_quotient": 33554432, + "min_slashing_penalty_quotient": 32, + "domain_beacon_proposer": 0, + "domain_beacon_attester": 1, + "domain_randao": 2, + "domain_deposit": 3, + "domain_voluntary_exit": 4, + "safe_slots_to_update_justified": 8, + "eth1_follow_distance": 1024, + "seconds_per_eth1_block": 14, + "boot_nodes": [], + "network_id": 1 +} +``` + +## `/spec/eth2_config` + +Requests the full `Eth2Config` object. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/spec/eth2_config` +Method | GET +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +{ + "spec_constants": "mainnet", + "spec": { + "genesis_slot": 0, + "base_rewards_per_epoch": 4, + "deposit_contract_tree_depth": 32, + "max_committees_per_slot": 64, + "target_committee_size": 128, + "min_per_epoch_churn_limit": 4, + "churn_limit_quotient": 65536, + "shuffle_round_count": 90, + "min_genesis_active_validator_count": 16384, + "min_genesis_time": 1578009600, + "min_deposit_amount": 1000000000, + "max_effective_balance": 32000000000, + "ejection_balance": 16000000000, + "effective_balance_increment": 1000000000, + "genesis_fork_version": "0x00000000", + "bls_withdrawal_prefix_byte": "0x00", + "min_genesis_delay": 86400, + "milliseconds_per_slot": 12000, + "min_attestation_inclusion_delay": 1, + "min_seed_lookahead": 1, + "max_seed_lookahead": 4, + "min_epochs_to_inactivity_penalty": 4, + "min_validator_withdrawability_delay": 256, + "persistent_committee_period": 2048, + "base_reward_factor": 64, + "whistleblower_reward_quotient": 512, + "proposer_reward_quotient": 8, + "inactivity_penalty_quotient": 33554432, + "min_slashing_penalty_quotient": 32, + "domain_beacon_proposer": 0, + "domain_beacon_attester": 1, + "domain_randao": 2, + "domain_deposit": 3, + "domain_voluntary_exit": 4, + "safe_slots_to_update_justified": 8, + "eth1_follow_distance": 1024, + "seconds_per_eth1_block": 14, + "boot_nodes": [], + "network_id": 1 + } +} +``` + +## `/spec/slots_per_epoch` + +Requests the `SLOTS_PER_EPOCH` parameter from the specs that the node is running. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/spec/slots_per_epoch` +Method | GET +JSON Encoding | Number +Query Parameters | None +Typical Responses | 200 + +### Example Response + +```json +32 +``` \ No newline at end of file diff --git a/book/src/http_validator.md b/book/src/http_validator.md index 413a8279b..5f91002a6 100644 --- a/book/src/http_validator.md +++ b/book/src/http_validator.md @@ -10,6 +10,11 @@ HTTP Path | Description | [`/validator/duties`](#validatorduties) | Provides block and attestation production information for validators. [`/validator/duties/all`](#validatordutiesall) | Provides block and attestation production information for all validators. [`/validator/duties/active`](#validatordutiesactive) | Provides block and attestation production information for all active validators. +[`/validator/block`](#validatorblock) | Produces a `BeaconBlock` object from current state. +[`/validator/attestation`](#validatorattestation) | Produces an unsigned `Attestation` object from current state. +[`/validator/block`](#validatorblock) | Processes a `SignedBeaconBlock` object and publishes it to the network. +[`/validator/attestation`](#validatorattestation) | Processes a signed `Attestation` and publishes it to the network. + ## `/validator/duties` @@ -151,3 +156,210 @@ parameter. This parameter is required. ### Returns The return format is identical to the [Validator Duties](#validator-duties) response body. + +## `/validator/block` + +Produces and returns a `BeaconBlock` object from the current state. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/validator/block` +Method | GET +JSON Encoding | Object +Query Parameters | `slot`, `randao_reveal` +Typical Responses | 200 + +### Parameters + + +- `slot` (`Slot`): The slot number for which the block is to be produced. +- `randao_reveal` (`Signature`): 96 bytes `Signature` for the randomness. + + +### Returns + +Returns a `BeaconBlock` object. + +#### Response Body + +```json +{ + "slot": 100, + "parent_root": "0xb6528491793fcda1c0629a10886d0046a25e0f1375094304c2355d3db9594166", + "state_root": "0x8db5a59e109a30dd951fe40db6440659cae0f0c517f7c7025fee625a47e85eae", + "body": { + "randao_reveal": "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "eth1_data": { + "deposit_root": "0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", + "deposit_count": 8, + "block_hash": "0x2b32db6c2c0a6235fb1397e8225ea85e0f0e6e8c7b126d0016ccbde0e667151e" + }, + "graffiti": "0x736967702f6c69676874686f7573652d302e312e312d70726572656c65617365", + "proposer_slashings": [], + "attester_slashings": [], + "attestations": [], + "deposits": [], + "voluntary_exits": [] + } +} +``` + +## `/validator/attestation` + +Produces and returns an unsigned `Attestation` from the current state. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/validator/attestation` +Method | GET +JSON Encoding | Object +Query Parameters | `slot`, `committee_index` +Typical Responses | 200 + +### Parameters + + +- `slot` (`Slot`): The slot number for which the attestation is to be produced. +- `committee_index` (`CommitteeIndex`): The index of the committee that makes the attestation. + + +### Returns + +Returns a `Attestation` object with a default signature. The `signature` field should be replaced by the valid signature. + +#### Response Body + +```json +{ + "aggregation_bits": "0x01", + "data": { + "slot": 100, + "index": 0, + "beacon_block_root": "0xf22e4ec281136d119eabcd4d9d248aeacd042eb63d8d7642f73ad3e71f1c9283", + "source": { + "epoch": 2, + "root": "0x34c1244535c923f08e7f83170d41a076e4f1ec61013846b3a615a1d109d3c329" + }, + "target": { + "epoch": 3, + "root": "0xaefd23b384994dc0c1a6b77836bdb2f24f209ebfe6c4819324d9685f4a43b4e1" + } + }, + "signature": "0xc00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +} +``` + +## `/validator/block` + +Publishes a `SignedBeaconBlock` object to the network. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/validator/block` +Method | POST +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200/202 + + +### Request Body + +Expects a JSON encoded `SignedBeaconBlock` in the POST request body: + +### Returns + +Returns a null object if the block passed all block validation and is published to the network. +Else, returns a processing error description. + +### Example + +### Request Body + +```json +{ + "message": { + "slot": 33, + "parent_root": "0xf54de54bd33e33aee4706cffff4bd991bcbf522f2551ab007180479c63f4fe912", + "state_root": "0x615c887bad27bc05754d627d941e1730e1b4c77b2eb4378c195ac8a8203bbf26", + "body": { + "randao_reveal": "0x8d7b2a32b026e9c79aae6ec6b83eabae89d60cacd65ac41ed7d2f4be9dd8c89c1bf7cd3d700374e18d03d12f6a054c23006f64f0e4e8b7cf37d6ac9a4c7d815c858120c54673b7d3cb2bb1550a4d659eaf46e34515677c678b70d6f62dbf89f", + "eth1_data": { + "deposit_root": "0x66687aadf862bd776c8fc18b8e9f8e20089714856ee233b3902a591d0d5f2925", + "deposit_count": 8, + "block_hash": "0x2b32db6c2c0a6235fb1397e8225ea85e0f0e6e8c7b126d0016ccbde0e667151e" + }, + "graffiti": "0x736967702f6c69676874686f7573652d302e312e312d7076572656c65617365", + "proposer_slashings": [ + + ], + "attester_slashings": [ + + ], + "attestations": [ + + ], + "deposits": [ + + ], + "voluntary_exits": [ + + ] + } + }, + "signature": "0x965ced900dbabd0a78b81a0abb5d03407be0d38762104316416347f2ea6f82652b5759396f402e85df8ee18ba2c60145037c73b1c335f4272f1751a1cd89862b7b4937c035e350d0108554bd4a8930437ec3311c801a65fe8e5ba022689b5c24" +} +``` + +## `/validator/attestation` + +Publishes a `Attestation` object to the network. + +### HTTP Specification + +| Property | Specification | +| --- |--- | +Path | `/validator/attestation` +Method | POST +JSON Encoding | Object +Query Parameters | None +Typical Responses | 200/202 + + +### Request Body + +Expects a JSON encoded signed`Attestation` object in the POST request body: + +### Returns + +Returns a null object if the attestation passed all validation and is published to the network. +Else, returns a processing error description. + +### Example + +### Request Body + +```json +{ + "aggregation_bits": "0x03", + "data": { + "slot": 3, + "index": 0, + "beacon_block_root": "0x0b6a1f7a9baa38d00ef079ba861b7587662565ca2502fb9901741c1feb8bb3c9", + "source": { + "epoch": 0, + "root": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "target": { + "epoch": 0, + "root": "0xad2c360ab8c8523db278a7d7ced22f3810800f2fdc282defb6db216689d376bd" + } + }, + "signature": "0xb76a1768c18615b5ade91a92e7d2ed0294f7e088e56e30fbe7e3aa6799c443b11bccadd578ca2cbd95d395ab689b9e4d03c88a56641791ab38dfa95dc1f4d24d1b19b9d36c96c20147ad03$649bd3c6c7e8a39cf2ffb99e07b4964d52854559f" +} +``` \ No newline at end of file