Repository: ethereum/web3.py
Pull request #3504 (2024-10-03 05:34:41)

beacon: add a few various endpoints
### What was wrong?

Missing some endpoints I wanted to use in my application

### How was it fixed?

Added the endpoints via the beacon API spec -> https://ethereum.github.io/beacon-APIs/

I needed to add a new async post method. I tried to follow the patterns as much as possible here to make it fit well into the existing codebase.

I added tests for the new endpoints also.

### Todo:

- [x] Clean up commit history
- [x] Add or update documentation related to these changes
- [x] Add entry to the [release notes](https://github.com/ethereum/web3.py/blob/main/newsfragments/README.md)

🐶 


reedsa: @lrusak If you have capacity to add sync methods/tests to this that would be awesome! Let us know what you are comfortable with.
lrusak: @reedsa I added the `Beacon` class methods. Sorry I didn't respond earlier, life's been busy.

If acceptable I can squash the `Async/Sync` pair commits together and amend the commit messages.
lrusak: ```
$ pytest -rP tests/beacon/test_async_beacon.py
=================================================================================== test session starts ====================================================================================
platform linux -- Python 3.13.0rc3, pytest-8.3.3, pluggy-1.5.0 -- /home/lukas/Documents/git/web3.py/venv/bin/python3
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/lukas/Documents/git/web3.py/.hypothesis/examples'))
rootdir: /home/lukas/Documents/git/web3.py
configfile: pyproject.toml
plugins: xdist-3.6.1, anyio-4.6.0, hypothesis-6.112.2, flaky-3.8.1, mock-3.14.0, asyncio-0.21.2
asyncio: mode=Mode.STRICT
collected 41 items

tests/beacon/test_async_beacon.py::test_async_cl_beacon_raises_exception_on_invalid_url PASSED                                                                                       [  2%]
tests/beacon/test_async_beacon.py::test_async_beacon_user_request_timeout PASSED                                                                                                     [  4%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_genesis PASSED                                                                                                           [  7%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_hash_root PASSED                                                                                                         [  9%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_fork_data PASSED                                                                                                         [ 12%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_finality_checkpoint PASSED                                                                                               [ 14%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_validators PASSED                                                                                                        [ 17%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_validator PASSED                                                                                                         [ 19%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_validator_balances PASSED                                                                                                [ 21%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_epoch_committees PASSED                                                                                                  [ 24%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_epoch_sync_committees PASSED                                                                                             [ 26%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_epoch_randao PASSED                                                                                                      [ 29%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_block_headers PASSED                                                                                                     [ 31%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_block_header PASSED                                                                                                      [ 34%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_block PASSED                                                                                                             [ 36%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_block_root PASSED                                                                                                        [ 39%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_blinded_blocks PASSED                                                                                                    [ 41%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_rewards PASSED                                                                                                           [ 43%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_block_attestations PASSED                                                                                                [ 46%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_attestations PASSED                                                                                                      [ 48%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_attester_slashings PASSED                                                                                                [ 51%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_proposer_slashings PASSED                                                                                                [ 53%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_voluntary_exits PASSED                                                                                                   [ 56%]
tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_bls_to_execution_changes PASSED                                                                                          [ 58%]
tests/beacon/test_async_beacon.py::test_async_cl_config_get_fork_schedule PASSED                                                                                                     [ 60%]
tests/beacon/test_async_beacon.py::test_async_cl_config_get_spec PASSED                                                                                                              [ 63%]
tests/beacon/test_async_beacon.py::test_async_cl_config_get_deposit_contract PASSED                                                                                                  [ 65%]
tests/beacon/test_async_beacon.py::test_async_cl_debug_get_beacon_state PASSED                                                                                                       [ 68%]
tests/beacon/test_async_beacon.py::test_async_cl_debug_get_beacon_heads PASSED                                                                                                       [ 70%]
tests/beacon/test_async_beacon.py::test_async_cl_node_get_node_identity PASSED                                                                                                       [ 73%]
tests/beacon/test_async_beacon.py::test_async_cl_node_get_peers PASSED                                                                                                               [ 75%]
tests/beacon/test_async_beacon.py::test_async_cl_node_get_peer PASSED                                                                                                                [ 78%]
tests/beacon/test_async_beacon.py::test_async_cl_node_get_peer_count PASSED                                                                                                          [ 80%]
tests/beacon/test_async_beacon.py::test_async_cl_node_get_health PASSED                                                                                                              [ 82%]
tests/beacon/test_async_beacon.py::test_async_cl_node_get_version PASSED                                                                                                             [ 85%]
tests/beacon/test_async_beacon.py::test_async_cl_node_get_syncing PASSED                                                                                                             [ 87%]
tests/beacon/test_async_beacon.py::test_async_cl_node_get_blob_sidecars PASSED                                                                                                       [ 90%]
tests/beacon/test_async_beacon.py::test_async_cl_validator_get_attester_duties PASSED                                                                                                [ 92%]
tests/beacon/test_async_beacon.py::test_async_cl_validator_get_block_proposer_duties PASSED                                                                                          [ 95%]
tests/beacon/test_async_beacon.py::test_async_cl_validator_get_sync_committee_duties PASSED                                                                                          [ 97%]
tests/beacon/test_async_beacon.py::test_async_cl_validator_get_attestations_rewards PASSED                                                                                           [100%]

========================================================================================== PASSES ==========================================================================================
=================================================================================== slowest 10 durations ===================================================================================
17.46s call     tests/beacon/test_async_beacon.py::test_async_cl_validator_get_attestations_rewards
16.88s call     tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_validator
16.88s call     tests/beacon/test_async_beacon.py::test_async_cl_validator_get_attester_duties
16.80s call     tests/beacon/test_async_beacon.py::test_async_cl_validator_get_sync_committee_duties
16.75s call     tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_validators
14.96s call     tests/beacon/test_async_beacon.py::test_async_cl_debug_get_beacon_state
2.03s call     tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_validator_balances
0.45s call     tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_epoch_committees
0.17s call     tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_epoch_sync_committees
0.17s call     tests/beacon/test_async_beacon.py::test_async_cl_beacon_get_attestations
============================================================================== 41 passed in 103.08s (0:01:43) ==============================================================================
```
and
```
$ pytest -rP tests/beacon/test_beacon.py
=================================================================================== test session starts ====================================================================================
platform linux -- Python 3.13.0rc3, pytest-8.3.3, pluggy-1.5.0 -- /home/lukas/Documents/git/web3.py/venv/bin/python3
cachedir: .pytest_cache
hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase(PosixPath('/home/lukas/Documents/git/web3.py/.hypothesis/examples'))
rootdir: /home/lukas/Documents/git/web3.py
configfile: pyproject.toml
plugins: xdist-3.6.1, anyio-4.6.0, hypothesis-6.112.2, flaky-3.8.1, mock-3.14.0, asyncio-0.21.2
asyncio: mode=Mode.STRICT
collected 41 items

tests/beacon/test_beacon.py::test_cl_beacon_raises_exception_on_invalid_url PASSED                                                                                                   [  2%]
tests/beacon/test_beacon.py::test_beacon_user_defined_request_timeout PASSED                                                                                                         [  4%]
tests/beacon/test_beacon.py::test_cl_beacon_get_genesis PASSED                                                                                                                       [  7%]
tests/beacon/test_beacon.py::test_cl_beacon_get_hash_root PASSED                                                                                                                     [  9%]
tests/beacon/test_beacon.py::test_cl_beacon_get_fork_data PASSED                                                                                                                     [ 12%]
tests/beacon/test_beacon.py::test_cl_beacon_get_finality_checkpoint PASSED                                                                                                           [ 14%]
tests/beacon/test_beacon.py::test_cl_beacon_get_validators PASSED                                                                                                                    [ 17%]
tests/beacon/test_beacon.py::test_cl_beacon_get_validator PASSED                                                                                                                     [ 19%]
tests/beacon/test_beacon.py::test_cl_beacon_get_validator_balances PASSED                                                                                                            [ 21%]
tests/beacon/test_beacon.py::test_cl_beacon_get_epoch_committees PASSED                                                                                                              [ 24%]
tests/beacon/test_beacon.py::test_cl_beacon_get_epoch_sync_committees PASSED                                                                                                         [ 26%]
tests/beacon/test_beacon.py::test_cl_beacon_get_epoch_randao PASSED                                                                                                                  [ 29%]
tests/beacon/test_beacon.py::test_cl_beacon_get_block_headers PASSED                                                                                                                 [ 31%]
tests/beacon/test_beacon.py::test_cl_beacon_get_block_header PASSED                                                                                                                  [ 34%]
tests/beacon/test_beacon.py::test_cl_beacon_get_block PASSED                                                                                                                         [ 36%]
tests/beacon/test_beacon.py::test_cl_beacon_get_block_root PASSED                                                                                                                    [ 39%]
tests/beacon/test_beacon.py::test_cl_beacon_get_blinded_blocks PASSED                                                                                                                [ 41%]
tests/beacon/test_beacon.py::test_cl_beacon_get_rewards PASSED                                                                                                                       [ 43%]
tests/beacon/test_beacon.py::test_cl_beacon_get_block_attestations PASSED                                                                                                            [ 46%]
tests/beacon/test_beacon.py::test_cl_beacon_get_attestations PASSED                                                                                                                  [ 48%]
tests/beacon/test_beacon.py::test_cl_beacon_get_attester_slashings PASSED                                                                                                            [ 51%]
tests/beacon/test_beacon.py::test_cl_beacon_get_proposer_slashings PASSED                                                                                                            [ 53%]
tests/beacon/test_beacon.py::test_cl_beacon_get_voluntary_exits PASSED                                                                                                               [ 56%]
tests/beacon/test_beacon.py::test_cl_beacon_get_bls_to_execution_changes PASSED                                                                                                      [ 58%]
tests/beacon/test_beacon.py::test_cl_config_get_fork_schedule PASSED                                                                                                                 [ 60%]
tests/beacon/test_beacon.py::test_cl_config_get_spec PASSED                                                                                                                          [ 63%]
tests/beacon/test_beacon.py::test_cl_config_get_deposit_contract PASSED                                                                                                              [ 65%]
tests/beacon/test_beacon.py::test_cl_debug_get_beacon_state PASSED                                                                                                                   [ 68%]
tests/beacon/test_beacon.py::test_cl_debug_get_beacon_heads PASSED                                                                                                                   [ 70%]
tests/beacon/test_beacon.py::test_cl_node_get_node_identity PASSED                                                                                                                   [ 73%]
tests/beacon/test_beacon.py::test_cl_node_get_peers PASSED                                                                                                                           [ 75%]
tests/beacon/test_beacon.py::test_cl_node_get_peer PASSED                                                                                                                            [ 78%]
tests/beacon/test_beacon.py::test_cl_node_get_peer_count PASSED                                                                                                                      [ 80%]
tests/beacon/test_beacon.py::test_cl_node_get_health PASSED                                                                                                                          [ 82%]
tests/beacon/test_beacon.py::test_cl_node_get_version PASSED                                                                                                                         [ 85%]
tests/beacon/test_beacon.py::test_cl_node_get_syncing PASSED                                                                                                                         [ 87%]
tests/beacon/test_beacon.py::test_cl_node_get_blob_sidecars PASSED                                                                                                                   [ 90%]
tests/beacon/test_beacon.py::test_cl_validator_get_attester_duties PASSED                                                                                                            [ 92%]
tests/beacon/test_beacon.py::test_cl_validator_get_block_proposer_duties PASSED                                                                                                      [ 95%]
tests/beacon/test_beacon.py::test_cl_validator_get_sync_committee_duties PASSED                                                                                                      [ 97%]
tests/beacon/test_beacon.py::test_cl_validator_get_attestations_rewards PASSED                                                                                                       [100%]

========================================================================================== PASSES ==========================================================================================
=================================================================================== slowest 10 durations ===================================================================================
17.40s call     tests/beacon/test_beacon.py::test_cl_beacon_get_validators
16.81s call     tests/beacon/test_beacon.py::test_cl_validator_get_sync_committee_duties
16.79s call     tests/beacon/test_beacon.py::test_cl_validator_get_attester_duties
16.73s call     tests/beacon/test_beacon.py::test_cl_validator_get_attestations_rewards
16.70s call     tests/beacon/test_beacon.py::test_cl_beacon_get_validator
14.65s call     tests/beacon/test_beacon.py::test_cl_debug_get_beacon_state
2.08s call     tests/beacon/test_beacon.py::test_cl_beacon_get_validator_balances
0.45s call     tests/beacon/test_beacon.py::test_cl_beacon_get_epoch_committees
0.17s call     tests/beacon/test_beacon.py::test_cl_beacon_get_attestations
0.10s call     tests/beacon/test_beacon.py::test_cl_validator_get_block_proposer_duties
============================================================================== 41 passed in 102.30s (0:01:42) ==============================================================================
```

although I seem to need to apply another patch to get the `test_async_beacon_user_request_timeout` test to pass -> https://github.com/lrusak/web3.py/commit/30f271b81440c9cbe4643e267b48b16a97044d3e

That could possibly just be an issue with python 3.13 though.
fselmo: Thanks @lrusak and apologies for the holdup on this one. This looks good to me. I squashed the commits, added a newsfragment, and added some basic tests for the json post request methods in the session manager. Will merge once the CI passes 👍🏼 

Patch:
diff --git a/newsfragments/3504.feature.rst b/newsfragments/3504.feature.rst
new file mode 100644
index 00000000..0a6f7f10
--- /dev/null
+++ b/newsfragments/3504.feature.rst
@@ -0,0 +1,1 @@
+New ``Beacon`` and ``AsyncBeacon`` endpoints: ``get_peer_count``, ``get_attester_duties``, ``get_block_proposer_duties``, ``get_sync_committee_duties``, and ``get_attestation_rewards``.
diff --git a/web3/_utils/http_session_manager.py b/web3/_utils/http_session_manager.py
index 353b4051..f3dad3b2 100644
--- a/web3/_utils/http_session_manager.py
+++ b/web3/_utils/http_session_manager.py
@@ -120,11 +120,19 @@ class HTTPSessionManager:
     def get_response_from_post_request(
         self, endpoint_uri: URI, *args: Any, **kwargs: Any
     ) -> requests.Response:
+        kwargs.setdefault("timeout", DEFAULT_HTTP_TIMEOUT)
         session = self.cache_and_return_session(
             endpoint_uri, request_timeout=kwargs["timeout"]
         )
         return session.post(endpoint_uri, *args, **kwargs)
 
+    def json_make_post_request(
+        self, endpoint_uri: URI, *args: Any, **kwargs: Any
+    ) -> Dict[str, Any]:
+        response = self.get_response_from_post_request(endpoint_uri, *args, **kwargs)
+        response.raise_for_status()
+        return response.json()
+
     def make_post_request(
         self, endpoint_uri: URI, data: Union[bytes, Dict[str, Any]], **kwargs: Any
     ) -> bytes:
@@ -143,8 +151,9 @@ class HTTPSessionManager:
             else:
                 return response.content
 
+    @staticmethod
     def _handle_streaming_response(
-        self, response: requests.Response, start: float, timeout: float
+        response: requests.Response, start: float, timeout: float
     ) -> bytes:
         response_body = b""
         for data in response.iter_content():
@@ -289,6 +298,15 @@ class HTTPSessionManager:
         response = await session.post(endpoint_uri, *args, **kwargs)
         return response
 
+    async def async_json_make_post_request(
+        self, endpoint_uri: URI, *args: Any, **kwargs: Any
+    ) -> Dict[str, Any]:
+        response = await self.async_get_response_from_post_request(
+            endpoint_uri, *args, **kwargs
+        )
+        response.raise_for_status()
+        return await response.json()
+
     async def async_make_post_request(
         self, endpoint_uri: URI, data: Union[bytes, Dict[str, Any]], **kwargs: Any
     ) -> bytes:
diff --git a/web3/beacon/api_endpoints.py b/web3/beacon/api_endpoints.py
index 4cd1a526..53d252e1 100644
--- a/web3/beacon/api_endpoints.py
+++ b/web3/beacon/api_endpoints.py
@@ -59,6 +59,16 @@ GET_BEACON_HEADS = "/eth/v1/debug/beacon/heads"
 GET_NODE_IDENTITY = "/eth/v1/node/identity"
 GET_PEERS = "/eth/v1/node/peers"
 GET_PEER = "/eth/v1/node/peers/{0}"
+GET_PEER_COUNT = "/eth/v1/node/peer_count"
 GET_HEALTH = "/eth/v1/node/health"
 GET_VERSION = "/eth/v1/node/version"
 GET_SYNCING = "/eth/v1/node/syncing"
+
+# [ VALIDATOR endpoints ]
+
+GET_ATTESTER_DUTIES = "/eth/v1/validator/duties/attester/{0}"
+GET_BLOCK_PROPOSERS_DUTIES = "/eth/v1/validator/duties/proposer/{0}"
+GET_SYNC_COMMITTEE_DUTIES = "/eth/v1/validator/duties/sync/{0}"
+
+# [ REWARDS endpoints ]
+GET_ATTESTATIONS_REWARDS = "/eth/v1/beacon/rewards/attestations/{0}"
diff --git a/web3/beacon/async_beacon.py b/web3/beacon/async_beacon.py
index 6d161504..928e386d 100644
--- a/web3/beacon/async_beacon.py
+++ b/web3/beacon/async_beacon.py
@@ -3,6 +3,7 @@ from typing import (
     Dict,
     List,
     Optional,
+    Union,
 )
 
 from aiohttp import (
@@ -18,6 +19,8 @@ from web3._utils.http_session_manager import (
 )
 from web3.beacon.api_endpoints import (
     GET_ATTESTATIONS,
+    GET_ATTESTATIONS_REWARDS,
+    GET_ATTESTER_DUTIES,
     GET_ATTESTER_SLASHINGS,
     GET_BEACON_HEADS,
     GET_BEACON_STATE,
@@ -27,6 +30,7 @@ from web3.beacon.api_endpoints import (
     GET_BLOCK_ATTESTATIONS,
     GET_BLOCK_HEADER,
     GET_BLOCK_HEADERS,
+    GET_BLOCK_PROPOSERS_DUTIES,
     GET_BLOCK_ROOT,
     GET_BLS_TO_EXECUTION_CHANGES,
     GET_DEPOSIT_CONTRACT,
@@ -45,10 +49,12 @@ from web3.beacon.api_endpoints import (
     GET_LIGHT_CLIENT_UPDATES,
     GET_NODE_IDENTITY,
     GET_PEER,
+    GET_PEER_COUNT,
     GET_PEERS,
     GET_PROPOSER_SLASHINGS,
     GET_REWARDS,
     GET_SPEC,
+    GET_SYNC_COMMITTEE_DUTIES,
     GET_SYNCING,
     GET_VALIDATOR,
     GET_VALIDATOR_BALANCES,
@@ -78,6 +84,14 @@ class AsyncBeacon:
             uri, params=params, timeout=ClientTimeout(self.request_timeout)
         )
 
+    async def _async_make_post_request(
+        self, endpoint_uri: str, body: Union[List[str], Dict[str, Any]]
+    ) -> Dict[str, Any]:
+        uri = URI(self.base_url + endpoint_uri)
+        return await self._request_session_manager.async_json_make_post_request(
+            uri, json=body, timeout=self.request_timeout
+        )
+
     # [ BEACON endpoints ]
 
     # states
@@ -216,6 +230,9 @@ class AsyncBeacon:
     async def get_peer(self, peer_id: str) -> Dict[str, Any]:
         return await self._async_make_get_request(GET_PEER.format(peer_id))
 
+    async def get_peer_count(self) -> Dict[str, Any]:
+        return await self._async_make_get_request(GET_PEER_COUNT)
+
     async def get_health(self) -> int:
         url = URI(self.base_url + GET_HEALTH)
         response = (
@@ -239,3 +256,33 @@ class AsyncBeacon:
             GET_BLOB_SIDECARS.format(block_id),
             params=indices_param,
         )
+
+    # [ VALIDATOR endpoints ]
+
+    async def get_attester_duties(
+        self, epoch: str, validator_indices: List[str]
+    ) -> Dict[str, Any]:
+        return await self._async_make_post_request(
+            GET_ATTESTER_DUTIES.format(epoch), validator_indices
+        )
+
+    async def get_block_proposer_duties(self, epoch: str) -> Dict[str, Any]:
+        return await self._async_make_get_request(
+            GET_BLOCK_PROPOSERS_DUTIES.format(epoch)
+        )
+
+    async def get_sync_committee_duties(
+        self, epoch: str, validator_indices: List[str]
+    ) -> Dict[str, Any]:
+        return await self._async_make_post_request(
+            GET_SYNC_COMMITTEE_DUTIES.format(epoch), validator_indices
+        )
+
+    # [ REWARDS endpoints ]
+
+    async def get_attestations_rewards(
+        self, epoch: str, validator_indices: List[str]
+    ) -> Dict[str, Any]:
+        return await self._async_make_post_request(
+            GET_ATTESTATIONS_REWARDS.format(epoch), validator_indices
+        )
diff --git a/web3/beacon/beacon.py b/web3/beacon/beacon.py
index 4553a8b8..bc1dd098 100644
--- a/web3/beacon/beacon.py
+++ b/web3/beacon/beacon.py
@@ -3,6 +3,7 @@ from typing import (
     Dict,
     List,
     Optional,
+    Union,
 )
 
 from eth_typing import (
@@ -15,6 +16,8 @@ from web3._utils.http_session_manager import (
 )
 from web3.beacon.api_endpoints import (
     GET_ATTESTATIONS,
+    GET_ATTESTATIONS_REWARDS,
+    GET_ATTESTER_DUTIES,
     GET_ATTESTER_SLASHINGS,
     GET_BEACON_HEADS,
     GET_BEACON_STATE,
@@ -24,6 +27,7 @@ from web3.beacon.api_endpoints import (
     GET_BLOCK_ATTESTATIONS,
     GET_BLOCK_HEADER,
     GET_BLOCK_HEADERS,
+    GET_BLOCK_PROPOSERS_DUTIES,
     GET_BLOCK_ROOT,
     GET_BLS_TO_EXECUTION_CHANGES,
     GET_DEPOSIT_CONTRACT,
@@ -42,10 +46,12 @@ from web3.beacon.api_endpoints import (
     GET_LIGHT_CLIENT_UPDATES,
     GET_NODE_IDENTITY,
     GET_PEER,
+    GET_PEER_COUNT,
     GET_PEERS,
     GET_PROPOSER_SLASHINGS,
     GET_REWARDS,
     GET_SPEC,
+    GET_SYNC_COMMITTEE_DUTIES,
     GET_SYNCING,
     GET_VALIDATOR,
     GET_VALIDATOR_BALANCES,
@@ -73,6 +79,14 @@ class Beacon:
             uri, params=params, timeout=self.request_timeout
         )
 
+    def _make_post_request(
+        self, endpoint_url: str, body: Union[List[str], Dict[str, Any]]
+    ) -> Dict[str, Any]:
+        uri = URI(self.base_url + endpoint_url)
+        return self._request_session_manager.json_make_post_request(
+            uri, json=body, timeout=self.request_timeout
+        )
+
     # [ BEACON endpoints ]
 
     # states
@@ -201,6 +215,9 @@ class Beacon:
     def get_peer(self, peer_id: str) -> Dict[str, Any]:
         return self._make_get_request(GET_PEER.format(peer_id))
 
+    def get_peer_count(self) -> Dict[str, Any]:
+        return self._make_get_request(GET_PEER_COUNT)
+
     def get_health(self) -> int:
         url = URI(self.base_url + GET_HEALTH)
         response = self._request_session_manager.get_response_from_get_request(url)
@@ -222,3 +239,31 @@ class Beacon:
             GET_BLOB_SIDECARS.format(block_id),
             params=indices_param,
         )
+
+    # [ VALIDATOR endpoints ]
+
+    def get_attester_duties(
+        self, epoch: str, validator_indices: List[str]
+    ) -> Dict[str, Any]:
+        return self._make_post_request(
+            GET_ATTESTER_DUTIES.format(epoch), validator_indices
+        )
+
+    def get_block_proposer_duties(self, epoch: str) -> Dict[str, Any]:
+        return self._make_get_request(GET_BLOCK_PROPOSERS_DUTIES.format(epoch))
+
+    def get_sync_committee_duties(
+        self, epoch: str, validator_indices: List[str]
+    ) -> Dict[str, Any]:
+        return self._make_post_request(
+            GET_SYNC_COMMITTEE_DUTIES.format(epoch), validator_indices
+        )
+
+    # [ REWARDS endpoints ]
+
+    def get_attestations_rewards(
+        self, epoch: str, validator_indices: List[str]
+    ) -> Dict[str, Any]:
+        return self._make_post_request(
+            GET_ATTESTATIONS_REWARDS.format(epoch), validator_indices
+        )


Test patch:
diff --git a/tests/beacon/test_async_beacon.py b/tests/beacon/test_async_beacon.py
index a129c348..aa257b9c 100644
--- a/tests/beacon/test_async_beacon.py
+++ b/tests/beacon/test_async_beacon.py
@@ -256,6 +256,12 @@ async def test_async_cl_node_get_peer(async_beacon):
     _assert_valid_response(response)
 
 
+@pytest.mark.asyncio
+async def test_async_cl_node_get_peer_count(async_beacon):
+    response = await async_beacon.get_peer_count()
+    _assert_valid_response(response)
+
+
 @pytest.mark.asyncio
 async def test_async_cl_node_get_health(async_beacon):
     response = await async_beacon.get_health()
@@ -285,3 +291,82 @@ async def test_async_cl_node_get_blob_sidecars(async_beacon):
     # test with indices
     with_indices = await async_beacon.get_blob_sidecars("head", [0, 1])
     _assert_valid_response(with_indices)
+
+
+# Validator endpoint tests:
+
+
+@pytest.mark.asyncio
+async def test_async_cl_validator_get_attester_duties(async_beacon):
+    finality_checkpoint_response = await async_beacon.get_finality_checkpoint()
+    _assert_valid_response(finality_checkpoint_response)
+
+    finality_checkpoint = finality_checkpoint_response["data"]
+    epoch = finality_checkpoint["finalized"]["epoch"]
+
+    validators_response = await async_beacon.get_validators()
+    _assert_valid_response(validators_response)
+
+    validators = validators_response["data"]
+    random_validator = validators[randint(0, len(validators))]
+    random_validator_index = random_validator["index"]
+
+    response = await async_beacon.get_attester_duties(epoch, [random_validator_index])
+    _assert_valid_response(response)
+
+
+@pytest.mark.asyncio
+async def test_async_cl_validator_get_block_proposer_duties(async_beacon):
+    finality_checkpoint_response = await async_beacon.get_finality_checkpoint()
+    _assert_valid_response(finality_checkpoint_response)
+
+    finality_checkpoint = finality_checkpoint_response["data"]
+    epoch = finality_checkpoint["finalized"]["epoch"]
+
+    response = await async_beacon.get_block_proposer_duties(epoch)
+    _assert_valid_response(response)
+
+
+@pytest.mark.asyncio
+async def test_async_cl_validator_get_sync_committee_duties(async_beacon):
+    finality_checkpoint_response = await async_beacon.get_finality_checkpoint()
+    _assert_valid_response(finality_checkpoint_response)
+
+    finality_checkpoint = finality_checkpoint_response["data"]
+    epoch = finality_checkpoint["finalized"]["epoch"]
+
+    validators_response = await async_beacon.get_validators()
+    _assert_valid_response(validators_response)
+
+    validators = validators_response["data"]
+    random_validator = validators[randint(0, len(validators))]
+    random_validator_index = random_validator["index"]
+
+    response = await async_beacon.get_sync_committee_duties(
+        epoch, [random_validator_index]
+    )
+    _assert_valid_response(response)
+
+
+# Rewards endpoint tests:
+
+
+@pytest.mark.asyncio
+async def test_async_cl_validator_get_attestations_rewards(async_beacon):
+    finality_checkpoint_response = await async_beacon.get_finality_checkpoint()
+    _assert_valid_response(finality_checkpoint_response)
+
+    finality_checkpoint = finality_checkpoint_response["data"]
+    epoch = finality_checkpoint["finalized"]["epoch"]
+
+    validators_response = await async_beacon.get_validators()
+    _assert_valid_response(validators_response)
+
+    validators = validators_response["data"]
+    random_validator = validators[randint(0, len(validators))]
+    random_validator_index = random_validator["index"]
+
+    response = await async_beacon.get_attestations_rewards(
+        epoch, [random_validator_index]
+    )
+    _assert_valid_response(response)
diff --git a/tests/beacon/test_beacon.py b/tests/beacon/test_beacon.py
index 3fc72a5c..c3617fb6 100644
--- a/tests/beacon/test_beacon.py
+++ b/tests/beacon/test_beacon.py
@@ -214,6 +214,11 @@ def test_cl_node_get_peer(beacon):
     _assert_valid_response(response)
 
 
+def test_cl_node_get_peer_count(beacon):
+    response = beacon.get_peer_count()
+    _assert_valid_response(response)
+
+
 def test_cl_node_get_health(beacon):
     response = beacon.get_health()
     assert isinstance(response, int)
@@ -239,3 +244,74 @@ def test_cl_node_get_blob_sidecars(beacon):
     # test with indices
     with_indices = beacon.get_blob_sidecars("head", [0, 1])
     _assert_valid_response(with_indices)
+
+
+# Validator endpoint tests:
+
+
+def test_cl_validator_get_attester_duties(beacon):
+    finality_checkpoint_response = beacon.get_finality_checkpoint()
+    _assert_valid_response(finality_checkpoint_response)
+
+    finality_checkpoint = finality_checkpoint_response["data"]
+    epoch = finality_checkpoint["finalized"]["epoch"]
+
+    validators_response = beacon.get_validators()
+    _assert_valid_response(validators_response)
+
+    validators = validators_response["data"]
+    random_validator = validators[randint(0, len(validators))]
+    random_validator_index = random_validator["index"]
+
+    response = beacon.get_attester_duties(epoch, [random_validator_index])
+    _assert_valid_response(response)
+
+
+def test_cl_validator_get_block_proposer_duties(beacon):
+    finality_checkpoint_response = beacon.get_finality_checkpoint()
+    _assert_valid_response(finality_checkpoint_response)
+
+    finality_checkpoint = finality_checkpoint_response["data"]
+    epoch = finality_checkpoint["finalized"]["epoch"]
+
+    response = beacon.get_block_proposer_duties(epoch)
+    _assert_valid_response(response)
+
+
+def test_cl_validator_get_sync_committee_duties(beacon):
+    finality_checkpoint_response = beacon.get_finality_checkpoint()
+    _assert_valid_response(finality_checkpoint_response)
+
+    finality_checkpoint = finality_checkpoint_response["data"]
+    epoch = finality_checkpoint["finalized"]["epoch"]
+
+    validators_response = beacon.get_validators()
+    _assert_valid_response(validators_response)
+
+    validators = validators_response["data"]
+    random_validator = validators[randint(0, len(validators))]
+    random_validator_index = random_validator["index"]
+
+    response = beacon.get_sync_committee_duties(epoch, [random_validator_index])
+    _assert_valid_response(response)
+
+
+# Rewards endpoint tests:
+
+
+def test_cl_validator_get_attestations_rewards(beacon):
+    finality_checkpoint_response = beacon.get_finality_checkpoint()
+    _assert_valid_response(finality_checkpoint_response)
+
+    finality_checkpoint = finality_checkpoint_response["data"]
+    epoch = finality_checkpoint["finalized"]["epoch"]
+
+    validators_response = beacon.get_validators()
+    _assert_valid_response(validators_response)
+
+    validators = validators_response["data"]
+    random_validator = validators[randint(0, len(validators))]
+    random_validator_index = random_validator["index"]
+
+    response = beacon.get_attestations_rewards(epoch, [random_validator_index])
+    _assert_valid_response(response)
diff --git a/tests/core/utilities/test_http_session_manager.py b/tests/core/utilities/test_http_session_manager.py
index 4ec143cc..10c77f45 100644
--- a/tests/core/utilities/test_http_session_manager.py
+++ b/tests/core/utilities/test_http_session_manager.py
@@ -112,6 +112,28 @@ def test_session_manager_json_make_get_request(mocker, http_session_manager):
     assert adapter._pool_maxsize == DEFAULT_POOLSIZE
 
 
+def test_session_manager_json_make_post_request(mocker, http_session_manager):
+    mocker.patch("requests.Session.post", return_value=MockedResponse())
+
+    # Submit a first request to create a session with default parameters
+    assert len(http_session_manager.session_cache) == 0
+    response = http_session_manager.json_make_post_request(
+        TEST_URI, json={"data": "request"}
+    )
+    assert response == json.dumps({"data": "content"})
+    assert len(http_session_manager.session_cache) == 1
+    cache_key = generate_cache_key(f"{threading.get_ident()}:{TEST_URI}")
+    session = http_session_manager.session_cache.get_cache_entry(cache_key)
+    session.post.assert_called_once_with(TEST_URI, json={"data": "request"}, timeout=30)
+
+    # Ensure the adapter was created with default values
+    check_adapters_mounted(session)
+    adapter = session.get_adapter(TEST_URI)
+    assert isinstance(adapter, HTTPAdapter)
+    assert adapter._pool_connections == DEFAULT_POOLSIZE
+    assert adapter._pool_maxsize == DEFAULT_POOLSIZE
+
+
 def test_session_manager_make_post_request_no_args(mocker, http_session_manager):
     mocker.patch("requests.Session.post", return_value=MockedResponse())
 
@@ -332,6 +354,32 @@ async def test_session_manager_async_json_make_get_request(
     await session.close()
 
 
+@pytest.mark.asyncio
+async def test_session_manager_async_json_make_post_request(
+    mocker, http_session_manager
+):
+    mocker.patch("aiohttp.ClientSession.post", return_value=AsyncMockedResponse())
+
+    # Submit a first request to create a session with default parameters
+    assert len(http_session_manager.session_cache) == 0
+    response = await http_session_manager.async_json_make_post_request(
+        TEST_URI, json={"data": "request"}
+    )
+    assert response == json.dumps({"data": "content"})
+    assert len(http_session_manager.session_cache) == 1
+    cache_key = generate_cache_key(f"{threading.get_ident()}:{TEST_URI}")
+    session = http_session_manager.session_cache.get_cache_entry(cache_key)
+    assert isinstance(session, ClientSession)
+    session.post.assert_called_once_with(
+        TEST_URI,
+        json={"data": "request"},
+        timeout=ClientTimeout(
+            total=30, connect=None, sock_read=None, sock_connect=None
+        ),
+    )
+    await session.close()
+
+
 @pytest.mark.asyncio
 async def test_session_manager_async_make_post_request(mocker, http_session_manager):
     mocker.patch("aiohttp.ClientSession.post", return_value=AsyncMockedResponse())