diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index 46fa15da86a..db3692170cf 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -319,6 +319,8 @@ jobs: bins: cargo-audit,cargo-deny - name: Check formatting with cargo fmt run: make cargo-fmt + - name: Markdown-linter + run: make mdlint - name: Lint code for quality and style with Clippy run: make lint-full - name: Certify Cargo.lock freshness @@ -333,8 +335,6 @@ jobs: run: make deny-CI - name: Run cargo vendor to make sure dependencies can be vendored for packaging, reproducibility and archival purpose run: CARGO_HOME=$(readlink -f $HOME) make vendor - - name: Markdown-linter - run: make mdlint - name: Spell-check uses: rojopolis/spellcheck-github-actions@v0 check-msrv: diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 095c52fb292..b9f8cdee93a 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -3068,6 +3068,34 @@ pub fn serve( }) }); + // GET lighthouse/analysis/total_supply/{state_root} + let get_lighthouse_total_supply = warp::path("lighthouse") + .and(warp::path("analysis")) + .and(warp::path("total_supply")) + .and(warp::path::param::()) + .and(warp::path::end()) + .and(task_spawner_filter.clone()) + .and(chain_filter.clone()) + .then( + |state_id: StateId, + task_spawner: TaskSpawner, + chain: Arc>| { + task_spawner.blocking_json_task(Priority::P1, move || { + state_id.map_state_and_execution_optimistic_and_finalized( + &chain, + |state, execution_optimistic, finalized| { + let response = state.balances().iter().sum::(); + Ok(api_types::GenericResponse::from(response) + .add_execution_optimistic_finalized( + execution_optimistic, + finalized, + )) + }, + ) + }) + }, + ); + // GET lighthouse/analysis/attestation_performance/{index} let get_lighthouse_attestation_performance = warp::path("lighthouse") .and(warp::path("analysis")) @@ -3356,6 +3384,7 @@ pub fn serve( .uor(get_lighthouse_database_info) .uor(get_lighthouse_custody_info) .uor(get_lighthouse_block_rewards) + .uor(get_lighthouse_total_supply) .uor(get_lighthouse_attestation_performance) .uor(get_beacon_light_client_optimistic_update) .uor(get_beacon_light_client_finality_update) diff --git a/beacon_node/http_api/tests/tests.rs b/beacon_node/http_api/tests/tests.rs index bef9fe6acda..d12fc50a9ec 100644 --- a/beacon_node/http_api/tests/tests.rs +++ b/beacon_node/http_api/tests/tests.rs @@ -6233,6 +6233,30 @@ impl ApiTester { self } + pub async fn test_get_lighthouse_total_supply(self) -> Self { + for state_id in self.interesting_state_ids() { + let state_opt = state_id + .state(&self.chain) + .ok() + .map(|(state, _execution_optimistic, _finalized)| state); + + let total_supply = state_opt + .as_ref() + .map(|state| state.balances().iter().sum::()); + + let api_total_supply = self + .client + .get_total_supply(state_id.0) + .await + .unwrap() + .map(|res| res.data); + + assert_eq!(total_supply, api_total_supply, "{:?}", state_id); + } + + self + } + pub async fn test_post_lighthouse_database_reconstruct(self) -> Self { let response = self .client @@ -8065,7 +8089,7 @@ async fn post_validator_liveness_epoch() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn lighthouse_endpoints() { +async fn lighthouse_get_endpoints() { ApiTester::new() .await .test_get_lighthouse_health() @@ -8077,6 +8101,14 @@ async fn lighthouse_endpoints() { .test_get_lighthouse_validator_inclusion() .await .test_get_lighthouse_validator_inclusion_global() + .await + .test_get_lighthouse_total_supply() + .await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn lighthouse_post_endpoints() { + ApiTester::new() .await .test_post_lighthouse_database_reconstruct() .await diff --git a/book/src/api_lighthouse.md b/book/src/api_lighthouse.md index 0442bf4ec09..f9bfbd36b78 100644 --- a/book/src/api_lighthouse.md +++ b/book/src/api_lighthouse.md @@ -686,6 +686,22 @@ Caveats: This is because the state *prior* to the `start_epoch` needs to be loaded from the database, and loading a state on a boundary is most efficient. +## `lighthouse/analysis/total_supply/{state_root}` + +Returns the sum of all validator balances for a given state root. + +```bash +curl -X GET "http://localhost:5052/lighthouse/analysis/total_supply/0x7e76880eb67bbdc86250aa578958e9d0675e64e714337855204fb5abaaf82c2b" -H "accept: application/json" | jq +``` + +```json +{ + "execution_optimistic": false, + "finalized": true, + "data": 674144000000000 +} +``` + ## `/lighthouse/logs` This is a Server Side Event subscription endpoint. This allows a user to read diff --git a/common/eth2/src/lib.rs b/common/eth2/src/lib.rs index 10382b028a8..729d45d2ce7 100644 --- a/common/eth2/src/lib.rs +++ b/common/eth2/src/lib.rs @@ -2787,6 +2787,23 @@ impl BeaconNodeHttpClient { Ok(()) } + /// `GET lighthouse/analysis/total_supply/{state_root}` + pub async fn get_total_supply( + &self, + state_id: StateId, + ) -> Result>, Error> { + let mut path = self.server.expose_full().clone(); + + path.path_segments_mut() + .map_err(|()| Error::InvalidUrl(self.server.clone()))? + .push("lighthouse") + .push("analysis") + .push("total_supply") + .push(&state_id.to_string()); + + self.get_opt(path).await + } + /// `GET events?topics` #[cfg(feature = "events")] pub async fn get_events(