Add beacon.watch
(#3362)
> This is currently a WIP and all features are subject to alteration or removal at any time. ## Overview The successor to #2873. Contains the backbone of `beacon.watch` including syncing code, the initial API, and several core database tables. See `watch/README.md` for more information, requirements and usage.
This commit is contained in:
parent
1e029ce538
commit
8630ddfec4
699
Cargo.lock
generated
699
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -87,6 +87,8 @@ members = [
|
||||
|
||||
"validator_client",
|
||||
"validator_client/slashing_protection",
|
||||
|
||||
"watch",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
|
@ -38,15 +38,15 @@ system_health = { path = "../../common/system_health" }
|
||||
directory = { path = "../../common/directory" }
|
||||
eth2_serde_utils = "0.1.1"
|
||||
operation_pool = { path = "../operation_pool" }
|
||||
sensitive_url = { path = "../../common/sensitive_url" }
|
||||
unused_port = {path = "../../common/unused_port"}
|
||||
logging = { path = "../../common/logging" }
|
||||
store = { path = "../store" }
|
||||
|
||||
[dev-dependencies]
|
||||
store = { path = "../store" }
|
||||
environment = { path = "../../lighthouse/environment" }
|
||||
sensitive_url = { path = "../../common/sensitive_url" }
|
||||
logging = { path = "../../common/logging" }
|
||||
serde_json = "1.0.58"
|
||||
proto_array = { path = "../../consensus/proto_array" }
|
||||
unused_port = {path = "../../common/unused_port"}
|
||||
genesis = { path = "../genesis" }
|
||||
|
||||
[[test]]
|
||||
|
@ -18,6 +18,7 @@ mod standard_block_rewards;
|
||||
mod state_id;
|
||||
mod sync_committee_rewards;
|
||||
mod sync_committees;
|
||||
pub mod test_utils;
|
||||
mod ui;
|
||||
mod validator_inclusion;
|
||||
mod version;
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::{Config, Context};
|
||||
use beacon_chain::{
|
||||
test_utils::{
|
||||
BeaconChainHarness, BoxedMutator, Builder as HarnessBuilder, EphemeralHarnessType,
|
||||
@ -6,7 +7,6 @@ use beacon_chain::{
|
||||
};
|
||||
use directory::DEFAULT_ROOT_DIR;
|
||||
use eth2::{BeaconNodeHttpClient, Timeouts};
|
||||
use http_api::{Config, Context};
|
||||
use lighthouse_network::{
|
||||
discv5::enr::{CombinedKey, EnrBuilder},
|
||||
libp2p::{
|
||||
@ -182,7 +182,7 @@ pub async fn create_api_server_on_port<T: BeaconChainTypes>(
|
||||
let eth1_service =
|
||||
eth1::Service::new(eth1::Config::default(), log.clone(), chain.spec.clone()).unwrap();
|
||||
|
||||
let context = Arc::new(Context {
|
||||
let ctx = Arc::new(Context {
|
||||
config: Config {
|
||||
enabled: true,
|
||||
listen_addr: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
|
||||
@ -193,19 +193,19 @@ pub async fn create_api_server_on_port<T: BeaconChainTypes>(
|
||||
data_dir: std::path::PathBuf::from(DEFAULT_ROOT_DIR),
|
||||
spec_fork_name: None,
|
||||
},
|
||||
chain: Some(chain.clone()),
|
||||
chain: Some(chain),
|
||||
network_senders: Some(network_senders),
|
||||
network_globals: Some(network_globals),
|
||||
eth1_service: Some(eth1_service),
|
||||
log,
|
||||
});
|
||||
let ctx = context.clone();
|
||||
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let server_shutdown = async {
|
||||
// It's not really interesting why this triggered, just that it happened.
|
||||
let _ = shutdown_rx.await;
|
||||
};
|
||||
let (listening_socket, server) = http_api::serve(ctx, server_shutdown).unwrap();
|
||||
let (listening_socket, server) = crate::serve(ctx, server_shutdown).unwrap();
|
||||
|
||||
ApiServer {
|
||||
server,
|
@ -1,11 +1,11 @@
|
||||
//! Tests for API behaviour across fork boundaries.
|
||||
use crate::common::*;
|
||||
use beacon_chain::{
|
||||
test_utils::{RelativeSyncCommittee, DEFAULT_ETH1_BLOCK_HASH, HARNESS_GENESIS_TIME},
|
||||
StateSkipConfig,
|
||||
};
|
||||
use eth2::types::{IndexedErrorMessage, StateId, SyncSubcommittee};
|
||||
use genesis::{bls_withdrawal_credentials, interop_genesis_state_with_withdrawal_credentials};
|
||||
use http_api::test_utils::*;
|
||||
use std::collections::HashSet;
|
||||
use types::{
|
||||
test_utils::{generate_deterministic_keypair, generate_deterministic_keypairs},
|
||||
|
@ -1,11 +1,11 @@
|
||||
//! Generic tests that make use of the (newer) `InteractiveApiTester`
|
||||
use crate::common::*;
|
||||
use beacon_chain::{
|
||||
chain_config::ReOrgThreshold,
|
||||
test_utils::{AttestationStrategy, BlockStrategy, SyncCommitteeStrategy},
|
||||
};
|
||||
use eth2::types::DepositContractData;
|
||||
use execution_layer::{ForkchoiceState, PayloadAttributes};
|
||||
use http_api::test_utils::InteractiveTester;
|
||||
use parking_lot::Mutex;
|
||||
use slot_clock::SlotClock;
|
||||
use state_processing::{
|
||||
|
@ -1,6 +1,5 @@
|
||||
#![cfg(not(debug_assertions))] // Tests are too slow in debug.
|
||||
|
||||
pub mod common;
|
||||
pub mod fork_tests;
|
||||
pub mod interactive_tests;
|
||||
pub mod tests;
|
||||
|
@ -1,4 +1,3 @@
|
||||
use crate::common::{create_api_server, create_api_server_on_port, ApiServer};
|
||||
use beacon_chain::test_utils::RelativeSyncCommittee;
|
||||
use beacon_chain::{
|
||||
test_utils::{AttestationStrategy, BeaconChainHarness, BlockStrategy, EphemeralHarnessType},
|
||||
@ -18,7 +17,10 @@ use execution_layer::test_utils::{
|
||||
};
|
||||
use futures::stream::{Stream, StreamExt};
|
||||
use futures::FutureExt;
|
||||
use http_api::{BlockId, StateId};
|
||||
use http_api::{
|
||||
test_utils::{create_api_server, create_api_server_on_port, ApiServer},
|
||||
BlockId, StateId,
|
||||
};
|
||||
use lighthouse_network::{Enr, EnrExt, PeerId};
|
||||
use network::NetworkReceivers;
|
||||
use proto_array::ExecutionStatus;
|
||||
|
@ -22,7 +22,7 @@ use lighthouse_network::PeerId;
|
||||
pub use reqwest;
|
||||
use reqwest::{IntoUrl, RequestBuilder, Response};
|
||||
pub use reqwest::{StatusCode, Url};
|
||||
pub use sensitive_url::SensitiveUrl;
|
||||
pub use sensitive_url::{SensitiveError, SensitiveUrl};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
|
@ -13,7 +13,7 @@ use crate::{
|
||||
BeaconState, ChainSpec, DepositTreeSnapshot, Epoch, EthSpec, FinalizedExecutionBlock,
|
||||
GenericResponse, ValidatorId,
|
||||
},
|
||||
BeaconNodeHttpClient, DepositData, Error, Eth1Data, Hash256, StateId, StatusCode,
|
||||
BeaconNodeHttpClient, DepositData, Error, Eth1Data, Hash256, Slot, StateId, StatusCode,
|
||||
};
|
||||
use proto_array::core::ProtoArray;
|
||||
use reqwest::IntoUrl;
|
||||
@ -566,4 +566,73 @@ impl BeaconNodeHttpClient {
|
||||
|
||||
self.post_with_response(path, &()).await
|
||||
}
|
||||
|
||||
///
|
||||
/// Analysis endpoints.
|
||||
///
|
||||
|
||||
/// `GET` lighthouse/analysis/block_rewards?start_slot,end_slot
|
||||
pub async fn get_lighthouse_analysis_block_rewards(
|
||||
&self,
|
||||
start_slot: Slot,
|
||||
end_slot: Slot,
|
||||
) -> Result<Vec<BlockReward>, Error> {
|
||||
let mut path = self.server.full.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("analysis")
|
||||
.push("block_rewards");
|
||||
|
||||
path.query_pairs_mut()
|
||||
.append_pair("start_slot", &start_slot.to_string())
|
||||
.append_pair("end_slot", &end_slot.to_string());
|
||||
|
||||
self.get(path).await
|
||||
}
|
||||
|
||||
/// `GET` lighthouse/analysis/block_packing?start_epoch,end_epoch
|
||||
pub async fn get_lighthouse_analysis_block_packing(
|
||||
&self,
|
||||
start_epoch: Epoch,
|
||||
end_epoch: Epoch,
|
||||
) -> Result<Vec<BlockPackingEfficiency>, Error> {
|
||||
let mut path = self.server.full.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("analysis")
|
||||
.push("block_packing_efficiency");
|
||||
|
||||
path.query_pairs_mut()
|
||||
.append_pair("start_epoch", &start_epoch.to_string())
|
||||
.append_pair("end_epoch", &end_epoch.to_string());
|
||||
|
||||
self.get(path).await
|
||||
}
|
||||
|
||||
/// `GET` lighthouse/analysis/attestation_performance/{index}?start_epoch,end_epoch
|
||||
pub async fn get_lighthouse_analysis_attestation_performance(
|
||||
&self,
|
||||
start_epoch: Epoch,
|
||||
end_epoch: Epoch,
|
||||
target: String,
|
||||
) -> Result<Vec<AttestationPerformance>, Error> {
|
||||
let mut path = self.server.full.clone();
|
||||
|
||||
path.path_segments_mut()
|
||||
.map_err(|()| Error::InvalidUrl(self.server.clone()))?
|
||||
.push("lighthouse")
|
||||
.push("analysis")
|
||||
.push("attestation_performance")
|
||||
.push(&target);
|
||||
|
||||
path.query_pairs_mut()
|
||||
.append_pair("start_epoch", &start_epoch.to_string())
|
||||
.append_pair("end_epoch", &end_epoch.to_string());
|
||||
|
||||
self.get(path).await
|
||||
}
|
||||
}
|
||||
|
1
watch/.gitignore
vendored
Normal file
1
watch/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
config.yaml
|
45
watch/Cargo.toml
Normal file
45
watch/Cargo.toml
Normal file
@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "watch"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
name = "watch"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "watch"
|
||||
path = "src/main.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = "2.33.3"
|
||||
log = "0.4.14"
|
||||
env_logger = "0.9.0"
|
||||
types = { path = "../consensus/types" }
|
||||
eth2 = { path = "../common/eth2" }
|
||||
beacon_node = { path = "../beacon_node"}
|
||||
tokio = { version = "1.14.0", features = ["time"] }
|
||||
axum = "0.5.15"
|
||||
hyper = "0.14.20"
|
||||
serde = "1.0.116"
|
||||
serde_json = "1.0.58"
|
||||
reqwest = { version = "0.11.0", features = ["json","stream"] }
|
||||
url = "2.2.2"
|
||||
rand = "0.7.3"
|
||||
diesel = { version = "2.0.2", features = ["postgres", "r2d2"] }
|
||||
diesel_migrations = { version = "2.0.0", features = ["postgres"] }
|
||||
byteorder = "1.4.3"
|
||||
bls = { path = "../crypto/bls" }
|
||||
hex = "0.4.2"
|
||||
r2d2 = "0.8.9"
|
||||
serde_yaml = "0.8.24"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio-postgres = "0.7.5"
|
||||
http_api = { path = "../beacon_node/http_api" }
|
||||
beacon_chain = { path = "../beacon_node/beacon_chain" }
|
||||
network = { path = "../beacon_node/network" }
|
||||
testcontainers = "0.14.0"
|
||||
unused_port = { path = "../common/unused_port" }
|
460
watch/README.md
Normal file
460
watch/README.md
Normal file
@ -0,0 +1,460 @@
|
||||
## beacon.watch
|
||||
|
||||
>beacon.watch is pre-MVP and still under active development and subject to change.
|
||||
|
||||
beacon.watch is an Ethereum Beacon Chain monitoring platform whose goal is to provide fast access to
|
||||
data which is:
|
||||
1. Not already stored natively in the Beacon Chain
|
||||
2. Too specialized for Block Explorers
|
||||
3. Too sensitive for public Block Explorers
|
||||
|
||||
|
||||
### Requirements
|
||||
- `git`
|
||||
- `rust` : https://rustup.rs/
|
||||
- `libpg` : https://www.postgresql.org/download/
|
||||
- `diesel_cli` :
|
||||
```
|
||||
cargo install diesel_cli --no-default-features --features postgres
|
||||
```
|
||||
- `docker` : https://docs.docker.com/engine/install/
|
||||
- `docker-compose` : https://docs.docker.com/compose/install/
|
||||
|
||||
### Setup
|
||||
1. Setup the database:
|
||||
```
|
||||
cd postgres_docker_compose
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
1. Ensure the tests pass:
|
||||
```
|
||||
cargo test --release
|
||||
```
|
||||
|
||||
1. Drop the database (if it already exists) and run the required migrations:
|
||||
```
|
||||
diesel database reset --database-url postgres://postgres:postgres@localhost/dev
|
||||
```
|
||||
|
||||
1. Ensure a synced Lighthouse beacon node with historical states is available
|
||||
at `localhost:5052`.
|
||||
The smaller the value of `--slots-per-restore-point` the faster beacon.watch
|
||||
will be able to sync to the beacon node.
|
||||
|
||||
1. Run the updater daemon:
|
||||
```
|
||||
cargo run --release -- run-updater
|
||||
```
|
||||
|
||||
1. Start the HTTP API server:
|
||||
```
|
||||
cargo run --release -- serve
|
||||
```
|
||||
|
||||
1. Ensure connectivity:
|
||||
```
|
||||
curl "http://localhost:5059/v1/slots/highest"
|
||||
```
|
||||
|
||||
> Functionality on MacOS has not been tested. Windows is not supported.
|
||||
|
||||
|
||||
### Configuration
|
||||
beacon.watch can be configured through the use of a config file.
|
||||
Available options can be seen in `config.yaml.default`.
|
||||
|
||||
You can specify a config file during runtime:
|
||||
```
|
||||
cargo run -- run-updater --config path/to/config.yaml
|
||||
cargo run -- serve --config path/to/config.yaml
|
||||
```
|
||||
|
||||
You can specify only the parts of the config file which you need changed.
|
||||
Missing values will remain as their defaults.
|
||||
|
||||
For example, if you wish to run with default settings but only wish to alter `log_level`
|
||||
your config file would be:
|
||||
```yaml
|
||||
# config.yaml
|
||||
log_level = "info"
|
||||
```
|
||||
|
||||
### Available Endpoints
|
||||
As beacon.watch continues to develop, more endpoints will be added.
|
||||
|
||||
> In these examples any data containing information from blockprint has either been redacted or fabricated.
|
||||
|
||||
#### `/v1/slots/{slot}`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/slots/4635296"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62",
|
||||
"skipped": false,
|
||||
"beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62"
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/slots?start_slot={}&end_slot={}`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/slots?start_slot=4635296&end_slot=4635297"
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"slot": "4635297",
|
||||
"root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182",
|
||||
"skipped": false,
|
||||
"beacon_block": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182"
|
||||
},
|
||||
{
|
||||
"slot": "4635296",
|
||||
"root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62",
|
||||
"skipped": false,
|
||||
"beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### `/v1/slots/lowest`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/slots/lowest"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62",
|
||||
"skipped": false,
|
||||
"beacon_block": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62"
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/slots/highest`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/slots/highest"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635358",
|
||||
"root": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b",
|
||||
"skipped": false,
|
||||
"beacon_block": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b"
|
||||
}
|
||||
```
|
||||
|
||||
#### `v1/slots/{slot}/block`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/slots/4635296/block"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62",
|
||||
"parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b"
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/blocks/{block_id}`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks/4635296"
|
||||
# OR
|
||||
curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62",
|
||||
"parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b"
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/blocks?start_slot={}&end_slot={}`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks?start_slot=4635296&end_slot=4635297"
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"slot": "4635297",
|
||||
"root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182",
|
||||
"parent_root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62"
|
||||
},
|
||||
{
|
||||
"slot": "4635296",
|
||||
"root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62",
|
||||
"parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### `/v1/blocks/{block_id}/previous`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks/4635297/previous"
|
||||
# OR
|
||||
curl "http://localhost:5059/v1/blocks/0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182/previous"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62",
|
||||
"parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b"
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/blocks/{block_id}/next`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks/4635296/next"
|
||||
# OR
|
||||
curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/next"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635297",
|
||||
"root": "0x04ad2e963811207e344bebeba5b1217805bcc3a9e2ed9fcf2205d491778c6182",
|
||||
"parent_root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62"
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/blocks/lowest`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks/lowest"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"root": "0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62",
|
||||
"parent_root": "0x7c4860b420a23de9d126da71f9043b3744af98c847efd9e1440f2da8fbf7f31b"
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/blocks/highest`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks/highest"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635358",
|
||||
"root": "0xe9eff13560688f1bf15cf07b60c84963d4d04a4a885ed0eb19ceb8450011894b",
|
||||
"parent_root": "0xb66e05418bb5b1d4a965c994e1f0e5b5f0d7b780e0df12f3f6321510654fa1d2"
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/blocks/{block_id}/proposer`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks/4635296/proposer"
|
||||
# OR
|
||||
curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/proposer"
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"proposer_index": 223126,
|
||||
"graffiti": ""
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/blocks/{block_id}/rewards`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks/4635296/reward"
|
||||
# OR
|
||||
curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/reward"
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"total": 25380059,
|
||||
"attestation_reward": 24351867,
|
||||
"sync_committee_reward": 1028192
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/blocks/{block_id}/packing`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/blocks/4635296/packing"
|
||||
# OR
|
||||
curl "http://localhost:5059/v1/blocks/0xf7063a9d6c663682e59bd0b41d29ce80c3ff0b089049ff8676d6f9ee79622c62/packing"
|
||||
|
||||
```
|
||||
```json
|
||||
{
|
||||
"slot": "4635296",
|
||||
"available": 16152,
|
||||
"included": 13101,
|
||||
"prior_skip_slots": 0
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/validators/{validator}`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/validators/1"
|
||||
# OR
|
||||
curl "http://localhost:5059/v1/validators/0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"index": 1,
|
||||
"public_key": "0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c",
|
||||
"status": "active_ongoing",
|
||||
"client": null,
|
||||
"activation_epoch": 0,
|
||||
"exit_epoch": null
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/validators/{validator}/attestation/{epoch}`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/validators/1/attestation/144853"
|
||||
# OR
|
||||
curl "http://localhost:5059/v1/validators/0xa1d1ad0714035353258038e964ae9675dc0252ee22cea896825c01458e1807bfad2f9969338798548d9858a571f7425c/attestation/144853"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"index": 1,
|
||||
"epoch": "144853",
|
||||
"source": true,
|
||||
"head": true,
|
||||
"target": true
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/validators/missed/{vote}/{epoch}`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/validators/missed/head/144853"
|
||||
```
|
||||
```json
|
||||
[
|
||||
63,
|
||||
67,
|
||||
98,
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
#### `/v1/validators/missed/{vote}/{epoch}/graffiti`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/validators/missed/head/144853/graffiti"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"Mr F was here": 3,
|
||||
"Lighthouse/v3.1.0-aa022f4": 5,
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/clients/missed/{vote}/{epoch}`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/clients/missed/source/144853"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"Lighthouse": 100,
|
||||
"Lodestar": 100,
|
||||
"Nimbus": 100,
|
||||
"Prysm": 100,
|
||||
"Teku": 100,
|
||||
"Unknown": 100
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/clients/missed/{vote}/{epoch}/percentages`
|
||||
Note that this endpoint expresses the following:
|
||||
```
|
||||
What percentage of each client implementation missed this vote?
|
||||
```
|
||||
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/clients/missed/target/144853/percentages"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"Lighthouse": 0.51234567890,
|
||||
"Lodestar": 0.51234567890,
|
||||
"Nimbus": 0.51234567890,
|
||||
"Prysm": 0.09876543210,
|
||||
"Teku": 0.09876543210,
|
||||
"Unknown": 0.05647382910
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/clients/missed/{vote}/{epoch}/percentages/relative`
|
||||
Note that this endpoint expresses the following:
|
||||
```
|
||||
For the validators which did miss this vote, what percentage of them were from each client implementation?
|
||||
```
|
||||
You can check these values against the output of `/v1/clients/percentages` to see any discrepancies.
|
||||
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/clients/missed/target/144853/percentages/relative"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"Lighthouse": 11.11111111111111,
|
||||
"Lodestar": 11.11111111111111,
|
||||
"Nimbus": 11.11111111111111,
|
||||
"Prysm": 16.66666666666667,
|
||||
"Teku": 16.66666666666667,
|
||||
"Unknown": 33.33333333333333
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
#### `/v1/clients`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/clients"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"Lighthouse": 5000,
|
||||
"Lodestar": 5000,
|
||||
"Nimbus": 5000,
|
||||
"Prysm": 5000,
|
||||
"Teku": 5000,
|
||||
"Unknown": 5000
|
||||
}
|
||||
```
|
||||
|
||||
#### `/v1/clients/percentages`
|
||||
```bash
|
||||
curl "http://localhost:5059/v1/clients/percentages"
|
||||
```
|
||||
```json
|
||||
{
|
||||
"Lighthouse": 16.66666666666667,
|
||||
"Lodestar": 16.66666666666667,
|
||||
"Nimbus": 16.66666666666667,
|
||||
"Prysm": 16.66666666666667,
|
||||
"Teku": 16.66666666666667,
|
||||
"Unknown": 16.66666666666667
|
||||
}
|
||||
```
|
||||
|
||||
### Future work
|
||||
- New tables
|
||||
- `skip_slots`?
|
||||
|
||||
|
||||
- More API endpoints
|
||||
- `/v1/proposers?start_epoch={}&end_epoch={}` and similar
|
||||
- `/v1/validators/{status}/count`
|
||||
|
||||
|
||||
- Concurrently backfill and forwards fill, so forwards fill is not bottlenecked by large backfills.
|
||||
|
||||
|
||||
- Better/prettier (async?) logging.
|
||||
|
||||
|
||||
- Connect to a range of beacon_nodes to sync different components concurrently.
|
||||
Generally, processing certain api queries such as `block_packing` and `attestation_performance` take the longest to sync.
|
||||
|
||||
|
||||
### Architecture
|
||||
Connection Pooling:
|
||||
- 1 Pool for Updater (read and write)
|
||||
- 1 Pool for HTTP Server (should be read only, although not sure if we can enforce this)
|
49
watch/config.yaml.default
Normal file
49
watch/config.yaml.default
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
database:
|
||||
user: "postgres"
|
||||
password: "postgres"
|
||||
dbname: "dev"
|
||||
default_dbname: "postgres"
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
connect_timeout_millis: 2000
|
||||
|
||||
server:
|
||||
listen_addr: "127.0.0.1"
|
||||
listen_port: 5059
|
||||
|
||||
updater:
|
||||
# The URL of the Beacon Node to perform sync tasks with.
|
||||
# Cannot yet accept multiple beacon nodes.
|
||||
beacon_node_url: "http://localhost:5052"
|
||||
# The number of epochs to backfill. Must be below 100.
|
||||
max_backfill_size_epochs: 2
|
||||
# The epoch at which to stop backfilling.
|
||||
backfill_stop_epoch: 0
|
||||
# Whether to sync the attestations table.
|
||||
attestations: true
|
||||
# Whether to sync the proposer_info table.
|
||||
proposer_info: true
|
||||
# Whether to sync the block_rewards table.
|
||||
block_rewards: true
|
||||
# Whether to sync the block_packing table.
|
||||
block_packing: true
|
||||
|
||||
blockprint:
|
||||
# Whether to sync client information from blockprint.
|
||||
enabled: false
|
||||
# The URL of the blockprint server.
|
||||
url: ""
|
||||
# The username used to authenticate to the blockprint server.
|
||||
username: ""
|
||||
# The password used to authenticate to the blockprint server.
|
||||
password: ""
|
||||
|
||||
# Log level.
|
||||
# Valid options are:
|
||||
# - "trace"
|
||||
# - "debug"
|
||||
# - "info"
|
||||
# - "warn"
|
||||
# - "error"
|
||||
log_level: "debug"
|
5
watch/diesel.toml
Normal file
5
watch/diesel.toml
Normal file
@ -0,0 +1,5 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/database/schema.rs"
|
0
watch/migrations/.gitkeep
Normal file
0
watch/migrations/.gitkeep
Normal file
@ -0,0 +1,6 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
36
watch/migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
36
watch/migrations/00000000000000_diesel_initial_setup/up.sql
Normal file
@ -0,0 +1,36 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
--
|
||||
-- # Example
|
||||
--
|
||||
-- ```sql
|
||||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
@ -0,0 +1 @@
|
||||
DROP TABLE canonical_slots
|
@ -0,0 +1,6 @@
|
||||
CREATE TABLE canonical_slots (
|
||||
slot integer PRIMARY KEY,
|
||||
root bytea NOT NULL,
|
||||
skipped boolean NOT NULL,
|
||||
beacon_block bytea UNIQUE
|
||||
)
|
@ -0,0 +1 @@
|
||||
DROP TABLE beacon_blocks
|
7
watch/migrations/2022-01-01-000001_beacon_blocks/up.sql
Normal file
7
watch/migrations/2022-01-01-000001_beacon_blocks/up.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE beacon_blocks (
|
||||
slot integer PRIMARY KEY REFERENCES canonical_slots(slot) ON DELETE CASCADE,
|
||||
root bytea REFERENCES canonical_slots(beacon_block) NOT NULL,
|
||||
parent_root bytea NOT NULL,
|
||||
attestation_count integer NOT NULL,
|
||||
transaction_count integer
|
||||
)
|
1
watch/migrations/2022-01-01-000002_validators/down.sql
Normal file
1
watch/migrations/2022-01-01-000002_validators/down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE validators
|
7
watch/migrations/2022-01-01-000002_validators/up.sql
Normal file
7
watch/migrations/2022-01-01-000002_validators/up.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE validators (
|
||||
index integer PRIMARY KEY,
|
||||
public_key bytea NOT NULL,
|
||||
status text NOT NULL,
|
||||
activation_epoch integer,
|
||||
exit_epoch integer
|
||||
)
|
@ -0,0 +1 @@
|
||||
DROP TABLE proposer_info
|
5
watch/migrations/2022-01-01-000003_proposer_info/up.sql
Normal file
5
watch/migrations/2022-01-01-000003_proposer_info/up.sql
Normal file
@ -0,0 +1,5 @@
|
||||
CREATE TABLE proposer_info (
|
||||
slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE,
|
||||
proposer_index integer REFERENCES validators(index) ON DELETE CASCADE NOT NULL,
|
||||
graffiti text NOT NULL
|
||||
)
|
@ -0,0 +1 @@
|
||||
DROP TABLE active_config
|
5
watch/migrations/2022-01-01-000004_active_config/up.sql
Normal file
5
watch/migrations/2022-01-01-000004_active_config/up.sql
Normal file
@ -0,0 +1,5 @@
|
||||
CREATE TABLE active_config (
|
||||
id integer PRIMARY KEY CHECK (id=1),
|
||||
config_name text NOT NULL,
|
||||
slots_per_epoch integer NOT NULL
|
||||
)
|
1
watch/migrations/2022-01-01-000010_blockprint/down.sql
Normal file
1
watch/migrations/2022-01-01-000010_blockprint/down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE blockprint
|
4
watch/migrations/2022-01-01-000010_blockprint/up.sql
Normal file
4
watch/migrations/2022-01-01-000010_blockprint/up.sql
Normal file
@ -0,0 +1,4 @@
|
||||
CREATE TABLE blockprint (
|
||||
slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE,
|
||||
best_guess text NOT NULL
|
||||
)
|
@ -0,0 +1 @@
|
||||
DROP TABLE block_rewards
|
6
watch/migrations/2022-01-01-000011_block_rewards/up.sql
Normal file
6
watch/migrations/2022-01-01-000011_block_rewards/up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE block_rewards (
|
||||
slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE,
|
||||
total integer NOT NULL,
|
||||
attestation_reward integer NOT NULL,
|
||||
sync_committee_reward integer NOT NULL
|
||||
)
|
@ -0,0 +1 @@
|
||||
DROP TABLE block_packing
|
6
watch/migrations/2022-01-01-000012_block_packing/up.sql
Normal file
6
watch/migrations/2022-01-01-000012_block_packing/up.sql
Normal file
@ -0,0 +1,6 @@
|
||||
CREATE TABLE block_packing (
|
||||
slot integer PRIMARY KEY REFERENCES beacon_blocks(slot) ON DELETE CASCADE,
|
||||
available integer NOT NULL,
|
||||
included integer NOT NULL,
|
||||
prior_skip_slots integer NOT NULL
|
||||
)
|
@ -0,0 +1 @@
|
||||
DROP TABLE suboptimal_attestations
|
@ -0,0 +1,8 @@
|
||||
CREATE TABLE suboptimal_attestations (
|
||||
epoch_start_slot integer CHECK (epoch_start_slot % 32 = 0) REFERENCES canonical_slots(slot) ON DELETE CASCADE,
|
||||
index integer NOT NULL REFERENCES validators(index) ON DELETE CASCADE,
|
||||
source boolean NOT NULL,
|
||||
head boolean NOT NULL,
|
||||
target boolean NOT NULL,
|
||||
PRIMARY KEY(epoch_start_slot, index)
|
||||
)
|
2
watch/migrations/2022-01-01-000020_capella/down.sql
Normal file
2
watch/migrations/2022-01-01-000020_capella/down.sql
Normal file
@ -0,0 +1,2 @@
|
||||
ALTER TABLE beacon_blocks
|
||||
DROP COLUMN withdrawal_count;
|
3
watch/migrations/2022-01-01-000020_capella/up.sql
Normal file
3
watch/migrations/2022-01-01-000020_capella/up.sql
Normal file
@ -0,0 +1,3 @@
|
||||
ALTER TABLE beacon_blocks
|
||||
ADD COLUMN withdrawal_count integer;
|
||||
|
16
watch/postgres_docker_compose/compose.yml
Normal file
16
watch/postgres_docker_compose/compose.yml
Normal file
@ -0,0 +1,16 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:12.3-alpine
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
volumes:
|
||||
- postgres:/var/lib/postgresql/data
|
||||
ports:
|
||||
- 127.0.0.1:5432:5432
|
||||
|
||||
volumes:
|
||||
postgres:
|
140
watch/src/block_packing/database.rs
Normal file
140
watch/src/block_packing/database.rs
Normal file
@ -0,0 +1,140 @@
|
||||
use crate::database::{
|
||||
schema::{beacon_blocks, block_packing},
|
||||
watch_types::{WatchHash, WatchSlot},
|
||||
Error, PgConn, MAX_SIZE_BATCH_INSERT,
|
||||
};
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel::{Insertable, Queryable};
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = block_packing)]
|
||||
pub struct WatchBlockPacking {
|
||||
pub slot: WatchSlot,
|
||||
pub available: i32,
|
||||
pub included: i32,
|
||||
pub prior_skip_slots: i32,
|
||||
}
|
||||
|
||||
/// Insert a batch of values into the `block_packing` table.
|
||||
///
|
||||
/// On a conflict, it will do nothing, leaving the old value.
|
||||
pub fn insert_batch_block_packing(
|
||||
conn: &mut PgConn,
|
||||
packing: Vec<WatchBlockPacking>,
|
||||
) -> Result<(), Error> {
|
||||
use self::block_packing::dsl::*;
|
||||
|
||||
let mut count = 0;
|
||||
let timer = Instant::now();
|
||||
|
||||
for chunk in packing.chunks(MAX_SIZE_BATCH_INSERT) {
|
||||
count += diesel::insert_into(block_packing)
|
||||
.values(chunk)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block packing inserted, count: {count}, time taken: {time_taken:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Selects the row from the `block_packing` table where `slot` is minimum.
|
||||
pub fn get_lowest_block_packing(conn: &mut PgConn) -> Result<Option<WatchBlockPacking>, Error> {
|
||||
use self::block_packing::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = block_packing
|
||||
.order_by(slot.asc())
|
||||
.limit(1)
|
||||
.first::<WatchBlockPacking>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block packing requested: lowest, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects the row from the `block_packing` table where `slot` is maximum.
|
||||
pub fn get_highest_block_packing(conn: &mut PgConn) -> Result<Option<WatchBlockPacking>, Error> {
|
||||
use self::block_packing::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = block_packing
|
||||
.order_by(slot.desc())
|
||||
.limit(1)
|
||||
.first::<WatchBlockPacking>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block packing requested: highest, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `block_packing` table corresponding to a given `root_query`.
|
||||
pub fn get_block_packing_by_root(
|
||||
conn: &mut PgConn,
|
||||
root_query: WatchHash,
|
||||
) -> Result<Option<WatchBlockPacking>, Error> {
|
||||
use self::beacon_blocks::dsl::{beacon_blocks, root};
|
||||
use self::block_packing::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let join = beacon_blocks.inner_join(block_packing);
|
||||
|
||||
let result = join
|
||||
.select((slot, available, included, prior_skip_slots))
|
||||
.filter(root.eq(root_query))
|
||||
.first::<WatchBlockPacking>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block packing requested: {root_query}, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `block_packing` table corresponding to a given `slot_query`.
|
||||
pub fn get_block_packing_by_slot(
|
||||
conn: &mut PgConn,
|
||||
slot_query: WatchSlot,
|
||||
) -> Result<Option<WatchBlockPacking>, Error> {
|
||||
use self::block_packing::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = block_packing
|
||||
.filter(slot.eq(slot_query))
|
||||
.first::<WatchBlockPacking>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block packing requested: {slot_query}, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding
|
||||
/// row in `block_packing`.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_unknown_block_packing(
|
||||
conn: &mut PgConn,
|
||||
slots_per_epoch: u64,
|
||||
) -> Result<Vec<Option<WatchSlot>>, Error> {
|
||||
use self::beacon_blocks::dsl::{beacon_blocks, root, slot};
|
||||
use self::block_packing::dsl::block_packing;
|
||||
|
||||
let join = beacon_blocks.left_join(block_packing);
|
||||
|
||||
let result = join
|
||||
.select(slot)
|
||||
.filter(root.is_null())
|
||||
// Block packing cannot be retrieved for epoch 0 so we need to exclude them.
|
||||
.filter(slot.ge(slots_per_epoch as i32))
|
||||
.order_by(slot.desc())
|
||||
.nullable()
|
||||
.load::<Option<WatchSlot>>(conn)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
38
watch/src/block_packing/mod.rs
Normal file
38
watch/src/block_packing/mod.rs
Normal file
@ -0,0 +1,38 @@
|
||||
pub mod database;
|
||||
pub mod server;
|
||||
pub mod updater;
|
||||
|
||||
use crate::database::watch_types::WatchSlot;
|
||||
use crate::updater::error::Error;
|
||||
|
||||
pub use database::{
|
||||
get_block_packing_by_root, get_block_packing_by_slot, get_highest_block_packing,
|
||||
get_lowest_block_packing, get_unknown_block_packing, insert_batch_block_packing,
|
||||
WatchBlockPacking,
|
||||
};
|
||||
pub use server::block_packing_routes;
|
||||
|
||||
use eth2::BeaconNodeHttpClient;
|
||||
use types::Epoch;
|
||||
|
||||
/// Sends a request to `lighthouse/analysis/block_packing`.
|
||||
/// Formats the response into a vector of `WatchBlockPacking`.
|
||||
///
|
||||
/// Will fail if `start_epoch == 0`.
|
||||
pub async fn get_block_packing(
|
||||
bn: &BeaconNodeHttpClient,
|
||||
start_epoch: Epoch,
|
||||
end_epoch: Epoch,
|
||||
) -> Result<Vec<WatchBlockPacking>, Error> {
|
||||
Ok(bn
|
||||
.get_lighthouse_analysis_block_packing(start_epoch, end_epoch)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|data| WatchBlockPacking {
|
||||
slot: WatchSlot::from_slot(data.slot),
|
||||
available: data.available_attestations as i32,
|
||||
included: data.included_attestations as i32,
|
||||
prior_skip_slots: data.prior_skip_slots as i32,
|
||||
})
|
||||
.collect())
|
||||
}
|
31
watch/src/block_packing/server.rs
Normal file
31
watch/src/block_packing/server.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use crate::block_packing::database::{
|
||||
get_block_packing_by_root, get_block_packing_by_slot, WatchBlockPacking,
|
||||
};
|
||||
use crate::database::{get_connection, PgPool, WatchHash, WatchSlot};
|
||||
use crate::server::Error;
|
||||
|
||||
use axum::{extract::Path, routing::get, Extension, Json, Router};
|
||||
use eth2::types::BlockId;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub async fn get_block_packing(
|
||||
Path(block_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchBlockPacking>>, Error> {
|
||||
let mut conn = get_connection(&pool).map_err(Error::Database)?;
|
||||
match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? {
|
||||
BlockId::Root(root) => Ok(Json(get_block_packing_by_root(
|
||||
&mut conn,
|
||||
WatchHash::from_hash(root),
|
||||
)?)),
|
||||
BlockId::Slot(slot) => Ok(Json(get_block_packing_by_slot(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(slot),
|
||||
)?)),
|
||||
_ => Err(Error::BadRequest),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_packing_routes() -> Router {
|
||||
Router::new().route("/v1/blocks/:block/packing", get(get_block_packing))
|
||||
}
|
211
watch/src/block_packing/updater.rs
Normal file
211
watch/src/block_packing/updater.rs
Normal file
@ -0,0 +1,211 @@
|
||||
use crate::database::{self, Error as DbError};
|
||||
use crate::updater::{Error, UpdateHandler};
|
||||
|
||||
use crate::block_packing::get_block_packing;
|
||||
|
||||
use eth2::types::{Epoch, EthSpec};
|
||||
use log::{debug, error, warn};
|
||||
|
||||
const MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING: u64 = 50;
|
||||
|
||||
impl<T: EthSpec> UpdateHandler<T> {
|
||||
/// Forward fills the `block_packing` table starting from the entry with the
|
||||
/// highest slot.
|
||||
///
|
||||
/// It constructs a request to the `get_block_packing` API with:
|
||||
/// `start_epoch` -> highest completely filled epoch + 1 (or epoch of lowest beacon block)
|
||||
/// `end_epoch` -> epoch of highest beacon block
|
||||
///
|
||||
/// It will resync the latest epoch if it is not fully filled.
|
||||
/// That is, `if highest_filled_slot % slots_per_epoch != 31`
|
||||
/// This means that if the last slot of an epoch is a skip slot, the whole epoch will be
|
||||
//// resynced during the next head update.
|
||||
///
|
||||
/// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`.
|
||||
pub async fn fill_block_packing(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
// Get the slot of the highest entry in the `block_packing` table.
|
||||
let highest_filled_slot_opt = if self.config.block_packing {
|
||||
database::get_highest_block_packing(&mut conn)?.map(|packing| packing.slot)
|
||||
} else {
|
||||
return Err(Error::NotEnabled("block_packing".to_string()));
|
||||
};
|
||||
|
||||
let mut start_epoch = if let Some(highest_filled_slot) = highest_filled_slot_opt {
|
||||
if highest_filled_slot.as_slot() % self.slots_per_epoch
|
||||
== self.slots_per_epoch.saturating_sub(1)
|
||||
{
|
||||
// The whole epoch is filled so we can begin syncing the next one.
|
||||
highest_filled_slot.as_slot().epoch(self.slots_per_epoch) + 1
|
||||
} else {
|
||||
// The epoch is only partially synced. Try to sync it fully.
|
||||
highest_filled_slot.as_slot().epoch(self.slots_per_epoch)
|
||||
}
|
||||
} else {
|
||||
// No entries in the `block_packing` table. Use `beacon_blocks` instead.
|
||||
if let Some(lowest_beacon_block) = database::get_lowest_beacon_block(&mut conn)? {
|
||||
lowest_beacon_block
|
||||
.slot
|
||||
.as_slot()
|
||||
.epoch(self.slots_per_epoch)
|
||||
} else {
|
||||
// There are no blocks in the database, do not fill the `block_packing` table.
|
||||
warn!("Refusing to fill block packing as there are no blocks in the database");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// The `get_block_packing` API endpoint cannot accept `start_epoch == 0`.
|
||||
if start_epoch == 0 {
|
||||
start_epoch += 1
|
||||
}
|
||||
|
||||
if let Some(highest_block_slot) =
|
||||
database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot())
|
||||
{
|
||||
let mut end_epoch = highest_block_slot.epoch(self.slots_per_epoch);
|
||||
|
||||
if start_epoch > end_epoch {
|
||||
debug!("Block packing is up to date with the head of the database");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure the size of the request does not exceed the maximum allowed value.
|
||||
if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING) {
|
||||
end_epoch = start_epoch + MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING
|
||||
}
|
||||
|
||||
if let Some(lowest_block_slot) =
|
||||
database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot())
|
||||
{
|
||||
let mut packing = get_block_packing(&self.bn, start_epoch, end_epoch).await?;
|
||||
|
||||
// Since we pull a full epoch of data but are not guaranteed to have all blocks of
|
||||
// that epoch available, only insert blocks with corresponding `beacon_block`s.
|
||||
packing.retain(|packing| {
|
||||
packing.slot.as_slot() >= lowest_block_slot
|
||||
&& packing.slot.as_slot() <= highest_block_slot
|
||||
});
|
||||
database::insert_batch_block_packing(&mut conn, packing)?;
|
||||
} else {
|
||||
return Err(Error::Database(DbError::Other(
|
||||
"Database did not return a lowest block when one exists".to_string(),
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// There are no blocks in the `beacon_blocks` database, but there are entries in the
|
||||
// `block_packing` table. This is a critical failure. It usually means someone has
|
||||
// manually tampered with the database tables and should not occur during normal
|
||||
// operation.
|
||||
error!("Database is corrupted. Please re-sync the database");
|
||||
return Err(Error::Database(DbError::DatabaseCorrupted));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Backfill the `block_packing` table starting from the entry with the lowest slot.
|
||||
///
|
||||
/// It constructs a request to the `get_block_packing` function with:
|
||||
/// `start_epoch` -> epoch of lowest_beacon_block
|
||||
/// `end_epoch` -> epoch of lowest filled `block_packing` - 1 (or epoch of highest beacon block)
|
||||
///
|
||||
/// It will resync the lowest epoch if it is not fully filled.
|
||||
/// That is, `if lowest_filled_slot % slots_per_epoch != 0`
|
||||
/// This means that if the last slot of an epoch is a skip slot, the whole epoch will be
|
||||
//// resynced during the next head update.
|
||||
///
|
||||
/// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`.
|
||||
pub async fn backfill_block_packing(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
let max_block_packing_backfill = self.config.max_backfill_size_epochs;
|
||||
|
||||
// Get the slot of the lowest entry in the `block_packing` table.
|
||||
let lowest_filled_slot_opt = if self.config.block_packing {
|
||||
database::get_lowest_block_packing(&mut conn)?.map(|packing| packing.slot)
|
||||
} else {
|
||||
return Err(Error::NotEnabled("block_packing".to_string()));
|
||||
};
|
||||
|
||||
let end_epoch = if let Some(lowest_filled_slot) = lowest_filled_slot_opt {
|
||||
if lowest_filled_slot.as_slot() % self.slots_per_epoch == 0 {
|
||||
lowest_filled_slot
|
||||
.as_slot()
|
||||
.epoch(self.slots_per_epoch)
|
||||
.saturating_sub(Epoch::new(1))
|
||||
} else {
|
||||
// The epoch is only partially synced. Try to sync it fully.
|
||||
lowest_filled_slot.as_slot().epoch(self.slots_per_epoch)
|
||||
}
|
||||
} else {
|
||||
// No entries in the `block_packing` table. Use `beacon_blocks` instead.
|
||||
if let Some(highest_beacon_block) =
|
||||
database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot)
|
||||
{
|
||||
highest_beacon_block.as_slot().epoch(self.slots_per_epoch)
|
||||
} else {
|
||||
// There are no blocks in the database, do not backfill the `block_packing` table.
|
||||
warn!("Refusing to backfill block packing as there are no blocks in the database");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if end_epoch <= 1 {
|
||||
debug!("Block packing backfill is complete");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(lowest_block_slot) =
|
||||
database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot())
|
||||
{
|
||||
let mut start_epoch = lowest_block_slot.epoch(self.slots_per_epoch);
|
||||
|
||||
if start_epoch >= end_epoch {
|
||||
debug!("Block packing is up to date with the base of the database");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure that the request range does not exceed `max_block_packing_backfill` or
|
||||
// `MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING`.
|
||||
if start_epoch < end_epoch.saturating_sub(max_block_packing_backfill) {
|
||||
start_epoch = end_epoch.saturating_sub(max_block_packing_backfill)
|
||||
}
|
||||
if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING) {
|
||||
start_epoch = end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_PACKING)
|
||||
}
|
||||
|
||||
// The `block_packing` API cannot accept `start_epoch == 0`.
|
||||
if start_epoch == 0 {
|
||||
start_epoch += 1
|
||||
}
|
||||
|
||||
if let Some(highest_block_slot) =
|
||||
database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot.as_slot())
|
||||
{
|
||||
let mut packing = get_block_packing(&self.bn, start_epoch, end_epoch).await?;
|
||||
|
||||
// Only insert blocks with corresponding `beacon_block`s.
|
||||
packing.retain(|packing| {
|
||||
packing.slot.as_slot() >= lowest_block_slot
|
||||
&& packing.slot.as_slot() <= highest_block_slot
|
||||
});
|
||||
|
||||
database::insert_batch_block_packing(&mut conn, packing)?;
|
||||
} else {
|
||||
return Err(Error::Database(DbError::Other(
|
||||
"Database did not return a lowest block when one exists".to_string(),
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// There are no blocks in the `beacon_blocks` database, but there are entries in the
|
||||
// `block_packing` table. This is a critical failure. It usually means someone has
|
||||
// manually tampered with the database tables and should not occur during normal
|
||||
// operation.
|
||||
error!("Database is corrupted. Please re-sync the database");
|
||||
return Err(Error::Database(DbError::DatabaseCorrupted));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
137
watch/src/block_rewards/database.rs
Normal file
137
watch/src/block_rewards/database.rs
Normal file
@ -0,0 +1,137 @@
|
||||
use crate::database::{
|
||||
schema::{beacon_blocks, block_rewards},
|
||||
watch_types::{WatchHash, WatchSlot},
|
||||
Error, PgConn, MAX_SIZE_BATCH_INSERT,
|
||||
};
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel::{Insertable, Queryable};
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
|
||||
#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = block_rewards)]
|
||||
pub struct WatchBlockRewards {
|
||||
pub slot: WatchSlot,
|
||||
pub total: i32,
|
||||
pub attestation_reward: i32,
|
||||
pub sync_committee_reward: i32,
|
||||
}
|
||||
|
||||
/// Insert a batch of values into the `block_rewards` table.
|
||||
///
|
||||
/// On a conflict, it will do nothing, leaving the old value.
|
||||
pub fn insert_batch_block_rewards(
|
||||
conn: &mut PgConn,
|
||||
rewards: Vec<WatchBlockRewards>,
|
||||
) -> Result<(), Error> {
|
||||
use self::block_rewards::dsl::*;
|
||||
|
||||
let mut count = 0;
|
||||
let timer = Instant::now();
|
||||
|
||||
for chunk in rewards.chunks(MAX_SIZE_BATCH_INSERT) {
|
||||
count += diesel::insert_into(block_rewards)
|
||||
.values(chunk)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block rewards inserted, count: {count}, time_taken: {time_taken:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Selects the row from the `block_rewards` table where `slot` is minimum.
|
||||
pub fn get_lowest_block_rewards(conn: &mut PgConn) -> Result<Option<WatchBlockRewards>, Error> {
|
||||
use self::block_rewards::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = block_rewards
|
||||
.order_by(slot.asc())
|
||||
.limit(1)
|
||||
.first::<WatchBlockRewards>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block rewards requested: lowest, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects the row from the `block_rewards` table where `slot` is maximum.
|
||||
pub fn get_highest_block_rewards(conn: &mut PgConn) -> Result<Option<WatchBlockRewards>, Error> {
|
||||
use self::block_rewards::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = block_rewards
|
||||
.order_by(slot.desc())
|
||||
.limit(1)
|
||||
.first::<WatchBlockRewards>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block rewards requested: highest, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `block_rewards` table corresponding to a given `root_query`.
|
||||
pub fn get_block_rewards_by_root(
|
||||
conn: &mut PgConn,
|
||||
root_query: WatchHash,
|
||||
) -> Result<Option<WatchBlockRewards>, Error> {
|
||||
use self::beacon_blocks::dsl::{beacon_blocks, root};
|
||||
use self::block_rewards::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let join = beacon_blocks.inner_join(block_rewards);
|
||||
|
||||
let result = join
|
||||
.select((slot, total, attestation_reward, sync_committee_reward))
|
||||
.filter(root.eq(root_query))
|
||||
.first::<WatchBlockRewards>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block rewards requested: {root_query}, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `block_rewards` table corresponding to a given `slot_query`.
|
||||
pub fn get_block_rewards_by_slot(
|
||||
conn: &mut PgConn,
|
||||
slot_query: WatchSlot,
|
||||
) -> Result<Option<WatchBlockRewards>, Error> {
|
||||
use self::block_rewards::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = block_rewards
|
||||
.filter(slot.eq(slot_query))
|
||||
.first::<WatchBlockRewards>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Block rewards requested: {slot_query}, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding
|
||||
/// row in `block_rewards`.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_unknown_block_rewards(conn: &mut PgConn) -> Result<Vec<Option<WatchSlot>>, Error> {
|
||||
use self::beacon_blocks::dsl::{beacon_blocks, root, slot};
|
||||
use self::block_rewards::dsl::block_rewards;
|
||||
|
||||
let join = beacon_blocks.left_join(block_rewards);
|
||||
|
||||
let result = join
|
||||
.select(slot)
|
||||
.filter(root.is_null())
|
||||
// Block rewards cannot be retrieved for `slot == 0` so we need to exclude it.
|
||||
.filter(slot.ne(0))
|
||||
.order_by(slot.desc())
|
||||
.nullable()
|
||||
.load::<Option<WatchSlot>>(conn)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
38
watch/src/block_rewards/mod.rs
Normal file
38
watch/src/block_rewards/mod.rs
Normal file
@ -0,0 +1,38 @@
|
||||
pub mod database;
|
||||
mod server;
|
||||
mod updater;
|
||||
|
||||
use crate::database::watch_types::WatchSlot;
|
||||
use crate::updater::error::Error;
|
||||
|
||||
pub use database::{
|
||||
get_block_rewards_by_root, get_block_rewards_by_slot, get_highest_block_rewards,
|
||||
get_lowest_block_rewards, get_unknown_block_rewards, insert_batch_block_rewards,
|
||||
WatchBlockRewards,
|
||||
};
|
||||
pub use server::block_rewards_routes;
|
||||
|
||||
use eth2::BeaconNodeHttpClient;
|
||||
use types::Slot;
|
||||
|
||||
/// Sends a request to `lighthouse/analysis/block_rewards`.
|
||||
/// Formats the response into a vector of `WatchBlockRewards`.
|
||||
///
|
||||
/// Will fail if `start_slot == 0`.
|
||||
pub async fn get_block_rewards(
|
||||
bn: &BeaconNodeHttpClient,
|
||||
start_slot: Slot,
|
||||
end_slot: Slot,
|
||||
) -> Result<Vec<WatchBlockRewards>, Error> {
|
||||
Ok(bn
|
||||
.get_lighthouse_analysis_block_rewards(start_slot, end_slot)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|data| WatchBlockRewards {
|
||||
slot: WatchSlot::from_slot(data.meta.slot),
|
||||
total: data.total as i32,
|
||||
attestation_reward: data.attestation_rewards.total as i32,
|
||||
sync_committee_reward: data.sync_committee_rewards as i32,
|
||||
})
|
||||
.collect())
|
||||
}
|
31
watch/src/block_rewards/server.rs
Normal file
31
watch/src/block_rewards/server.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use crate::block_rewards::database::{
|
||||
get_block_rewards_by_root, get_block_rewards_by_slot, WatchBlockRewards,
|
||||
};
|
||||
use crate::database::{get_connection, PgPool, WatchHash, WatchSlot};
|
||||
use crate::server::Error;
|
||||
|
||||
use axum::{extract::Path, routing::get, Extension, Json, Router};
|
||||
use eth2::types::BlockId;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub async fn get_block_rewards(
|
||||
Path(block_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchBlockRewards>>, Error> {
|
||||
let mut conn = get_connection(&pool).map_err(Error::Database)?;
|
||||
match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? {
|
||||
BlockId::Root(root) => Ok(Json(get_block_rewards_by_root(
|
||||
&mut conn,
|
||||
WatchHash::from_hash(root),
|
||||
)?)),
|
||||
BlockId::Slot(slot) => Ok(Json(get_block_rewards_by_slot(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(slot),
|
||||
)?)),
|
||||
_ => Err(Error::BadRequest),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_rewards_routes() -> Router {
|
||||
Router::new().route("/v1/blocks/:block/rewards", get(get_block_rewards))
|
||||
}
|
157
watch/src/block_rewards/updater.rs
Normal file
157
watch/src/block_rewards/updater.rs
Normal file
@ -0,0 +1,157 @@
|
||||
use crate::database::{self, Error as DbError};
|
||||
use crate::updater::{Error, UpdateHandler};
|
||||
|
||||
use crate::block_rewards::get_block_rewards;
|
||||
|
||||
use eth2::types::EthSpec;
|
||||
use log::{debug, error, warn};
|
||||
|
||||
const MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS: u64 = 1600;
|
||||
|
||||
impl<T: EthSpec> UpdateHandler<T> {
|
||||
/// Forward fills the `block_rewards` table starting from the entry with the
|
||||
/// highest slot.
|
||||
///
|
||||
/// It constructs a request to the `get_block_rewards` API with:
|
||||
/// `start_slot` -> highest filled `block_rewards` + 1 (or lowest beacon block)
|
||||
/// `end_slot` -> highest beacon block
|
||||
///
|
||||
/// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`.
|
||||
pub async fn fill_block_rewards(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
// Get the slot of the highest entry in the `block_rewards` table.
|
||||
let highest_filled_slot_opt = if self.config.block_rewards {
|
||||
database::get_highest_block_rewards(&mut conn)?.map(|reward| reward.slot)
|
||||
} else {
|
||||
return Err(Error::NotEnabled("block_rewards".to_string()));
|
||||
};
|
||||
|
||||
let mut start_slot = if let Some(highest_filled_slot) = highest_filled_slot_opt {
|
||||
highest_filled_slot.as_slot() + 1
|
||||
} else {
|
||||
// No entries in the `block_rewards` table. Use `beacon_blocks` instead.
|
||||
if let Some(lowest_beacon_block) =
|
||||
database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot)
|
||||
{
|
||||
lowest_beacon_block.as_slot()
|
||||
} else {
|
||||
// There are no blocks in the database, do not fill the `block_rewards` table.
|
||||
warn!("Refusing to fill block rewards as there are no blocks in the database");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// The `block_rewards` API cannot accept `start_slot == 0`.
|
||||
if start_slot == 0 {
|
||||
start_slot += 1;
|
||||
}
|
||||
|
||||
if let Some(highest_beacon_block) =
|
||||
database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot)
|
||||
{
|
||||
let mut end_slot = highest_beacon_block.as_slot();
|
||||
|
||||
if start_slot > end_slot {
|
||||
debug!("Block rewards are up to date with the head of the database");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure the size of the request does not exceed the maximum allowed value.
|
||||
if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS) {
|
||||
end_slot = start_slot + MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS
|
||||
}
|
||||
|
||||
let rewards = get_block_rewards(&self.bn, start_slot, end_slot).await?;
|
||||
database::insert_batch_block_rewards(&mut conn, rewards)?;
|
||||
} else {
|
||||
// There are no blocks in the `beacon_blocks` database, but there are entries in the
|
||||
// `block_rewards` table. This is a critical failure. It usually means someone has
|
||||
// manually tampered with the database tables and should not occur during normal
|
||||
// operation.
|
||||
error!("Database is corrupted. Please re-sync the database");
|
||||
return Err(Error::Database(DbError::DatabaseCorrupted));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Backfill the `block_rewards` tables starting from the entry with the
|
||||
/// lowest slot.
|
||||
///
|
||||
/// It constructs a request to the `get_block_rewards` API with:
|
||||
/// `start_slot` -> lowest_beacon_block
|
||||
/// `end_slot` -> lowest filled `block_rewards` - 1 (or highest beacon block)
|
||||
///
|
||||
/// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`.
|
||||
pub async fn backfill_block_rewards(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
let max_block_reward_backfill = self.config.max_backfill_size_epochs * self.slots_per_epoch;
|
||||
|
||||
// Get the slot of the lowest entry in the `block_rewards` table.
|
||||
let lowest_filled_slot_opt = if self.config.block_rewards {
|
||||
database::get_lowest_block_rewards(&mut conn)?.map(|reward| reward.slot)
|
||||
} else {
|
||||
return Err(Error::NotEnabled("block_rewards".to_string()));
|
||||
};
|
||||
|
||||
let end_slot = if let Some(lowest_filled_slot) = lowest_filled_slot_opt {
|
||||
lowest_filled_slot.as_slot().saturating_sub(1_u64)
|
||||
} else {
|
||||
// No entries in the `block_rewards` table. Use `beacon_blocks` instead.
|
||||
if let Some(highest_beacon_block) =
|
||||
database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot)
|
||||
{
|
||||
highest_beacon_block.as_slot()
|
||||
} else {
|
||||
// There are no blocks in the database, do not backfill the `block_rewards` table.
|
||||
warn!("Refusing to backfill block rewards as there are no blocks in the database");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if end_slot <= 1 {
|
||||
debug!("Block rewards backfill is complete");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(lowest_block_slot) = database::get_lowest_beacon_block(&mut conn)? {
|
||||
let mut start_slot = lowest_block_slot.slot.as_slot();
|
||||
|
||||
if start_slot >= end_slot {
|
||||
debug!("Block rewards are up to date with the base of the database");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure that the request range does not exceed `max_block_reward_backfill` or
|
||||
// `MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS`.
|
||||
if start_slot < end_slot.saturating_sub(max_block_reward_backfill) {
|
||||
start_slot = end_slot.saturating_sub(max_block_reward_backfill)
|
||||
}
|
||||
|
||||
if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS) {
|
||||
start_slot = end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCK_REWARDS)
|
||||
}
|
||||
|
||||
// The `block_rewards` API cannot accept `start_slot == 0`.
|
||||
if start_slot == 0 {
|
||||
start_slot += 1
|
||||
}
|
||||
|
||||
let rewards = get_block_rewards(&self.bn, start_slot, end_slot).await?;
|
||||
|
||||
if self.config.block_rewards {
|
||||
database::insert_batch_block_rewards(&mut conn, rewards)?;
|
||||
}
|
||||
} else {
|
||||
// There are no blocks in the `beacon_blocks` database, but there are entries in the
|
||||
// `block_rewards` table. This is a critical failure. It usually means someone has
|
||||
// manually tampered with the database tables and should not occur during normal
|
||||
// operation.
|
||||
error!("Database is corrupted. Please re-sync the database");
|
||||
return Err(Error::Database(DbError::DatabaseCorrupted));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
40
watch/src/blockprint/config.rs
Normal file
40
watch/src/blockprint/config.rs
Normal file
@ -0,0 +1,40 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const fn enabled() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub const fn url() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
pub const fn username() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
pub const fn password() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default = "enabled")]
|
||||
pub enabled: bool,
|
||||
#[serde(default = "url")]
|
||||
pub url: Option<String>,
|
||||
#[serde(default = "username")]
|
||||
pub username: Option<String>,
|
||||
#[serde(default = "password")]
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
enabled: enabled(),
|
||||
url: url(),
|
||||
username: username(),
|
||||
password: password(),
|
||||
}
|
||||
}
|
||||
}
|
224
watch/src/blockprint/database.rs
Normal file
224
watch/src/blockprint/database.rs
Normal file
@ -0,0 +1,224 @@
|
||||
use crate::database::{
|
||||
self,
|
||||
schema::{beacon_blocks, blockprint},
|
||||
watch_types::{WatchHash, WatchSlot},
|
||||
Error, PgConn, MAX_SIZE_BATCH_INSERT,
|
||||
};
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel::sql_types::{Integer, Text};
|
||||
use diesel::{Insertable, Queryable};
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
|
||||
type WatchConsensusClient = String;
|
||||
pub fn list_consensus_clients() -> Vec<WatchConsensusClient> {
|
||||
vec![
|
||||
"Lighthouse".to_string(),
|
||||
"Lodestar".to_string(),
|
||||
"Nimbus".to_string(),
|
||||
"Prysm".to_string(),
|
||||
"Teku".to_string(),
|
||||
"Unknown".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = blockprint)]
|
||||
pub struct WatchBlockprint {
|
||||
pub slot: WatchSlot,
|
||||
pub best_guess: WatchConsensusClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, QueryableByName, diesel::FromSqlRow)]
|
||||
pub struct WatchValidatorBlockprint {
|
||||
#[diesel(sql_type = Integer)]
|
||||
pub proposer_index: i32,
|
||||
#[diesel(sql_type = Text)]
|
||||
pub best_guess: WatchConsensusClient,
|
||||
#[diesel(sql_type = Integer)]
|
||||
pub slot: WatchSlot,
|
||||
}
|
||||
|
||||
/// Insert a batch of values into the `blockprint` table.
|
||||
///
|
||||
/// On a conflict, it will do nothing, leaving the old value.
|
||||
pub fn insert_batch_blockprint(
|
||||
conn: &mut PgConn,
|
||||
prints: Vec<WatchBlockprint>,
|
||||
) -> Result<(), Error> {
|
||||
use self::blockprint::dsl::*;
|
||||
|
||||
let mut count = 0;
|
||||
let timer = Instant::now();
|
||||
|
||||
for chunk in prints.chunks(MAX_SIZE_BATCH_INSERT) {
|
||||
count += diesel::insert_into(blockprint)
|
||||
.values(chunk)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Blockprint inserted, count: {count}, time_taken: {time_taken:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Selects the row from the `blockprint` table where `slot` is minimum.
|
||||
pub fn get_lowest_blockprint(conn: &mut PgConn) -> Result<Option<WatchBlockprint>, Error> {
|
||||
use self::blockprint::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = blockprint
|
||||
.order_by(slot.asc())
|
||||
.limit(1)
|
||||
.first::<WatchBlockprint>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Blockprint requested: lowest, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects the row from the `blockprint` table where `slot` is maximum.
|
||||
pub fn get_highest_blockprint(conn: &mut PgConn) -> Result<Option<WatchBlockprint>, Error> {
|
||||
use self::blockprint::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = blockprint
|
||||
.order_by(slot.desc())
|
||||
.limit(1)
|
||||
.first::<WatchBlockprint>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Blockprint requested: highest, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `blockprint` table corresponding to a given `root_query`.
|
||||
pub fn get_blockprint_by_root(
|
||||
conn: &mut PgConn,
|
||||
root_query: WatchHash,
|
||||
) -> Result<Option<WatchBlockprint>, Error> {
|
||||
use self::beacon_blocks::dsl::{beacon_blocks, root};
|
||||
use self::blockprint::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let join = beacon_blocks.inner_join(blockprint);
|
||||
|
||||
let result = join
|
||||
.select((slot, best_guess))
|
||||
.filter(root.eq(root_query))
|
||||
.first::<WatchBlockprint>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Blockprint requested: {root_query}, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `blockprint` table corresponding to a given `slot_query`.
|
||||
pub fn get_blockprint_by_slot(
|
||||
conn: &mut PgConn,
|
||||
slot_query: WatchSlot,
|
||||
) -> Result<Option<WatchBlockprint>, Error> {
|
||||
use self::blockprint::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = blockprint
|
||||
.filter(slot.eq(slot_query))
|
||||
.first::<WatchBlockprint>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Blockprint requested: {slot_query}, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects `slot` from all rows of the `beacon_blocks` table which do not have a corresponding
|
||||
/// row in `blockprint`.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_unknown_blockprint(conn: &mut PgConn) -> Result<Vec<Option<WatchSlot>>, Error> {
|
||||
use self::beacon_blocks::dsl::{beacon_blocks, root, slot};
|
||||
use self::blockprint::dsl::blockprint;
|
||||
|
||||
let join = beacon_blocks.left_join(blockprint);
|
||||
|
||||
let result = join
|
||||
.select(slot)
|
||||
.filter(root.is_null())
|
||||
.order_by(slot.desc())
|
||||
.nullable()
|
||||
.load::<Option<WatchSlot>>(conn)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Constructs a HashMap of `index` -> `best_guess` for each validator's latest proposal at or before
|
||||
/// `target_slot`.
|
||||
/// Inserts `"Unknown" if no prior proposals exist.
|
||||
pub fn construct_validator_blockprints_at_slot(
|
||||
conn: &mut PgConn,
|
||||
target_slot: WatchSlot,
|
||||
slots_per_epoch: u64,
|
||||
) -> Result<HashMap<i32, WatchConsensusClient>, Error> {
|
||||
use self::blockprint::dsl::{blockprint, slot};
|
||||
|
||||
let total_validators =
|
||||
database::count_validators_activated_before_slot(conn, target_slot, slots_per_epoch)?
|
||||
as usize;
|
||||
|
||||
let mut blockprint_map = HashMap::with_capacity(total_validators);
|
||||
|
||||
let latest_proposals =
|
||||
database::get_all_validators_latest_proposer_info_at_slot(conn, target_slot)?;
|
||||
|
||||
let latest_proposal_slots: Vec<WatchSlot> = latest_proposals.clone().into_keys().collect();
|
||||
|
||||
let result = blockprint
|
||||
.filter(slot.eq_any(latest_proposal_slots))
|
||||
.load::<WatchBlockprint>(conn)?;
|
||||
|
||||
// Insert the validators which have available blockprints.
|
||||
for print in result {
|
||||
if let Some(proposer) = latest_proposals.get(&print.slot) {
|
||||
blockprint_map.insert(*proposer, print.best_guess);
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the rest of the unknown validators.
|
||||
for validator_index in 0..total_validators {
|
||||
blockprint_map
|
||||
.entry(validator_index as i32)
|
||||
.or_insert_with(|| "Unknown".to_string());
|
||||
}
|
||||
|
||||
Ok(blockprint_map)
|
||||
}
|
||||
|
||||
/// Counts the number of occurances of each `client` present in the `validators` table at or before some
|
||||
/// `target_slot`.
|
||||
pub fn get_validators_clients_at_slot(
|
||||
conn: &mut PgConn,
|
||||
target_slot: WatchSlot,
|
||||
slots_per_epoch: u64,
|
||||
) -> Result<HashMap<WatchConsensusClient, usize>, Error> {
|
||||
let mut client_map: HashMap<WatchConsensusClient, usize> = HashMap::new();
|
||||
|
||||
// This includes all validators which were activated at or before `target_slot`.
|
||||
let validator_blockprints =
|
||||
construct_validator_blockprints_at_slot(conn, target_slot, slots_per_epoch)?;
|
||||
|
||||
for client in list_consensus_clients() {
|
||||
let count = validator_blockprints
|
||||
.iter()
|
||||
.filter(|(_, v)| (*v).clone() == client)
|
||||
.count();
|
||||
client_map.insert(client, count);
|
||||
}
|
||||
|
||||
Ok(client_map)
|
||||
}
|
149
watch/src/blockprint/mod.rs
Normal file
149
watch/src/blockprint/mod.rs
Normal file
@ -0,0 +1,149 @@
|
||||
pub mod database;
|
||||
pub mod server;
|
||||
pub mod updater;
|
||||
|
||||
mod config;
|
||||
|
||||
use crate::database::WatchSlot;
|
||||
|
||||
use eth2::SensitiveUrl;
|
||||
use reqwest::{Client, Response, Url};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
use types::Slot;
|
||||
|
||||
pub use config::Config;
|
||||
pub use database::{
|
||||
get_blockprint_by_root, get_blockprint_by_slot, get_highest_blockprint, get_lowest_blockprint,
|
||||
get_unknown_blockprint, get_validators_clients_at_slot, insert_batch_blockprint,
|
||||
list_consensus_clients, WatchBlockprint,
|
||||
};
|
||||
pub use server::blockprint_routes;
|
||||
|
||||
const TIMEOUT: Duration = Duration::from_secs(50);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Reqwest(reqwest::Error),
|
||||
Url(url::ParseError),
|
||||
BlockprintNotSynced,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Error::Reqwest(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(e: url::ParseError) -> Self {
|
||||
Error::Url(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WatchBlockprintClient {
|
||||
pub client: Client,
|
||||
pub server: SensitiveUrl,
|
||||
pub username: Option<String>,
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct BlockprintSyncingResponse {
|
||||
pub greatest_block_slot: Slot,
|
||||
pub synced: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct BlockprintResponse {
|
||||
pub proposer_index: i32,
|
||||
pub slot: Slot,
|
||||
pub best_guess_single: String,
|
||||
}
|
||||
|
||||
impl WatchBlockprintClient {
|
||||
async fn get(&self, url: Url) -> Result<Response, Error> {
|
||||
let mut builder = self.client.get(url).timeout(TIMEOUT);
|
||||
if let Some(username) = &self.username {
|
||||
builder = builder.basic_auth(username, self.password.as_ref());
|
||||
}
|
||||
let response = builder.send().await.map_err(Error::Reqwest)?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(Error::Other(response.text().await?));
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
// Returns the `greatest_block_slot` as reported by the Blockprint server.
|
||||
// Will error if the Blockprint server is not synced.
|
||||
#[allow(dead_code)]
|
||||
pub async fn ensure_synced(&self) -> Result<Slot, Error> {
|
||||
let url = self.server.full.join("sync/")?.join("status")?;
|
||||
|
||||
let response = self.get(url).await?;
|
||||
|
||||
let result = response.json::<BlockprintSyncingResponse>().await?;
|
||||
if !result.synced {
|
||||
return Err(Error::BlockprintNotSynced);
|
||||
}
|
||||
|
||||
Ok(result.greatest_block_slot)
|
||||
}
|
||||
|
||||
// Pulls the latest blockprint for all validators.
|
||||
#[allow(dead_code)]
|
||||
pub async fn blockprint_all_validators(
|
||||
&self,
|
||||
highest_validator: i32,
|
||||
) -> Result<HashMap<i32, String>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.full
|
||||
.join("validator/")?
|
||||
.join("blocks/")?
|
||||
.join("latest")?;
|
||||
|
||||
let response = self.get(url).await?;
|
||||
|
||||
let mut result = response.json::<Vec<BlockprintResponse>>().await?;
|
||||
result.retain(|print| print.proposer_index <= highest_validator);
|
||||
|
||||
let mut map: HashMap<i32, String> = HashMap::with_capacity(result.len());
|
||||
for print in result {
|
||||
map.insert(print.proposer_index, print.best_guess_single);
|
||||
}
|
||||
|
||||
Ok(map)
|
||||
}
|
||||
|
||||
// Construct a request to the Blockprint server for a range of slots between `start_slot` and
|
||||
// `end_slot`.
|
||||
pub async fn get_blockprint(
|
||||
&self,
|
||||
start_slot: Slot,
|
||||
end_slot: Slot,
|
||||
) -> Result<Vec<WatchBlockprint>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.full
|
||||
.join("blocks/")?
|
||||
.join(&format!("{start_slot}/{end_slot}"))?;
|
||||
|
||||
let response = self.get(url).await?;
|
||||
|
||||
let result = response
|
||||
.json::<Vec<BlockprintResponse>>()
|
||||
.await?
|
||||
.iter()
|
||||
.map(|response| WatchBlockprint {
|
||||
slot: WatchSlot::from_slot(response.slot),
|
||||
best_guess: response.best_guess_single.clone(),
|
||||
})
|
||||
.collect();
|
||||
Ok(result)
|
||||
}
|
||||
}
|
31
watch/src/blockprint/server.rs
Normal file
31
watch/src/blockprint/server.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use crate::blockprint::database::{
|
||||
get_blockprint_by_root, get_blockprint_by_slot, WatchBlockprint,
|
||||
};
|
||||
use crate::database::{get_connection, PgPool, WatchHash, WatchSlot};
|
||||
use crate::server::Error;
|
||||
|
||||
use axum::{extract::Path, routing::get, Extension, Json, Router};
|
||||
use eth2::types::BlockId;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub async fn get_blockprint(
|
||||
Path(block_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchBlockprint>>, Error> {
|
||||
let mut conn = get_connection(&pool).map_err(Error::Database)?;
|
||||
match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? {
|
||||
BlockId::Root(root) => Ok(Json(get_blockprint_by_root(
|
||||
&mut conn,
|
||||
WatchHash::from_hash(root),
|
||||
)?)),
|
||||
BlockId::Slot(slot) => Ok(Json(get_blockprint_by_slot(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(slot),
|
||||
)?)),
|
||||
_ => Err(Error::BadRequest),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blockprint_routes() -> Router {
|
||||
Router::new().route("/v1/blocks/:block/blockprint", get(get_blockprint))
|
||||
}
|
172
watch/src/blockprint/updater.rs
Normal file
172
watch/src/blockprint/updater.rs
Normal file
@ -0,0 +1,172 @@
|
||||
use crate::database::{self, Error as DbError};
|
||||
use crate::updater::{Error, UpdateHandler};
|
||||
|
||||
use eth2::types::EthSpec;
|
||||
use log::{debug, error, warn};
|
||||
|
||||
const MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT: u64 = 1600;
|
||||
|
||||
impl<T: EthSpec> UpdateHandler<T> {
|
||||
/// Forward fills the `blockprint` table starting from the entry with the
|
||||
/// highest slot.
|
||||
///
|
||||
/// It constructs a request to the `get_blockprint` API with:
|
||||
/// `start_slot` -> highest filled `blockprint` + 1 (or lowest beacon block)
|
||||
/// `end_slot` -> highest beacon block
|
||||
///
|
||||
/// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`.
|
||||
pub async fn fill_blockprint(&mut self) -> Result<(), Error> {
|
||||
// Ensure blockprint in enabled.
|
||||
if let Some(blockprint_client) = &self.blockprint {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
// Get the slot of the highest entry in the `blockprint` table.
|
||||
let mut start_slot = if let Some(highest_filled_slot) =
|
||||
database::get_highest_blockprint(&mut conn)?.map(|print| print.slot)
|
||||
{
|
||||
highest_filled_slot.as_slot() + 1
|
||||
} else {
|
||||
// No entries in the `blockprint` table. Use `beacon_blocks` instead.
|
||||
if let Some(lowest_beacon_block) =
|
||||
database::get_lowest_beacon_block(&mut conn)?.map(|block| block.slot)
|
||||
{
|
||||
lowest_beacon_block.as_slot()
|
||||
} else {
|
||||
// There are no blocks in the database, do not fill the `blockprint` table.
|
||||
warn!("Refusing to fill blockprint as there are no blocks in the database");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
// The `blockprint` API cannot accept `start_slot == 0`.
|
||||
if start_slot == 0 {
|
||||
start_slot += 1;
|
||||
}
|
||||
|
||||
if let Some(highest_beacon_block) =
|
||||
database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot)
|
||||
{
|
||||
let mut end_slot = highest_beacon_block.as_slot();
|
||||
|
||||
if start_slot > end_slot {
|
||||
debug!("Blockprint is up to date with the head of the database");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure the size of the request does not exceed the maximum allowed value.
|
||||
if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT) {
|
||||
end_slot = start_slot + MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT
|
||||
}
|
||||
|
||||
let mut prints = blockprint_client
|
||||
.get_blockprint(start_slot, end_slot)
|
||||
.await?;
|
||||
|
||||
// Ensure the prints returned from blockprint are for slots which exist in the
|
||||
// `beacon_blocks` table.
|
||||
prints.retain(|print| {
|
||||
database::get_beacon_block_by_slot(&mut conn, print.slot)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some()
|
||||
});
|
||||
|
||||
database::insert_batch_blockprint(&mut conn, prints)?;
|
||||
} else {
|
||||
// There are no blocks in the `beacon_blocks` database, but there are entries in either
|
||||
// `blockprint` table. This is a critical failure. It usually means
|
||||
// someone has manually tampered with the database tables and should not occur during
|
||||
// normal operation.
|
||||
error!("Database is corrupted. Please re-sync the database");
|
||||
return Err(Error::Database(DbError::DatabaseCorrupted));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Backfill the `blockprint` table starting from the entry with the lowest slot.
|
||||
///
|
||||
/// It constructs a request to the `get_blockprint` API with:
|
||||
/// `start_slot` -> lowest_beacon_block
|
||||
/// `end_slot` -> lowest filled `blockprint` - 1 (or highest beacon block)
|
||||
///
|
||||
/// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`.
|
||||
pub async fn backfill_blockprint(&mut self) -> Result<(), Error> {
|
||||
// Ensure blockprint in enabled.
|
||||
if let Some(blockprint_client) = &self.blockprint {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
let max_blockprint_backfill =
|
||||
self.config.max_backfill_size_epochs * self.slots_per_epoch;
|
||||
|
||||
// Get the slot of the lowest entry in the `blockprint` table.
|
||||
let end_slot = if let Some(lowest_filled_slot) =
|
||||
database::get_lowest_blockprint(&mut conn)?.map(|print| print.slot)
|
||||
{
|
||||
lowest_filled_slot.as_slot().saturating_sub(1_u64)
|
||||
} else {
|
||||
// No entries in the `blockprint` table. Use `beacon_blocks` instead.
|
||||
if let Some(highest_beacon_block) =
|
||||
database::get_highest_beacon_block(&mut conn)?.map(|block| block.slot)
|
||||
{
|
||||
highest_beacon_block.as_slot()
|
||||
} else {
|
||||
// There are no blocks in the database, do not backfill the `blockprint` table.
|
||||
warn!("Refusing to backfill blockprint as there are no blocks in the database");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if end_slot <= 1 {
|
||||
debug!("Blockprint backfill is complete");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(lowest_block_slot) = database::get_lowest_beacon_block(&mut conn)? {
|
||||
let mut start_slot = lowest_block_slot.slot.as_slot();
|
||||
|
||||
if start_slot >= end_slot {
|
||||
debug!("Blockprint are up to date with the base of the database");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure that the request range does not exceed `max_blockprint_backfill` or
|
||||
// `MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT`.
|
||||
if start_slot < end_slot.saturating_sub(max_blockprint_backfill) {
|
||||
start_slot = end_slot.saturating_sub(max_blockprint_backfill)
|
||||
}
|
||||
|
||||
if start_slot < end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT) {
|
||||
start_slot = end_slot.saturating_sub(MAX_SIZE_SINGLE_REQUEST_BLOCKPRINT)
|
||||
}
|
||||
|
||||
// The `blockprint` API cannot accept `start_slot == 0`.
|
||||
if start_slot == 0 {
|
||||
start_slot += 1
|
||||
}
|
||||
|
||||
let mut prints = blockprint_client
|
||||
.get_blockprint(start_slot, end_slot)
|
||||
.await?;
|
||||
|
||||
// Ensure the prints returned from blockprint are for slots which exist in the
|
||||
// `beacon_blocks` table.
|
||||
prints.retain(|print| {
|
||||
database::get_beacon_block_by_slot(&mut conn, print.slot)
|
||||
.ok()
|
||||
.flatten()
|
||||
.is_some()
|
||||
});
|
||||
|
||||
database::insert_batch_blockprint(&mut conn, prints)?;
|
||||
} else {
|
||||
// There are no blocks in the `beacon_blocks` database, but there are entries in the `blockprint`
|
||||
// table. This is a critical failure. It usually means someone has manually tampered with the
|
||||
// database tables and should not occur during normal operation.
|
||||
error!("Database is corrupted. Please re-sync the database");
|
||||
return Err(Error::Database(DbError::DatabaseCorrupted));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
55
watch/src/cli.rs
Normal file
55
watch/src/cli.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use crate::{config::Config, logger, server, updater};
|
||||
use clap::{App, Arg};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub const SERVE: &str = "serve";
|
||||
pub const RUN_UPDATER: &str = "run-updater";
|
||||
pub const CONFIG: &str = "config";
|
||||
|
||||
fn run_updater<'a, 'b>() -> App<'a, 'b> {
|
||||
App::new(RUN_UPDATER).setting(clap::AppSettings::ColoredHelp)
|
||||
}
|
||||
|
||||
fn serve<'a, 'b>() -> App<'a, 'b> {
|
||||
App::new(SERVE).setting(clap::AppSettings::ColoredHelp)
|
||||
}
|
||||
|
||||
pub fn app<'a, 'b>() -> App<'a, 'b> {
|
||||
App::new("beacon_watch_daemon")
|
||||
.author("Sigma Prime <contact@sigmaprime.io>")
|
||||
.setting(clap::AppSettings::ColoredHelp)
|
||||
.arg(
|
||||
Arg::with_name(CONFIG)
|
||||
.long(CONFIG)
|
||||
.value_name("PATH_TO_CONFIG")
|
||||
.help("Path to configuration file")
|
||||
.takes_value(true)
|
||||
.global(true),
|
||||
)
|
||||
.subcommand(run_updater())
|
||||
.subcommand(serve())
|
||||
}
|
||||
|
||||
pub async fn run() -> Result<(), String> {
|
||||
let matches = app().get_matches();
|
||||
|
||||
let config = match matches.value_of(CONFIG) {
|
||||
Some(path) => Config::load_from_file(path.to_string())?,
|
||||
None => Config::default(),
|
||||
};
|
||||
|
||||
logger::init_logger(&config.log_level);
|
||||
|
||||
match matches.subcommand() {
|
||||
(RUN_UPDATER, Some(_)) => updater::run_updater(config)
|
||||
.await
|
||||
.map_err(|e| format!("Failure: {:?}", e)),
|
||||
(SERVE, Some(_)) => {
|
||||
let (_shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
server::serve(config, shutdown_rx)
|
||||
.await
|
||||
.map_err(|e| format!("Failure: {:?}", e))
|
||||
}
|
||||
_ => Err("Unsupported subcommand. See --help".into()),
|
||||
}
|
||||
}
|
178
watch/src/client.rs
Normal file
178
watch/src/client.rs
Normal file
@ -0,0 +1,178 @@
|
||||
use crate::block_packing::WatchBlockPacking;
|
||||
use crate::block_rewards::WatchBlockRewards;
|
||||
use crate::database::models::{
|
||||
WatchBeaconBlock, WatchCanonicalSlot, WatchProposerInfo, WatchValidator,
|
||||
};
|
||||
use crate::suboptimal_attestations::WatchAttestation;
|
||||
|
||||
use eth2::types::BlockId;
|
||||
use reqwest::Client;
|
||||
use serde::de::DeserializeOwned;
|
||||
use types::Hash256;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Reqwest(reqwest::Error),
|
||||
Url(url::ParseError),
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for Error {
|
||||
fn from(e: reqwest::Error) -> Self {
|
||||
Error::Reqwest(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<url::ParseError> for Error {
|
||||
fn from(e: url::ParseError) -> Self {
|
||||
Error::Url(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WatchHttpClient {
|
||||
pub client: Client,
|
||||
pub server: Url,
|
||||
}
|
||||
|
||||
impl WatchHttpClient {
|
||||
async fn get_opt<T: DeserializeOwned>(&self, url: Url) -> Result<Option<T>, Error> {
|
||||
let response = self.client.get(url).send().await?;
|
||||
|
||||
if response.status() == 404 {
|
||||
Ok(None)
|
||||
} else {
|
||||
response
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_beacon_blocks(
|
||||
&self,
|
||||
block_id: BlockId,
|
||||
) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.join("v1/")?
|
||||
.join("blocks/")?
|
||||
.join(&block_id.to_string())?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_lowest_canonical_slot(&self) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
let url = self.server.join("v1/")?.join("slots/")?.join("lowest")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_highest_canonical_slot(&self) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
let url = self.server.join("v1/")?.join("slots/")?.join("highest")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_lowest_beacon_block(&self) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
let url = self.server.join("v1/")?.join("blocks/")?.join("lowest")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_highest_beacon_block(&self) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
let url = self.server.join("v1/")?.join("blocks/")?.join("highest")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_next_beacon_block(
|
||||
&self,
|
||||
parent: Hash256,
|
||||
) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.join("v1/")?
|
||||
.join("blocks/")?
|
||||
.join(&format!("{parent:?}/"))?
|
||||
.join("next")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_validator_by_index(
|
||||
&self,
|
||||
index: i32,
|
||||
) -> Result<Option<WatchValidator>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.join("v1/")?
|
||||
.join("validators/")?
|
||||
.join(&format!("{index}"))?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_proposer_info(
|
||||
&self,
|
||||
block_id: BlockId,
|
||||
) -> Result<Option<WatchProposerInfo>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.join("v1/")?
|
||||
.join("blocks/")?
|
||||
.join(&format!("{block_id}/"))?
|
||||
.join("proposer")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_block_reward(
|
||||
&self,
|
||||
block_id: BlockId,
|
||||
) -> Result<Option<WatchBlockRewards>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.join("v1/")?
|
||||
.join("blocks/")?
|
||||
.join(&format!("{block_id}/"))?
|
||||
.join("rewards")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_block_packing(
|
||||
&self,
|
||||
block_id: BlockId,
|
||||
) -> Result<Option<WatchBlockPacking>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.join("v1/")?
|
||||
.join("blocks/")?
|
||||
.join(&format!("{block_id}/"))?
|
||||
.join("packing")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_all_validators(&self) -> Result<Option<Vec<WatchValidator>>, Error> {
|
||||
let url = self.server.join("v1/")?.join("validators/")?.join("all")?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
|
||||
pub async fn get_attestations(
|
||||
&self,
|
||||
epoch: i32,
|
||||
) -> Result<Option<Vec<WatchAttestation>>, Error> {
|
||||
let url = self
|
||||
.server
|
||||
.join("v1/")?
|
||||
.join("validators/")?
|
||||
.join("all/")?
|
||||
.join("attestation/")?
|
||||
.join(&format!("{epoch}"))?;
|
||||
|
||||
self.get_opt(url).await
|
||||
}
|
||||
}
|
50
watch/src/config.rs
Normal file
50
watch/src/config.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use crate::blockprint::Config as BlockprintConfig;
|
||||
use crate::database::Config as DatabaseConfig;
|
||||
use crate::server::Config as ServerConfig;
|
||||
use crate::updater::Config as UpdaterConfig;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
|
||||
pub const LOG_LEVEL: &str = "debug";
|
||||
|
||||
fn log_level() -> String {
|
||||
LOG_LEVEL.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub blockprint: BlockprintConfig,
|
||||
#[serde(default)]
|
||||
pub database: DatabaseConfig,
|
||||
#[serde(default)]
|
||||
pub server: ServerConfig,
|
||||
#[serde(default)]
|
||||
pub updater: UpdaterConfig,
|
||||
/// The minimum severity for log messages.
|
||||
#[serde(default = "log_level")]
|
||||
pub log_level: String,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blockprint: BlockprintConfig::default(),
|
||||
database: DatabaseConfig::default(),
|
||||
server: ServerConfig::default(),
|
||||
updater: UpdaterConfig::default(),
|
||||
log_level: log_level(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load_from_file(path_to_file: String) -> Result<Config, String> {
|
||||
let file =
|
||||
File::open(path_to_file).map_err(|e| format!("Error reading config file: {:?}", e))?;
|
||||
let config: Config = serde_yaml::from_reader(file)
|
||||
.map_err(|e| format!("Error parsing config file: {:?}", e))?;
|
||||
Ok(config)
|
||||
}
|
||||
}
|
49
watch/src/database/compat.rs
Normal file
49
watch/src/database/compat.rs
Normal file
@ -0,0 +1,49 @@
|
||||
//! Implementations of PostgreSQL compatibility traits.
|
||||
use crate::database::watch_types::{WatchHash, WatchPK, WatchSlot};
|
||||
use diesel::deserialize::{self, FromSql};
|
||||
use diesel::pg::{Pg, PgValue};
|
||||
use diesel::serialize::{self, Output, ToSql};
|
||||
use diesel::sql_types::{Binary, Integer};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
macro_rules! impl_to_from_sql_int {
|
||||
($type:ty) => {
|
||||
impl ToSql<Integer, Pg> for $type
|
||||
where
|
||||
i32: ToSql<Integer, Pg>,
|
||||
{
|
||||
fn to_sql<'a>(&'a self, out: &mut Output<'a, '_, Pg>) -> serialize::Result {
|
||||
let v = i32::try_from(self.as_u64()).map_err(|e| Box::new(e))?;
|
||||
<i32 as ToSql<Integer, Pg>>::to_sql(&v, &mut out.reborrow())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<Integer, Pg> for $type {
|
||||
fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
|
||||
Ok(Self::new(i32::from_sql(bytes)? as u64))
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! impl_to_from_sql_binary {
|
||||
($type:ty) => {
|
||||
impl ToSql<Binary, Pg> for $type {
|
||||
fn to_sql<'a>(&'a self, out: &mut Output<'a, '_, Pg>) -> serialize::Result {
|
||||
let b = self.as_bytes();
|
||||
<&[u8] as ToSql<Binary, Pg>>::to_sql(&b, &mut out.reborrow())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql<Binary, Pg> for $type {
|
||||
fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
|
||||
Self::from_bytes(bytes.as_bytes()).map_err(|e| e.to_string().into())
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_to_from_sql_int!(WatchSlot);
|
||||
impl_to_from_sql_binary!(WatchHash);
|
||||
impl_to_from_sql_binary!(WatchPK);
|
74
watch/src/database/config.rs
Normal file
74
watch/src/database/config.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const USER: &str = "postgres";
|
||||
pub const PASSWORD: &str = "postgres";
|
||||
pub const DBNAME: &str = "dev";
|
||||
pub const DEFAULT_DBNAME: &str = "postgres";
|
||||
pub const HOST: &str = "localhost";
|
||||
pub const fn port() -> u16 {
|
||||
5432
|
||||
}
|
||||
pub const fn connect_timeout_millis() -> u64 {
|
||||
2_000 // 2s
|
||||
}
|
||||
|
||||
fn user() -> String {
|
||||
USER.to_string()
|
||||
}
|
||||
|
||||
fn password() -> String {
|
||||
PASSWORD.to_string()
|
||||
}
|
||||
|
||||
fn dbname() -> String {
|
||||
DBNAME.to_string()
|
||||
}
|
||||
|
||||
fn default_dbname() -> String {
|
||||
DEFAULT_DBNAME.to_string()
|
||||
}
|
||||
|
||||
fn host() -> String {
|
||||
HOST.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default = "user")]
|
||||
pub user: String,
|
||||
#[serde(default = "password")]
|
||||
pub password: String,
|
||||
#[serde(default = "dbname")]
|
||||
pub dbname: String,
|
||||
#[serde(default = "default_dbname")]
|
||||
pub default_dbname: String,
|
||||
#[serde(default = "host")]
|
||||
pub host: String,
|
||||
#[serde(default = "port")]
|
||||
pub port: u16,
|
||||
#[serde(default = "connect_timeout_millis")]
|
||||
pub connect_timeout_millis: u64,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
user: user(),
|
||||
password: password(),
|
||||
dbname: dbname(),
|
||||
default_dbname: default_dbname(),
|
||||
host: host(),
|
||||
port: port(),
|
||||
connect_timeout_millis: connect_timeout_millis(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn build_database_url(&self) -> String {
|
||||
format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
self.user, self.password, self.host, self.port, self.dbname
|
||||
)
|
||||
}
|
||||
}
|
55
watch/src/database/error.rs
Normal file
55
watch/src/database/error.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use bls::Error as BlsError;
|
||||
use diesel::result::{ConnectionError, Error as PgError};
|
||||
use eth2::SensitiveError;
|
||||
use r2d2::Error as PoolError;
|
||||
use std::fmt;
|
||||
use types::BeaconStateError;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
BeaconState(BeaconStateError),
|
||||
Database(PgError),
|
||||
DatabaseCorrupted,
|
||||
InvalidSig(BlsError),
|
||||
PostgresConnection(ConnectionError),
|
||||
Pool(PoolError),
|
||||
SensitiveUrl(SensitiveError),
|
||||
InvalidRoot,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BeaconStateError> for Error {
|
||||
fn from(e: BeaconStateError) -> Self {
|
||||
Error::BeaconState(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConnectionError> for Error {
|
||||
fn from(e: ConnectionError) -> Self {
|
||||
Error::PostgresConnection(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PgError> for Error {
|
||||
fn from(e: PgError) -> Self {
|
||||
Error::Database(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PoolError> for Error {
|
||||
fn from(e: PoolError) -> Self {
|
||||
Error::Pool(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlsError> for Error {
|
||||
fn from(e: BlsError) -> Self {
|
||||
Error::InvalidSig(e)
|
||||
}
|
||||
}
|
782
watch/src/database/mod.rs
Normal file
782
watch/src/database/mod.rs
Normal file
@ -0,0 +1,782 @@
|
||||
mod config;
|
||||
mod error;
|
||||
|
||||
pub mod compat;
|
||||
pub mod models;
|
||||
pub mod schema;
|
||||
pub mod utils;
|
||||
pub mod watch_types;
|
||||
|
||||
use self::schema::{
|
||||
active_config, beacon_blocks, canonical_slots, proposer_info, suboptimal_attestations,
|
||||
validators,
|
||||
};
|
||||
|
||||
use diesel::dsl::max;
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use diesel::r2d2::{Builder, ConnectionManager, Pool, PooledConnection};
|
||||
use diesel::upsert::excluded;
|
||||
use log::{debug, info};
|
||||
use std::collections::HashMap;
|
||||
use std::time::Instant;
|
||||
use types::{EthSpec, SignedBeaconBlock};
|
||||
|
||||
pub use self::error::Error;
|
||||
pub use self::models::{WatchBeaconBlock, WatchCanonicalSlot, WatchProposerInfo, WatchValidator};
|
||||
pub use self::watch_types::{WatchHash, WatchPK, WatchSlot};
|
||||
|
||||
pub use crate::block_rewards::{
|
||||
get_block_rewards_by_root, get_block_rewards_by_slot, get_highest_block_rewards,
|
||||
get_lowest_block_rewards, get_unknown_block_rewards, insert_batch_block_rewards,
|
||||
WatchBlockRewards,
|
||||
};
|
||||
|
||||
pub use crate::block_packing::{
|
||||
get_block_packing_by_root, get_block_packing_by_slot, get_highest_block_packing,
|
||||
get_lowest_block_packing, get_unknown_block_packing, insert_batch_block_packing,
|
||||
WatchBlockPacking,
|
||||
};
|
||||
|
||||
pub use crate::suboptimal_attestations::{
|
||||
get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, get_attestation_by_pubkey,
|
||||
get_highest_attestation, get_lowest_attestation, insert_batch_suboptimal_attestations,
|
||||
WatchAttestation, WatchSuboptimalAttestation,
|
||||
};
|
||||
|
||||
pub use crate::blockprint::{
|
||||
get_blockprint_by_root, get_blockprint_by_slot, get_highest_blockprint, get_lowest_blockprint,
|
||||
get_unknown_blockprint, get_validators_clients_at_slot, insert_batch_blockprint,
|
||||
WatchBlockprint,
|
||||
};
|
||||
|
||||
pub use config::Config;
|
||||
|
||||
/// Batch inserts cannot exceed a certain size.
|
||||
/// See https://github.com/diesel-rs/diesel/issues/2414.
|
||||
/// For some reason, this seems to translate to 65535 / 5 (13107) records.
|
||||
pub const MAX_SIZE_BATCH_INSERT: usize = 13107;
|
||||
|
||||
pub type PgPool = Pool<ConnectionManager<PgConnection>>;
|
||||
pub type PgConn = PooledConnection<ConnectionManager<PgConnection>>;
|
||||
|
||||
/// Connect to a Postgresql database and build a connection pool.
|
||||
pub fn build_connection_pool(config: &Config) -> Result<PgPool, Error> {
|
||||
let database_url = config.clone().build_database_url();
|
||||
info!("Building connection pool at: {database_url}");
|
||||
let pg = ConnectionManager::<PgConnection>::new(&database_url);
|
||||
Builder::new().build(pg).map_err(Error::Pool)
|
||||
}
|
||||
|
||||
/// Retrieve an idle connection from the pool.
|
||||
pub fn get_connection(pool: &PgPool) -> Result<PgConn, Error> {
|
||||
pool.get().map_err(Error::Pool)
|
||||
}
|
||||
|
||||
/// Insert the active config into the database. This is used to check if the connected beacon node
|
||||
/// is compatible with the database. These values will not change (except
|
||||
/// `current_blockprint_checkpoint`).
|
||||
pub fn insert_active_config(
|
||||
conn: &mut PgConn,
|
||||
new_config_name: String,
|
||||
new_slots_per_epoch: u64,
|
||||
) -> Result<(), Error> {
|
||||
use self::active_config::dsl::*;
|
||||
|
||||
diesel::insert_into(active_config)
|
||||
.values(&vec![(
|
||||
id.eq(1),
|
||||
config_name.eq(new_config_name),
|
||||
slots_per_epoch.eq(new_slots_per_epoch as i32),
|
||||
)])
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the active config from the database.
|
||||
pub fn get_active_config(conn: &mut PgConn) -> Result<Option<(String, i32)>, Error> {
|
||||
use self::active_config::dsl::*;
|
||||
Ok(active_config
|
||||
.select((config_name, slots_per_epoch))
|
||||
.filter(id.eq(1))
|
||||
.first::<(String, i32)>(conn)
|
||||
.optional()?)
|
||||
}
|
||||
|
||||
///
|
||||
/// INSERT statements
|
||||
///
|
||||
|
||||
/// Inserts a single row into the `canonical_slots` table.
|
||||
/// If `new_slot.beacon_block` is `None`, the value in the row will be `null`.
|
||||
///
|
||||
/// On a conflict, it will do nothing, leaving the old value.
|
||||
pub fn insert_canonical_slot(conn: &mut PgConn, new_slot: WatchCanonicalSlot) -> Result<(), Error> {
|
||||
diesel::insert_into(canonical_slots::table)
|
||||
.values(&new_slot)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
|
||||
debug!("Canonical slot inserted: {}", new_slot.slot);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_beacon_block<T: EthSpec>(
|
||||
conn: &mut PgConn,
|
||||
block: SignedBeaconBlock<T>,
|
||||
root: WatchHash,
|
||||
) -> Result<(), Error> {
|
||||
use self::canonical_slots::dsl::{beacon_block, slot as canonical_slot};
|
||||
|
||||
let block_message = block.message();
|
||||
|
||||
// Pull out relevant values from the block.
|
||||
let slot = WatchSlot::from_slot(block.slot());
|
||||
let parent_root = WatchHash::from_hash(block.parent_root());
|
||||
let proposer_index = block_message.proposer_index() as i32;
|
||||
let graffiti = block_message.body().graffiti().as_utf8_lossy();
|
||||
let attestation_count = block_message.body().attestations().len() as i32;
|
||||
|
||||
let full_payload = block_message.execution_payload().ok();
|
||||
|
||||
let transaction_count: Option<i32> = if let Some(bellatrix_payload) =
|
||||
full_payload.and_then(|payload| payload.execution_payload_merge().ok())
|
||||
{
|
||||
Some(bellatrix_payload.transactions.len() as i32)
|
||||
} else {
|
||||
full_payload
|
||||
.and_then(|payload| payload.execution_payload_capella().ok())
|
||||
.map(|payload| payload.transactions.len() as i32)
|
||||
};
|
||||
|
||||
let withdrawal_count: Option<i32> = full_payload
|
||||
.and_then(|payload| payload.execution_payload_capella().ok())
|
||||
.map(|payload| payload.withdrawals.len() as i32);
|
||||
|
||||
let block_to_add = WatchBeaconBlock {
|
||||
slot,
|
||||
root,
|
||||
parent_root,
|
||||
attestation_count,
|
||||
transaction_count,
|
||||
withdrawal_count,
|
||||
};
|
||||
|
||||
let proposer_info_to_add = WatchProposerInfo {
|
||||
slot,
|
||||
proposer_index,
|
||||
graffiti,
|
||||
};
|
||||
|
||||
// Update the canonical slots table.
|
||||
diesel::update(canonical_slots::table)
|
||||
.set(beacon_block.eq(root))
|
||||
.filter(canonical_slot.eq(slot))
|
||||
// Do not overwrite the value if it already exists.
|
||||
.filter(beacon_block.is_null())
|
||||
.execute(conn)?;
|
||||
|
||||
diesel::insert_into(beacon_blocks::table)
|
||||
.values(block_to_add)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
|
||||
diesel::insert_into(proposer_info::table)
|
||||
.values(proposer_info_to_add)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
|
||||
debug!("Beacon block inserted at slot: {slot}, root: {root}, parent: {parent_root}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert a validator into the `validators` table
|
||||
///
|
||||
/// On a conflict, it will only overwrite `status`, `activation_epoch` and `exit_epoch`.
|
||||
pub fn insert_validator(conn: &mut PgConn, validator: WatchValidator) -> Result<(), Error> {
|
||||
use self::validators::dsl::*;
|
||||
let new_index = validator.index;
|
||||
let new_public_key = validator.public_key;
|
||||
|
||||
diesel::insert_into(validators)
|
||||
.values(validator)
|
||||
.on_conflict(index)
|
||||
.do_update()
|
||||
.set((
|
||||
status.eq(excluded(status)),
|
||||
activation_epoch.eq(excluded(activation_epoch)),
|
||||
exit_epoch.eq(excluded(exit_epoch)),
|
||||
))
|
||||
.execute(conn)?;
|
||||
|
||||
debug!("Validator inserted, index: {new_index}, public_key: {new_public_key}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert a batch of values into the `validators` table.
|
||||
///
|
||||
/// On a conflict, it will do nothing.
|
||||
///
|
||||
/// Should not be used when updating validators.
|
||||
/// Validators should be updated through the `insert_validator` function which contains the correct
|
||||
/// `on_conflict` clauses.
|
||||
pub fn insert_batch_validators(
|
||||
conn: &mut PgConn,
|
||||
all_validators: Vec<WatchValidator>,
|
||||
) -> Result<(), Error> {
|
||||
use self::validators::dsl::*;
|
||||
|
||||
let mut count = 0;
|
||||
|
||||
for chunk in all_validators.chunks(1000) {
|
||||
count += diesel::insert_into(validators)
|
||||
.values(chunk)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
debug!("Validators inserted, count: {count}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
/// SELECT statements
|
||||
///
|
||||
|
||||
/// Selects a single row of the `canonical_slots` table corresponding to a given `slot_query`.
|
||||
pub fn get_canonical_slot(
|
||||
conn: &mut PgConn,
|
||||
slot_query: WatchSlot,
|
||||
) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = canonical_slots
|
||||
.filter(slot.eq(slot_query))
|
||||
.first::<WatchCanonicalSlot>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Canonical slot requested: {slot_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `canonical_slots` table corresponding to a given `root_query`.
|
||||
/// Only returns the non-skipped slot which matches `root`.
|
||||
pub fn get_canonical_slot_by_root(
|
||||
conn: &mut PgConn,
|
||||
root_query: WatchHash,
|
||||
) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = canonical_slots
|
||||
.filter(root.eq(root_query))
|
||||
.filter(skipped.eq(false))
|
||||
.first::<WatchCanonicalSlot>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Canonical root requested: {root_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects `root` from a single row of the `canonical_slots` table corresponding to a given
|
||||
/// `slot_query`.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_root_at_slot(
|
||||
conn: &mut PgConn,
|
||||
slot_query: WatchSlot,
|
||||
) -> Result<Option<WatchHash>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = canonical_slots
|
||||
.select(root)
|
||||
.filter(slot.eq(slot_query))
|
||||
.first::<WatchHash>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Canonical slot requested: {slot_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects `slot` from the row of the `canonical_slots` table corresponding to the minimum value
|
||||
/// of `slot`.
|
||||
pub fn get_lowest_canonical_slot(conn: &mut PgConn) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = canonical_slots
|
||||
.order_by(slot.asc())
|
||||
.limit(1)
|
||||
.first::<WatchCanonicalSlot>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Canonical slot requested: lowest, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects `slot` from the row of the `canonical_slots` table corresponding to the minimum value
|
||||
/// of `slot` and where `skipped == false`.
|
||||
pub fn get_lowest_non_skipped_canonical_slot(
|
||||
conn: &mut PgConn,
|
||||
) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = canonical_slots
|
||||
.filter(skipped.eq(false))
|
||||
.order_by(slot.asc())
|
||||
.limit(1)
|
||||
.first::<WatchCanonicalSlot>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Canonical slot requested: lowest_non_skipped, time taken: {time_taken:?})");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Select 'slot' from the row of the `canonical_slots` table corresponding to the maximum value
|
||||
/// of `slot`.
|
||||
pub fn get_highest_canonical_slot(conn: &mut PgConn) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = canonical_slots
|
||||
.order_by(slot.desc())
|
||||
.limit(1)
|
||||
.first::<WatchCanonicalSlot>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Canonical slot requested: highest, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Select 'slot' from the row of the `canonical_slots` table corresponding to the maximum value
|
||||
/// of `slot` and where `skipped == false`.
|
||||
pub fn get_highest_non_skipped_canonical_slot(
|
||||
conn: &mut PgConn,
|
||||
) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = canonical_slots
|
||||
.filter(skipped.eq(false))
|
||||
.order_by(slot.desc())
|
||||
.limit(1)
|
||||
.first::<WatchCanonicalSlot>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Canonical slot requested: highest_non_skipped, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Select all rows of the `canonical_slots` table where `slot >= `start_slot && slot <=
|
||||
/// `end_slot`.
|
||||
pub fn get_canonical_slots_by_range(
|
||||
conn: &mut PgConn,
|
||||
start_slot: WatchSlot,
|
||||
end_slot: WatchSlot,
|
||||
) -> Result<Option<Vec<WatchCanonicalSlot>>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = canonical_slots
|
||||
.filter(slot.ge(start_slot))
|
||||
.filter(slot.le(end_slot))
|
||||
.load::<WatchCanonicalSlot>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!(
|
||||
"Canonical slots by range requested, start_slot: {}, end_slot: {}, time_taken: {:?}",
|
||||
start_slot.as_u64(),
|
||||
end_slot.as_u64(),
|
||||
time_taken
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects `root` from all rows of the `canonical_slots` table which have `beacon_block == null`
|
||||
/// and `skipped == false`
|
||||
pub fn get_unknown_canonical_blocks(conn: &mut PgConn) -> Result<Vec<WatchHash>, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
|
||||
let result = canonical_slots
|
||||
.select(root)
|
||||
.filter(beacon_block.is_null())
|
||||
.filter(skipped.eq(false))
|
||||
.order_by(slot.desc())
|
||||
.load::<WatchHash>(conn)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects the row from the `beacon_blocks` table where `slot` is minimum.
|
||||
pub fn get_lowest_beacon_block(conn: &mut PgConn) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
use self::beacon_blocks::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = beacon_blocks
|
||||
.order_by(slot.asc())
|
||||
.limit(1)
|
||||
.first::<WatchBeaconBlock>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Beacon block requested: lowest, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects the row from the `beacon_blocks` table where `slot` is maximum.
|
||||
pub fn get_highest_beacon_block(conn: &mut PgConn) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
use self::beacon_blocks::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = beacon_blocks
|
||||
.order_by(slot.desc())
|
||||
.limit(1)
|
||||
.first::<WatchBeaconBlock>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Beacon block requested: highest, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row from the `beacon_blocks` table corresponding to a given `root_query`.
|
||||
pub fn get_beacon_block_by_root(
|
||||
conn: &mut PgConn,
|
||||
root_query: WatchHash,
|
||||
) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
use self::beacon_blocks::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = beacon_blocks
|
||||
.filter(root.eq(root_query))
|
||||
.first::<WatchBeaconBlock>(conn)
|
||||
.optional()?;
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Beacon block requested: {root_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row from the `beacon_blocks` table corresponding to a given `slot_query`.
|
||||
pub fn get_beacon_block_by_slot(
|
||||
conn: &mut PgConn,
|
||||
slot_query: WatchSlot,
|
||||
) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
use self::beacon_blocks::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = beacon_blocks
|
||||
.filter(slot.eq(slot_query))
|
||||
.first::<WatchBeaconBlock>(conn)
|
||||
.optional()?;
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Beacon block requested: {slot_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects the row from the `beacon_blocks` table where `parent_root` equals the given `parent`.
|
||||
/// This fetches the next block in the database.
|
||||
///
|
||||
/// Will return `Ok(None)` if there are no matching blocks (e.g. the tip of the chain).
|
||||
pub fn get_beacon_block_with_parent(
|
||||
conn: &mut PgConn,
|
||||
parent: WatchHash,
|
||||
) -> Result<Option<WatchBeaconBlock>, Error> {
|
||||
use self::beacon_blocks::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = beacon_blocks
|
||||
.filter(parent_root.eq(parent))
|
||||
.first::<WatchBeaconBlock>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Next beacon block requested: {parent}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Select all rows of the `beacon_blocks` table where `slot >= `start_slot && slot <=
|
||||
/// `end_slot`.
|
||||
pub fn get_beacon_blocks_by_range(
|
||||
conn: &mut PgConn,
|
||||
start_slot: WatchSlot,
|
||||
end_slot: WatchSlot,
|
||||
) -> Result<Option<Vec<WatchBeaconBlock>>, Error> {
|
||||
use self::beacon_blocks::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = beacon_blocks
|
||||
.filter(slot.ge(start_slot))
|
||||
.filter(slot.le(end_slot))
|
||||
.load::<WatchBeaconBlock>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Beacon blocks by range requested, start_slot: {start_slot}, end_slot: {end_slot}, time_taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `proposer_info` table corresponding to a given `root_query`.
|
||||
pub fn get_proposer_info_by_root(
|
||||
conn: &mut PgConn,
|
||||
root_query: WatchHash,
|
||||
) -> Result<Option<WatchProposerInfo>, Error> {
|
||||
use self::beacon_blocks::dsl::{beacon_blocks, root};
|
||||
use self::proposer_info::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let join = beacon_blocks.inner_join(proposer_info);
|
||||
|
||||
let result = join
|
||||
.select((slot, proposer_index, graffiti))
|
||||
.filter(root.eq(root_query))
|
||||
.first::<WatchProposerInfo>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Proposer info requested for block: {root_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row of the `proposer_info` table corresponding to a given `slot_query`.
|
||||
pub fn get_proposer_info_by_slot(
|
||||
conn: &mut PgConn,
|
||||
slot_query: WatchSlot,
|
||||
) -> Result<Option<WatchProposerInfo>, Error> {
|
||||
use self::proposer_info::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = proposer_info
|
||||
.filter(slot.eq(slot_query))
|
||||
.first::<WatchProposerInfo>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Proposer info requested for slot: {slot_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects multiple rows of the `proposer_info` table between `start_slot` and `end_slot`.
|
||||
/// Selects a single row of the `proposer_info` table corresponding to a given `slot_query`.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_proposer_info_by_range(
|
||||
conn: &mut PgConn,
|
||||
start_slot: WatchSlot,
|
||||
end_slot: WatchSlot,
|
||||
) -> Result<Option<Vec<WatchProposerInfo>>, Error> {
|
||||
use self::proposer_info::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = proposer_info
|
||||
.filter(slot.ge(start_slot))
|
||||
.filter(slot.le(end_slot))
|
||||
.load::<WatchProposerInfo>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!(
|
||||
"Proposer info requested for range: {start_slot} to {end_slot}, time taken: {time_taken:?}"
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn get_validators_latest_proposer_info(
|
||||
conn: &mut PgConn,
|
||||
indices_query: Vec<i32>,
|
||||
) -> Result<HashMap<i32, WatchProposerInfo>, Error> {
|
||||
use self::proposer_info::dsl::*;
|
||||
|
||||
let proposers = proposer_info
|
||||
.filter(proposer_index.eq_any(indices_query))
|
||||
.load::<WatchProposerInfo>(conn)?;
|
||||
|
||||
let mut result = HashMap::new();
|
||||
for proposer in proposers {
|
||||
result
|
||||
.entry(proposer.proposer_index)
|
||||
.or_insert_with(|| proposer.clone());
|
||||
let entry = result
|
||||
.get_mut(&proposer.proposer_index)
|
||||
.ok_or_else(|| Error::Other("An internal error occured".to_string()))?;
|
||||
if proposer.slot > entry.slot {
|
||||
entry.slot = proposer.slot
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects the max(`slot`) and `proposer_index` of each unique index in the
|
||||
/// `proposer_info` table and returns them formatted as a `HashMap`.
|
||||
/// Only returns rows which have `slot <= target_slot`.
|
||||
///
|
||||
/// Ideally, this would return the full row, but I have not found a way to do that without using
|
||||
/// a much more expensive SQL query.
|
||||
pub fn get_all_validators_latest_proposer_info_at_slot(
|
||||
conn: &mut PgConn,
|
||||
target_slot: WatchSlot,
|
||||
) -> Result<HashMap<WatchSlot, i32>, Error> {
|
||||
use self::proposer_info::dsl::*;
|
||||
|
||||
let latest_proposals: Vec<(i32, Option<WatchSlot>)> = proposer_info
|
||||
.group_by(proposer_index)
|
||||
.select((proposer_index, max(slot)))
|
||||
.filter(slot.le(target_slot))
|
||||
.load::<(i32, Option<WatchSlot>)>(conn)?;
|
||||
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for proposal in latest_proposals {
|
||||
if let Some(latest_slot) = proposal.1 {
|
||||
result.insert(latest_slot, proposal.0);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row from the `validators` table corresponding to a given
|
||||
/// `validator_index_query`.
|
||||
pub fn get_validator_by_index(
|
||||
conn: &mut PgConn,
|
||||
validator_index_query: i32,
|
||||
) -> Result<Option<WatchValidator>, Error> {
|
||||
use self::validators::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = validators
|
||||
.filter(index.eq(validator_index_query))
|
||||
.first::<WatchValidator>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Validator requested: {validator_index_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row from the `validators` table corresponding to a given
|
||||
/// `public_key_query`.
|
||||
pub fn get_validator_by_public_key(
|
||||
conn: &mut PgConn,
|
||||
public_key_query: WatchPK,
|
||||
) -> Result<Option<WatchValidator>, Error> {
|
||||
use self::validators::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = validators
|
||||
.filter(public_key.eq(public_key_query))
|
||||
.first::<WatchValidator>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Validator requested: {public_key_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects all rows from the `validators` table which have an `index` contained in
|
||||
/// the `indices_query`.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_validators_by_indices(
|
||||
conn: &mut PgConn,
|
||||
indices_query: Vec<i32>,
|
||||
) -> Result<Vec<WatchValidator>, Error> {
|
||||
use self::validators::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let query_len = indices_query.len();
|
||||
let result = validators
|
||||
.filter(index.eq_any(indices_query))
|
||||
.load::<WatchValidator>(conn)?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("{query_len} validators requested, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// Selects all rows from the `validators` table.
|
||||
pub fn get_all_validators(conn: &mut PgConn) -> Result<Vec<WatchValidator>, Error> {
|
||||
use self::validators::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = validators.load::<WatchValidator>(conn)?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("All validators requested, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Counts the number of rows in the `validators` table.
|
||||
#[allow(dead_code)]
|
||||
pub fn count_validators(conn: &mut PgConn) -> Result<i64, Error> {
|
||||
use self::validators::dsl::*;
|
||||
|
||||
validators.count().get_result(conn).map_err(Error::Database)
|
||||
}
|
||||
|
||||
/// Counts the number of rows in the `validators` table where
|
||||
/// `activation_epoch <= target_slot.epoch()`.
|
||||
pub fn count_validators_activated_before_slot(
|
||||
conn: &mut PgConn,
|
||||
target_slot: WatchSlot,
|
||||
slots_per_epoch: u64,
|
||||
) -> Result<i64, Error> {
|
||||
use self::validators::dsl::*;
|
||||
|
||||
let target_epoch = target_slot.epoch(slots_per_epoch);
|
||||
|
||||
validators
|
||||
.count()
|
||||
.filter(activation_epoch.le(target_epoch.as_u64() as i32))
|
||||
.get_result(conn)
|
||||
.map_err(Error::Database)
|
||||
}
|
||||
|
||||
///
|
||||
/// DELETE statements.
|
||||
///
|
||||
|
||||
/// Deletes all rows of the `canonical_slots` table which have `slot` greater than `slot_query`.
|
||||
///
|
||||
/// Due to the ON DELETE CASCADE clause present in the database migration SQL, deleting rows from
|
||||
/// `canonical_slots` will delete all corresponding rows in `beacon_blocks, `block_rewards`,
|
||||
/// `block_packing` and `proposer_info`.
|
||||
pub fn delete_canonical_slots_above(
|
||||
conn: &mut PgConn,
|
||||
slot_query: WatchSlot,
|
||||
) -> Result<usize, Error> {
|
||||
use self::canonical_slots::dsl::*;
|
||||
|
||||
let result = diesel::delete(canonical_slots)
|
||||
.filter(slot.gt(slot_query))
|
||||
.execute(conn)?;
|
||||
|
||||
debug!("Deleted canonical slots above {slot_query}: {result} rows deleted");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Deletes all rows of the `suboptimal_attestations` table which have `epoch_start_slot` greater
|
||||
/// than `epoch_start_slot_query`.
|
||||
pub fn delete_suboptimal_attestations_above(
|
||||
conn: &mut PgConn,
|
||||
epoch_start_slot_query: WatchSlot,
|
||||
) -> Result<usize, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
|
||||
let result = diesel::delete(suboptimal_attestations)
|
||||
.filter(epoch_start_slot.gt(epoch_start_slot_query))
|
||||
.execute(conn)?;
|
||||
|
||||
debug!("Deleted attestations above: {epoch_start_slot_query}, rows deleted: {result}");
|
||||
Ok(result)
|
||||
}
|
67
watch/src/database/models.rs
Normal file
67
watch/src/database/models.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use crate::database::{
|
||||
schema::{beacon_blocks, canonical_slots, proposer_info, validators},
|
||||
watch_types::{WatchHash, WatchPK, WatchSlot},
|
||||
};
|
||||
use diesel::{Insertable, Queryable};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
pub type WatchEpoch = i32;
|
||||
|
||||
#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = canonical_slots)]
|
||||
pub struct WatchCanonicalSlot {
|
||||
pub slot: WatchSlot,
|
||||
pub root: WatchHash,
|
||||
pub skipped: bool,
|
||||
pub beacon_block: Option<WatchHash>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = beacon_blocks)]
|
||||
pub struct WatchBeaconBlock {
|
||||
pub slot: WatchSlot,
|
||||
pub root: WatchHash,
|
||||
pub parent_root: WatchHash,
|
||||
pub attestation_count: i32,
|
||||
pub transaction_count: Option<i32>,
|
||||
pub withdrawal_count: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = validators)]
|
||||
pub struct WatchValidator {
|
||||
pub index: i32,
|
||||
pub public_key: WatchPK,
|
||||
pub status: String,
|
||||
pub activation_epoch: Option<WatchEpoch>,
|
||||
pub exit_epoch: Option<WatchEpoch>,
|
||||
}
|
||||
|
||||
// Implement a minimal version of `Hash` and `Eq` so that we know if a validator status has changed.
|
||||
impl Hash for WatchValidator {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.index.hash(state);
|
||||
self.status.hash(state);
|
||||
self.activation_epoch.hash(state);
|
||||
self.exit_epoch.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for WatchValidator {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index
|
||||
&& self.status == other.status
|
||||
&& self.activation_epoch == other.activation_epoch
|
||||
&& self.exit_epoch == other.exit_epoch
|
||||
}
|
||||
}
|
||||
impl Eq for WatchValidator {}
|
||||
|
||||
#[derive(Clone, Debug, Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = proposer_info)]
|
||||
pub struct WatchProposerInfo {
|
||||
pub slot: WatchSlot,
|
||||
pub proposer_index: i32,
|
||||
pub graffiti: String,
|
||||
}
|
102
watch/src/database/schema.rs
Normal file
102
watch/src/database/schema.rs
Normal file
@ -0,0 +1,102 @@
|
||||
// @generated automatically by Diesel CLI.
|
||||
|
||||
diesel::table! {
|
||||
active_config (id) {
|
||||
id -> Int4,
|
||||
config_name -> Text,
|
||||
slots_per_epoch -> Int4,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
beacon_blocks (slot) {
|
||||
slot -> Int4,
|
||||
root -> Bytea,
|
||||
parent_root -> Bytea,
|
||||
attestation_count -> Int4,
|
||||
transaction_count -> Nullable<Int4>,
|
||||
withdrawal_count -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
block_packing (slot) {
|
||||
slot -> Int4,
|
||||
available -> Int4,
|
||||
included -> Int4,
|
||||
prior_skip_slots -> Int4,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
block_rewards (slot) {
|
||||
slot -> Int4,
|
||||
total -> Int4,
|
||||
attestation_reward -> Int4,
|
||||
sync_committee_reward -> Int4,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
blockprint (slot) {
|
||||
slot -> Int4,
|
||||
best_guess -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
canonical_slots (slot) {
|
||||
slot -> Int4,
|
||||
root -> Bytea,
|
||||
skipped -> Bool,
|
||||
beacon_block -> Nullable<Bytea>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
proposer_info (slot) {
|
||||
slot -> Int4,
|
||||
proposer_index -> Int4,
|
||||
graffiti -> Text,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
suboptimal_attestations (epoch_start_slot, index) {
|
||||
epoch_start_slot -> Int4,
|
||||
index -> Int4,
|
||||
source -> Bool,
|
||||
head -> Bool,
|
||||
target -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
validators (index) {
|
||||
index -> Int4,
|
||||
public_key -> Bytea,
|
||||
status -> Text,
|
||||
activation_epoch -> Nullable<Int4>,
|
||||
exit_epoch -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(block_packing -> beacon_blocks (slot));
|
||||
diesel::joinable!(block_rewards -> beacon_blocks (slot));
|
||||
diesel::joinable!(blockprint -> beacon_blocks (slot));
|
||||
diesel::joinable!(proposer_info -> beacon_blocks (slot));
|
||||
diesel::joinable!(proposer_info -> validators (proposer_index));
|
||||
diesel::joinable!(suboptimal_attestations -> canonical_slots (epoch_start_slot));
|
||||
diesel::joinable!(suboptimal_attestations -> validators (index));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
active_config,
|
||||
beacon_blocks,
|
||||
block_packing,
|
||||
block_rewards,
|
||||
blockprint,
|
||||
canonical_slots,
|
||||
proposer_info,
|
||||
suboptimal_attestations,
|
||||
validators,
|
||||
);
|
29
watch/src/database/utils.rs
Normal file
29
watch/src/database/utils.rs
Normal file
@ -0,0 +1,29 @@
|
||||
#![allow(dead_code)]
|
||||
use crate::database::config::Config;
|
||||
use diesel::pg::PgConnection;
|
||||
use diesel::prelude::*;
|
||||
use diesel_migrations::{FileBasedMigrations, MigrationHarness};
|
||||
|
||||
/// Sets `config.dbname` to `config.default_dbname` and returns `(new_config, old_dbname)`.
|
||||
///
|
||||
/// This is useful for creating or dropping databases, since these actions must be done by
|
||||
/// logging into another database.
|
||||
pub fn get_config_using_default_db(config: &Config) -> (Config, String) {
|
||||
let mut config = config.clone();
|
||||
let new_dbname = std::mem::replace(&mut config.dbname, config.default_dbname.clone());
|
||||
(config, new_dbname)
|
||||
}
|
||||
|
||||
/// Runs the set of migrations as detected in the local directory.
|
||||
/// Equivalent to `diesel migration run`.
|
||||
///
|
||||
/// Contains `unwrap`s so is only suitable for test code.
|
||||
/// TODO(mac) refactor to return Result<PgConnection, Error>
|
||||
pub fn run_migrations(config: &Config) -> PgConnection {
|
||||
let database_url = config.clone().build_database_url();
|
||||
let mut conn = PgConnection::establish(&database_url).unwrap();
|
||||
let migrations = FileBasedMigrations::find_migrations_directory().unwrap();
|
||||
conn.run_pending_migrations(migrations).unwrap();
|
||||
conn.begin_test_transaction().unwrap();
|
||||
conn
|
||||
}
|
119
watch/src/database/watch_types.rs
Normal file
119
watch/src/database/watch_types.rs
Normal file
@ -0,0 +1,119 @@
|
||||
use crate::database::error::Error;
|
||||
use diesel::{
|
||||
sql_types::{Binary, Integer},
|
||||
AsExpression, FromSqlRow,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use types::{Epoch, Hash256, PublicKeyBytes, Slot};
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
AsExpression,
|
||||
FromSqlRow,
|
||||
Deserialize,
|
||||
Serialize,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
)]
|
||||
#[diesel(sql_type = Integer)]
|
||||
pub struct WatchSlot(Slot);
|
||||
|
||||
impl fmt::Display for WatchSlot {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl WatchSlot {
|
||||
pub fn new(slot: u64) -> Self {
|
||||
Self(Slot::new(slot))
|
||||
}
|
||||
|
||||
pub fn from_slot(slot: Slot) -> Self {
|
||||
Self(slot)
|
||||
}
|
||||
|
||||
pub fn as_slot(self) -> Slot {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn as_u64(self) -> u64 {
|
||||
self.0.as_u64()
|
||||
}
|
||||
|
||||
pub fn epoch(self, slots_per_epoch: u64) -> Epoch {
|
||||
self.as_slot().epoch(slots_per_epoch)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, AsExpression, FromSqlRow, Deserialize, Serialize)]
|
||||
#[diesel(sql_type = Binary)]
|
||||
pub struct WatchHash(Hash256);
|
||||
|
||||
impl fmt::Display for WatchHash {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl WatchHash {
|
||||
pub fn as_hash(&self) -> Hash256 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn from_hash(hash: Hash256) -> Self {
|
||||
WatchHash(hash)
|
||||
}
|
||||
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_bytes()
|
||||
}
|
||||
|
||||
pub fn from_bytes(src: &[u8]) -> Result<WatchHash, Error> {
|
||||
if src.len() == 32 {
|
||||
Ok(WatchHash(Hash256::from_slice(src)))
|
||||
} else {
|
||||
Err(Error::InvalidRoot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, AsExpression, FromSqlRow, Serialize, Deserialize)]
|
||||
#[diesel(sql_type = Binary)]
|
||||
pub struct WatchPK(PublicKeyBytes);
|
||||
|
||||
impl fmt::Display for WatchPK {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl WatchPK {
|
||||
pub fn as_bytes(&self) -> &[u8] {
|
||||
self.0.as_serialized()
|
||||
}
|
||||
|
||||
pub fn from_bytes(src: &[u8]) -> Result<WatchPK, Error> {
|
||||
Ok(WatchPK(PublicKeyBytes::deserialize(src)?))
|
||||
}
|
||||
|
||||
pub fn from_pubkey(key: PublicKeyBytes) -> Self {
|
||||
WatchPK(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for WatchPK {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Ok(WatchPK(
|
||||
PublicKeyBytes::from_str(s).map_err(|e| format!("Cannot be parsed: {}", e))?,
|
||||
))
|
||||
}
|
||||
}
|
12
watch/src/lib.rs
Normal file
12
watch/src/lib.rs
Normal file
@ -0,0 +1,12 @@
|
||||
#![cfg(unix)]
|
||||
pub mod block_packing;
|
||||
pub mod block_rewards;
|
||||
pub mod blockprint;
|
||||
pub mod cli;
|
||||
pub mod client;
|
||||
pub mod config;
|
||||
pub mod database;
|
||||
pub mod logger;
|
||||
pub mod server;
|
||||
pub mod suboptimal_attestations;
|
||||
pub mod updater;
|
24
watch/src/logger.rs
Normal file
24
watch/src/logger.rs
Normal file
@ -0,0 +1,24 @@
|
||||
use env_logger::Builder;
|
||||
use log::{info, LevelFilter};
|
||||
use std::process;
|
||||
|
||||
pub fn init_logger(log_level: &str) {
|
||||
let log_level = match log_level.to_lowercase().as_str() {
|
||||
"trace" => LevelFilter::Trace,
|
||||
"debug" => LevelFilter::Debug,
|
||||
"info" => LevelFilter::Info,
|
||||
"warn" => LevelFilter::Warn,
|
||||
"error" => LevelFilter::Error,
|
||||
_ => {
|
||||
eprintln!("Unsupported log level");
|
||||
process::exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
let mut builder = Builder::new();
|
||||
builder.filter(Some("watch"), log_level);
|
||||
|
||||
builder.init();
|
||||
|
||||
info!("Logger initialized with log-level: {log_level}");
|
||||
}
|
41
watch/src/main.rs
Normal file
41
watch/src/main.rs
Normal file
@ -0,0 +1,41 @@
|
||||
#[cfg(unix)]
|
||||
use std::process;
|
||||
|
||||
#[cfg(unix)]
|
||||
mod block_packing;
|
||||
#[cfg(unix)]
|
||||
mod block_rewards;
|
||||
#[cfg(unix)]
|
||||
mod blockprint;
|
||||
#[cfg(unix)]
|
||||
mod cli;
|
||||
#[cfg(unix)]
|
||||
mod config;
|
||||
#[cfg(unix)]
|
||||
mod database;
|
||||
#[cfg(unix)]
|
||||
mod logger;
|
||||
#[cfg(unix)]
|
||||
mod server;
|
||||
#[cfg(unix)]
|
||||
mod suboptimal_attestations;
|
||||
#[cfg(unix)]
|
||||
mod updater;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
match cli::run().await {
|
||||
Ok(()) => process::exit(0),
|
||||
Err(e) => {
|
||||
eprintln!("Command failed with: {}", e);
|
||||
drop(e);
|
||||
process::exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn main() {
|
||||
eprintln!("Windows is not supported. Exiting.");
|
||||
}
|
28
watch/src/server/config.rs
Normal file
28
watch/src/server/config.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub const LISTEN_ADDR: &str = "127.0.0.1";
|
||||
|
||||
pub const fn listen_port() -> u16 {
|
||||
5059
|
||||
}
|
||||
fn listen_addr() -> IpAddr {
|
||||
LISTEN_ADDR.parse().expect("Server address is not valid")
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default = "listen_addr")]
|
||||
pub listen_addr: IpAddr,
|
||||
#[serde(default = "listen_port")]
|
||||
pub listen_port: u16,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
listen_addr: listen_addr(),
|
||||
listen_port: listen_port(),
|
||||
}
|
||||
}
|
||||
}
|
50
watch/src/server/error.rs
Normal file
50
watch/src/server/error.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use crate::database::Error as DbError;
|
||||
use axum::Error as AxumError;
|
||||
use axum::{http::StatusCode, response::IntoResponse, Json};
|
||||
use hyper::Error as HyperError;
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Axum(AxumError),
|
||||
Hyper(HyperError),
|
||||
Database(DbError),
|
||||
BadRequest,
|
||||
NotFound,
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
let (status, error_message) = match self {
|
||||
Self::BadRequest => (StatusCode::BAD_REQUEST, "Bad Request"),
|
||||
Self::NotFound => (StatusCode::NOT_FOUND, "Not Found"),
|
||||
_ => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error"),
|
||||
};
|
||||
(status, Json(json!({ "error": error_message }))).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HyperError> for Error {
|
||||
fn from(e: HyperError) -> Self {
|
||||
Error::Hyper(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AxumError> for Error {
|
||||
fn from(e: AxumError) -> Self {
|
||||
Error::Axum(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DbError> for Error {
|
||||
fn from(e: DbError) -> Self {
|
||||
Error::Database(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(e: String) -> Self {
|
||||
Error::Other(e)
|
||||
}
|
||||
}
|
266
watch/src/server/handler.rs
Normal file
266
watch/src/server/handler.rs
Normal file
@ -0,0 +1,266 @@
|
||||
use crate::database::{
|
||||
self, Error as DbError, PgPool, WatchBeaconBlock, WatchCanonicalSlot, WatchHash, WatchPK,
|
||||
WatchProposerInfo, WatchSlot, WatchValidator,
|
||||
};
|
||||
use crate::server::Error;
|
||||
use axum::{
|
||||
extract::{Path, Query},
|
||||
Extension, Json,
|
||||
};
|
||||
use eth2::types::BlockId;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
|
||||
pub async fn get_slot(
|
||||
Path(slot): Path<u64>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchCanonicalSlot>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
Ok(Json(database::get_canonical_slot(
|
||||
&mut conn,
|
||||
WatchSlot::new(slot),
|
||||
)?))
|
||||
}
|
||||
|
||||
pub async fn get_slot_lowest(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchCanonicalSlot>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
Ok(Json(database::get_lowest_canonical_slot(&mut conn)?))
|
||||
}
|
||||
|
||||
pub async fn get_slot_highest(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchCanonicalSlot>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
Ok(Json(database::get_highest_canonical_slot(&mut conn)?))
|
||||
}
|
||||
|
||||
pub async fn get_slots_by_range(
|
||||
Query(query): Query<HashMap<String, u64>>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<Vec<WatchCanonicalSlot>>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
if let Some(start_slot) = query.get("start_slot") {
|
||||
if let Some(end_slot) = query.get("end_slot") {
|
||||
if start_slot > end_slot {
|
||||
Err(Error::BadRequest)
|
||||
} else {
|
||||
Ok(Json(database::get_canonical_slots_by_range(
|
||||
&mut conn,
|
||||
WatchSlot::new(*start_slot),
|
||||
WatchSlot::new(*end_slot),
|
||||
)?))
|
||||
}
|
||||
} else {
|
||||
Err(Error::BadRequest)
|
||||
}
|
||||
} else {
|
||||
Err(Error::BadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_block(
|
||||
Path(block_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchBeaconBlock>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
let block_id: BlockId = BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)?;
|
||||
match block_id {
|
||||
BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(slot),
|
||||
)?)),
|
||||
BlockId::Root(root) => Ok(Json(database::get_beacon_block_by_root(
|
||||
&mut conn,
|
||||
WatchHash::from_hash(root),
|
||||
)?)),
|
||||
_ => Err(Error::BadRequest),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_block_lowest(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchBeaconBlock>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
Ok(Json(database::get_lowest_beacon_block(&mut conn)?))
|
||||
}
|
||||
|
||||
pub async fn get_block_highest(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchBeaconBlock>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
Ok(Json(database::get_highest_beacon_block(&mut conn)?))
|
||||
}
|
||||
|
||||
pub async fn get_block_previous(
|
||||
Path(block_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchBeaconBlock>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? {
|
||||
BlockId::Root(root) => {
|
||||
if let Some(block) =
|
||||
database::get_beacon_block_by_root(&mut conn, WatchHash::from_hash(root))?
|
||||
.map(|block| block.parent_root)
|
||||
{
|
||||
Ok(Json(database::get_beacon_block_by_root(&mut conn, block)?))
|
||||
} else {
|
||||
Err(Error::NotFound)
|
||||
}
|
||||
}
|
||||
BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot(
|
||||
&mut conn,
|
||||
WatchSlot::new(slot.as_u64().checked_sub(1_u64).ok_or(Error::NotFound)?),
|
||||
)?)),
|
||||
_ => Err(Error::BadRequest),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_block_next(
|
||||
Path(block_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchBeaconBlock>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? {
|
||||
BlockId::Root(root) => Ok(Json(database::get_beacon_block_with_parent(
|
||||
&mut conn,
|
||||
WatchHash::from_hash(root),
|
||||
)?)),
|
||||
BlockId::Slot(slot) => Ok(Json(database::get_beacon_block_by_slot(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(slot + 1_u64),
|
||||
)?)),
|
||||
_ => Err(Error::BadRequest),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_blocks_by_range(
|
||||
Query(query): Query<HashMap<String, u64>>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<Vec<WatchBeaconBlock>>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
if let Some(start_slot) = query.get("start_slot") {
|
||||
if let Some(end_slot) = query.get("end_slot") {
|
||||
if start_slot > end_slot {
|
||||
Err(Error::BadRequest)
|
||||
} else {
|
||||
Ok(Json(database::get_beacon_blocks_by_range(
|
||||
&mut conn,
|
||||
WatchSlot::new(*start_slot),
|
||||
WatchSlot::new(*end_slot),
|
||||
)?))
|
||||
}
|
||||
} else {
|
||||
Err(Error::BadRequest)
|
||||
}
|
||||
} else {
|
||||
Err(Error::BadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_block_proposer(
|
||||
Path(block_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchProposerInfo>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
match BlockId::from_str(&block_query).map_err(|_| Error::BadRequest)? {
|
||||
BlockId::Root(root) => Ok(Json(database::get_proposer_info_by_root(
|
||||
&mut conn,
|
||||
WatchHash::from_hash(root),
|
||||
)?)),
|
||||
BlockId::Slot(slot) => Ok(Json(database::get_proposer_info_by_slot(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(slot),
|
||||
)?)),
|
||||
_ => Err(Error::BadRequest),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_validator(
|
||||
Path(validator_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Option<WatchValidator>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
if validator_query.starts_with("0x") {
|
||||
let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?;
|
||||
Ok(Json(database::get_validator_by_public_key(
|
||||
&mut conn, pubkey,
|
||||
)?))
|
||||
} else {
|
||||
let index = i32::from_str(&validator_query).map_err(|_| Error::BadRequest)?;
|
||||
Ok(Json(database::get_validator_by_index(&mut conn, index)?))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_all_validators(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<Vec<WatchValidator>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
Ok(Json(database::get_all_validators(&mut conn)?))
|
||||
}
|
||||
|
||||
pub async fn get_validator_latest_proposal(
|
||||
Path(validator_query): Path<String>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
) -> Result<Json<HashMap<i32, WatchProposerInfo>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
if validator_query.starts_with("0x") {
|
||||
let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?;
|
||||
let validator =
|
||||
database::get_validator_by_public_key(&mut conn, pubkey)?.ok_or(Error::NotFound)?;
|
||||
Ok(Json(database::get_validators_latest_proposer_info(
|
||||
&mut conn,
|
||||
vec![validator.index],
|
||||
)?))
|
||||
} else {
|
||||
let index = i32::from_str(&validator_query).map_err(|_| Error::BadRequest)?;
|
||||
Ok(Json(database::get_validators_latest_proposer_info(
|
||||
&mut conn,
|
||||
vec![index],
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_client_breakdown(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<HashMap<String, usize>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
|
||||
if let Some(target_slot) = database::get_highest_canonical_slot(&mut conn)? {
|
||||
Ok(Json(database::get_validators_clients_at_slot(
|
||||
&mut conn,
|
||||
target_slot.slot,
|
||||
slots_per_epoch,
|
||||
)?))
|
||||
} else {
|
||||
Err(Error::Database(DbError::Other(
|
||||
"No slots found in database.".to_string(),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_client_breakdown_percentages(
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<HashMap<String, f64>>, Error> {
|
||||
let mut conn = database::get_connection(&pool).map_err(Error::Database)?;
|
||||
|
||||
let mut result = HashMap::new();
|
||||
if let Some(target_slot) = database::get_highest_canonical_slot(&mut conn)? {
|
||||
let total = database::count_validators_activated_before_slot(
|
||||
&mut conn,
|
||||
target_slot.slot,
|
||||
slots_per_epoch,
|
||||
)?;
|
||||
let clients =
|
||||
database::get_validators_clients_at_slot(&mut conn, target_slot.slot, slots_per_epoch)?;
|
||||
for (client, number) in clients.iter() {
|
||||
let percentage: f64 = *number as f64 / total as f64 * 100.0;
|
||||
result.insert(client.to_string(), percentage);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
134
watch/src/server/mod.rs
Normal file
134
watch/src/server/mod.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use crate::block_packing::block_packing_routes;
|
||||
use crate::block_rewards::block_rewards_routes;
|
||||
use crate::blockprint::blockprint_routes;
|
||||
use crate::config::Config as FullConfig;
|
||||
use crate::database::{self, PgPool};
|
||||
use crate::suboptimal_attestations::{attestation_routes, blockprint_attestation_routes};
|
||||
use axum::{
|
||||
handler::Handler,
|
||||
http::{StatusCode, Uri},
|
||||
routing::get,
|
||||
Extension, Json, Router,
|
||||
};
|
||||
use eth2::types::ErrorMessage;
|
||||
use log::info;
|
||||
use std::future::Future;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
|
||||
mod config;
|
||||
mod error;
|
||||
mod handler;
|
||||
|
||||
pub async fn serve(config: FullConfig, shutdown: oneshot::Receiver<()>) -> Result<(), Error> {
|
||||
let db = database::build_connection_pool(&config.database)?;
|
||||
let (_, slots_per_epoch) = database::get_active_config(&mut database::get_connection(&db)?)?
|
||||
.ok_or_else(|| {
|
||||
Error::Other(
|
||||
"Database not found. Please run the updater prior to starting the server"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let server = start_server(&config, slots_per_epoch as u64, db, async {
|
||||
let _ = shutdown.await;
|
||||
})?;
|
||||
|
||||
server.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Creates a server that will serve requests using information from `config`.
|
||||
///
|
||||
/// The server will create its own connection pool to serve connections to the database.
|
||||
/// This is separate to the connection pool that is used for the `updater`.
|
||||
///
|
||||
/// The server will shut down gracefully when the `shutdown` future resolves.
|
||||
///
|
||||
/// ## Returns
|
||||
///
|
||||
/// This function will bind the server to the address specified in the config and then return a
|
||||
/// Future representing the actual server that will need to be awaited.
|
||||
///
|
||||
/// ## Errors
|
||||
///
|
||||
/// Returns an error if the server is unable to bind or there is another error during
|
||||
/// configuration.
|
||||
pub fn start_server(
|
||||
config: &FullConfig,
|
||||
slots_per_epoch: u64,
|
||||
pool: PgPool,
|
||||
shutdown: impl Future<Output = ()> + Send + Sync + 'static,
|
||||
) -> Result<impl Future<Output = Result<(), hyper::Error>> + 'static, Error> {
|
||||
let mut routes = Router::new()
|
||||
.route("/v1/slots", get(handler::get_slots_by_range))
|
||||
.route("/v1/slots/:slot", get(handler::get_slot))
|
||||
.route("/v1/slots/lowest", get(handler::get_slot_lowest))
|
||||
.route("/v1/slots/highest", get(handler::get_slot_highest))
|
||||
.route("/v1/slots/:slot/block", get(handler::get_block))
|
||||
.route("/v1/blocks", get(handler::get_blocks_by_range))
|
||||
.route("/v1/blocks/:block", get(handler::get_block))
|
||||
.route("/v1/blocks/lowest", get(handler::get_block_lowest))
|
||||
.route("/v1/blocks/highest", get(handler::get_block_highest))
|
||||
.route(
|
||||
"/v1/blocks/:block/previous",
|
||||
get(handler::get_block_previous),
|
||||
)
|
||||
.route("/v1/blocks/:block/next", get(handler::get_block_next))
|
||||
.route(
|
||||
"/v1/blocks/:block/proposer",
|
||||
get(handler::get_block_proposer),
|
||||
)
|
||||
.route("/v1/validators/:validator", get(handler::get_validator))
|
||||
.route("/v1/validators/all", get(handler::get_all_validators))
|
||||
.route(
|
||||
"/v1/validators/:validator/latest_proposal",
|
||||
get(handler::get_validator_latest_proposal),
|
||||
)
|
||||
.route("/v1/clients", get(handler::get_client_breakdown))
|
||||
.route(
|
||||
"/v1/clients/percentages",
|
||||
get(handler::get_client_breakdown_percentages),
|
||||
)
|
||||
.merge(attestation_routes())
|
||||
.merge(blockprint_routes())
|
||||
.merge(block_packing_routes())
|
||||
.merge(block_rewards_routes());
|
||||
|
||||
if config.blockprint.enabled && config.updater.attestations {
|
||||
routes = routes.merge(blockprint_attestation_routes())
|
||||
}
|
||||
|
||||
let app = routes
|
||||
.fallback(route_not_found.into_service())
|
||||
.layer(Extension(pool))
|
||||
.layer(Extension(slots_per_epoch));
|
||||
|
||||
let addr = SocketAddr::new(config.server.listen_addr, config.server.listen_port);
|
||||
|
||||
let server = axum::Server::try_bind(&addr)?.serve(app.into_make_service());
|
||||
|
||||
let server = server.with_graceful_shutdown(async {
|
||||
shutdown.await;
|
||||
});
|
||||
|
||||
info!("HTTP server listening on {}", addr);
|
||||
|
||||
Ok(server)
|
||||
}
|
||||
|
||||
// The default route indicating that no available routes matched the request.
|
||||
async fn route_not_found(uri: Uri) -> (StatusCode, Json<ErrorMessage>) {
|
||||
(
|
||||
StatusCode::METHOD_NOT_ALLOWED,
|
||||
Json(ErrorMessage {
|
||||
code: StatusCode::METHOD_NOT_ALLOWED.as_u16(),
|
||||
message: format!("No route for {uri}"),
|
||||
stacktraces: vec![],
|
||||
}),
|
||||
)
|
||||
}
|
224
watch/src/suboptimal_attestations/database.rs
Normal file
224
watch/src/suboptimal_attestations/database.rs
Normal file
@ -0,0 +1,224 @@
|
||||
use crate::database::{
|
||||
schema::{suboptimal_attestations, validators},
|
||||
watch_types::{WatchPK, WatchSlot},
|
||||
Error, PgConn, MAX_SIZE_BATCH_INSERT,
|
||||
};
|
||||
|
||||
use diesel::prelude::*;
|
||||
use diesel::{Insertable, Queryable};
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Instant;
|
||||
|
||||
use types::Epoch;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct WatchAttestation {
|
||||
pub index: i32,
|
||||
pub epoch: Epoch,
|
||||
pub source: bool,
|
||||
pub head: bool,
|
||||
pub target: bool,
|
||||
}
|
||||
|
||||
impl WatchAttestation {
|
||||
pub fn optimal(index: i32, epoch: Epoch) -> WatchAttestation {
|
||||
WatchAttestation {
|
||||
index,
|
||||
epoch,
|
||||
source: true,
|
||||
head: true,
|
||||
target: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Queryable, Insertable, Serialize, Deserialize)]
|
||||
#[diesel(table_name = suboptimal_attestations)]
|
||||
pub struct WatchSuboptimalAttestation {
|
||||
pub epoch_start_slot: WatchSlot,
|
||||
pub index: i32,
|
||||
pub source: bool,
|
||||
pub head: bool,
|
||||
pub target: bool,
|
||||
}
|
||||
|
||||
impl WatchSuboptimalAttestation {
|
||||
pub fn to_attestation(&self, slots_per_epoch: u64) -> WatchAttestation {
|
||||
WatchAttestation {
|
||||
index: self.index,
|
||||
epoch: self.epoch_start_slot.epoch(slots_per_epoch),
|
||||
source: self.source,
|
||||
head: self.head,
|
||||
target: self.target,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a batch of values into the `suboptimal_attestations` table
|
||||
///
|
||||
/// Since attestations technically occur per-slot but we only store them per-epoch (via its
|
||||
/// `start_slot`) so if any slot in the epoch changes, we need to resync the whole epoch as a
|
||||
/// 'suboptimal' attestation could now be 'optimal'.
|
||||
///
|
||||
/// This is handled in the update code, where in the case of a re-org, the affected epoch is
|
||||
/// deleted completely.
|
||||
///
|
||||
/// On a conflict, it will do nothing.
|
||||
pub fn insert_batch_suboptimal_attestations(
|
||||
conn: &mut PgConn,
|
||||
attestations: Vec<WatchSuboptimalAttestation>,
|
||||
) -> Result<(), Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
|
||||
let mut count = 0;
|
||||
let timer = Instant::now();
|
||||
|
||||
for chunk in attestations.chunks(MAX_SIZE_BATCH_INSERT) {
|
||||
count += diesel::insert_into(suboptimal_attestations)
|
||||
.values(chunk)
|
||||
.on_conflict_do_nothing()
|
||||
.execute(conn)?;
|
||||
}
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Attestations inserted, count: {count}, time taken: {time_taken:?}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Selects the row from the `suboptimal_attestations` table where `epoch_start_slot` is minimum.
|
||||
pub fn get_lowest_attestation(
|
||||
conn: &mut PgConn,
|
||||
) -> Result<Option<WatchSuboptimalAttestation>, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
|
||||
Ok(suboptimal_attestations
|
||||
.order_by(epoch_start_slot.asc())
|
||||
.limit(1)
|
||||
.first::<WatchSuboptimalAttestation>(conn)
|
||||
.optional()?)
|
||||
}
|
||||
|
||||
/// Selects the row from the `suboptimal_attestations` table where `epoch_start_slot` is maximum.
|
||||
pub fn get_highest_attestation(
|
||||
conn: &mut PgConn,
|
||||
) -> Result<Option<WatchSuboptimalAttestation>, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
|
||||
Ok(suboptimal_attestations
|
||||
.order_by(epoch_start_slot.desc())
|
||||
.limit(1)
|
||||
.first::<WatchSuboptimalAttestation>(conn)
|
||||
.optional()?)
|
||||
}
|
||||
|
||||
/// Selects a single row from the `suboptimal_attestations` table corresponding to a given
|
||||
/// `index_query` and `epoch_query`.
|
||||
pub fn get_attestation_by_index(
|
||||
conn: &mut PgConn,
|
||||
index_query: i32,
|
||||
epoch_query: Epoch,
|
||||
slots_per_epoch: u64,
|
||||
) -> Result<Option<WatchSuboptimalAttestation>, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
let timer = Instant::now();
|
||||
|
||||
let result = suboptimal_attestations
|
||||
.filter(epoch_start_slot.eq(WatchSlot::from_slot(
|
||||
epoch_query.start_slot(slots_per_epoch),
|
||||
)))
|
||||
.filter(index.eq(index_query))
|
||||
.first::<WatchSuboptimalAttestation>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Attestation requested for validator: {index_query}, epoch: {epoch_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects a single row from the `suboptimal_attestations` table corresponding
|
||||
/// to a given `pubkey_query` and `epoch_query`.
|
||||
#[allow(dead_code)]
|
||||
pub fn get_attestation_by_pubkey(
|
||||
conn: &mut PgConn,
|
||||
pubkey_query: WatchPK,
|
||||
epoch_query: Epoch,
|
||||
slots_per_epoch: u64,
|
||||
) -> Result<Option<WatchSuboptimalAttestation>, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
use self::validators::dsl::{public_key, validators};
|
||||
let timer = Instant::now();
|
||||
|
||||
let join = validators.inner_join(suboptimal_attestations);
|
||||
|
||||
let result = join
|
||||
.select((epoch_start_slot, index, source, head, target))
|
||||
.filter(epoch_start_slot.eq(WatchSlot::from_slot(
|
||||
epoch_query.start_slot(slots_per_epoch),
|
||||
)))
|
||||
.filter(public_key.eq(pubkey_query))
|
||||
.first::<WatchSuboptimalAttestation>(conn)
|
||||
.optional()?;
|
||||
|
||||
let time_taken = timer.elapsed();
|
||||
debug!("Attestation requested for validator: {pubkey_query}, epoch: {epoch_query}, time taken: {time_taken:?}");
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Selects `index` for all validators in the suboptimal_attestations table
|
||||
/// that have `source == false` for the corresponding `epoch_start_slot_query`.
|
||||
pub fn get_validators_missed_source(
|
||||
conn: &mut PgConn,
|
||||
epoch_start_slot_query: WatchSlot,
|
||||
) -> Result<Vec<i32>, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
|
||||
Ok(suboptimal_attestations
|
||||
.select(index)
|
||||
.filter(epoch_start_slot.eq(epoch_start_slot_query))
|
||||
.filter(source.eq(false))
|
||||
.load::<i32>(conn)?)
|
||||
}
|
||||
|
||||
/// Selects `index` for all validators in the suboptimal_attestations table
|
||||
/// that have `head == false` for the corresponding `epoch_start_slot_query`.
|
||||
pub fn get_validators_missed_head(
|
||||
conn: &mut PgConn,
|
||||
epoch_start_slot_query: WatchSlot,
|
||||
) -> Result<Vec<i32>, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
|
||||
Ok(suboptimal_attestations
|
||||
.select(index)
|
||||
.filter(epoch_start_slot.eq(epoch_start_slot_query))
|
||||
.filter(head.eq(false))
|
||||
.load::<i32>(conn)?)
|
||||
}
|
||||
|
||||
/// Selects `index` for all validators in the suboptimal_attestations table
|
||||
/// that have `target == false` for the corresponding `epoch_start_slot_query`.
|
||||
pub fn get_validators_missed_target(
|
||||
conn: &mut PgConn,
|
||||
epoch_start_slot_query: WatchSlot,
|
||||
) -> Result<Vec<i32>, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
|
||||
Ok(suboptimal_attestations
|
||||
.select(index)
|
||||
.filter(epoch_start_slot.eq(epoch_start_slot_query))
|
||||
.filter(target.eq(false))
|
||||
.load::<i32>(conn)?)
|
||||
}
|
||||
|
||||
/// Selects all rows from the `suboptimal_attestations` table for the given
|
||||
/// `epoch_start_slot_query`.
|
||||
pub fn get_all_suboptimal_attestations_for_epoch(
|
||||
conn: &mut PgConn,
|
||||
epoch_start_slot_query: WatchSlot,
|
||||
) -> Result<Vec<WatchSuboptimalAttestation>, Error> {
|
||||
use self::suboptimal_attestations::dsl::*;
|
||||
|
||||
Ok(suboptimal_attestations
|
||||
.filter(epoch_start_slot.eq(epoch_start_slot_query))
|
||||
.load::<WatchSuboptimalAttestation>(conn)?)
|
||||
}
|
56
watch/src/suboptimal_attestations/mod.rs
Normal file
56
watch/src/suboptimal_attestations/mod.rs
Normal file
@ -0,0 +1,56 @@
|
||||
pub mod database;
|
||||
pub mod server;
|
||||
pub mod updater;
|
||||
|
||||
use crate::database::watch_types::WatchSlot;
|
||||
use crate::updater::error::Error;
|
||||
|
||||
pub use database::{
|
||||
get_all_suboptimal_attestations_for_epoch, get_attestation_by_index, get_attestation_by_pubkey,
|
||||
get_highest_attestation, get_lowest_attestation, insert_batch_suboptimal_attestations,
|
||||
WatchAttestation, WatchSuboptimalAttestation,
|
||||
};
|
||||
|
||||
pub use server::{attestation_routes, blockprint_attestation_routes};
|
||||
|
||||
use eth2::BeaconNodeHttpClient;
|
||||
use types::Epoch;
|
||||
|
||||
/// Sends a request to `lighthouse/analysis/attestation_performance`.
|
||||
/// Formats the response into a vector of `WatchSuboptimalAttestation`.
|
||||
///
|
||||
/// Any attestations with `source == true && head == true && target == true` are ignored.
|
||||
pub async fn get_attestation_performances(
|
||||
bn: &BeaconNodeHttpClient,
|
||||
start_epoch: Epoch,
|
||||
end_epoch: Epoch,
|
||||
slots_per_epoch: u64,
|
||||
) -> Result<Vec<WatchSuboptimalAttestation>, Error> {
|
||||
let mut output = Vec::new();
|
||||
let result = bn
|
||||
.get_lighthouse_analysis_attestation_performance(
|
||||
start_epoch,
|
||||
end_epoch,
|
||||
"global".to_string(),
|
||||
)
|
||||
.await?;
|
||||
for index in result {
|
||||
for epoch in index.epochs {
|
||||
if epoch.1.active {
|
||||
// Check if the attestation is suboptimal.
|
||||
if !epoch.1.source || !epoch.1.head || !epoch.1.target {
|
||||
output.push(WatchSuboptimalAttestation {
|
||||
epoch_start_slot: WatchSlot::from_slot(
|
||||
Epoch::new(epoch.0).start_slot(slots_per_epoch),
|
||||
),
|
||||
index: index.index as i32,
|
||||
source: epoch.1.source,
|
||||
head: epoch.1.head,
|
||||
target: epoch.1.target,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(output)
|
||||
}
|
299
watch/src/suboptimal_attestations/server.rs
Normal file
299
watch/src/suboptimal_attestations/server.rs
Normal file
@ -0,0 +1,299 @@
|
||||
use crate::database::{
|
||||
get_canonical_slot, get_connection, get_validator_by_index, get_validator_by_public_key,
|
||||
get_validators_clients_at_slot, get_validators_latest_proposer_info, PgPool, WatchPK,
|
||||
WatchSlot,
|
||||
};
|
||||
|
||||
use crate::blockprint::database::construct_validator_blockprints_at_slot;
|
||||
use crate::server::Error;
|
||||
use crate::suboptimal_attestations::database::{
|
||||
get_all_suboptimal_attestations_for_epoch, get_attestation_by_index,
|
||||
get_validators_missed_head, get_validators_missed_source, get_validators_missed_target,
|
||||
WatchAttestation, WatchSuboptimalAttestation,
|
||||
};
|
||||
|
||||
use axum::{extract::Path, routing::get, Extension, Json, Router};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use types::Epoch;
|
||||
|
||||
// Will return Ok(None) if the epoch is not synced or if the validator does not exist.
|
||||
// In the future it might be worth differentiating these events.
|
||||
pub async fn get_validator_attestation(
|
||||
Path((validator_query, epoch_query)): Path<(String, u64)>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<Option<WatchAttestation>>, Error> {
|
||||
let mut conn = get_connection(&pool).map_err(Error::Database)?;
|
||||
let epoch = Epoch::new(epoch_query);
|
||||
|
||||
// Ensure the database has synced the target epoch.
|
||||
if get_canonical_slot(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(epoch.end_slot(slots_per_epoch)),
|
||||
)?
|
||||
.is_none()
|
||||
{
|
||||
// Epoch is not fully synced.
|
||||
return Ok(Json(None));
|
||||
}
|
||||
|
||||
let index = if validator_query.starts_with("0x") {
|
||||
let pubkey = WatchPK::from_str(&validator_query).map_err(|_| Error::BadRequest)?;
|
||||
get_validator_by_public_key(&mut conn, pubkey)?
|
||||
.ok_or(Error::NotFound)?
|
||||
.index
|
||||
} else {
|
||||
i32::from_str(&validator_query).map_err(|_| Error::BadRequest)?
|
||||
};
|
||||
let attestation = if let Some(suboptimal_attestation) =
|
||||
get_attestation_by_index(&mut conn, index, epoch, slots_per_epoch)?
|
||||
{
|
||||
Some(suboptimal_attestation.to_attestation(slots_per_epoch))
|
||||
} else {
|
||||
// Attestation was not in database. Check if the validator was active.
|
||||
match get_validator_by_index(&mut conn, index)? {
|
||||
Some(validator) => {
|
||||
if let Some(activation_epoch) = validator.activation_epoch {
|
||||
if activation_epoch <= epoch.as_u64() as i32 {
|
||||
if let Some(exit_epoch) = validator.exit_epoch {
|
||||
if exit_epoch > epoch.as_u64() as i32 {
|
||||
// Validator is active and has not yet exited.
|
||||
Some(WatchAttestation::optimal(index, epoch))
|
||||
} else {
|
||||
// Validator has exited.
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// Validator is active and has not yet exited.
|
||||
Some(WatchAttestation::optimal(index, epoch))
|
||||
}
|
||||
} else {
|
||||
// Validator is not yet active.
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// Validator is not yet active.
|
||||
None
|
||||
}
|
||||
}
|
||||
None => return Err(Error::Other("Validator index does not exist".to_string())),
|
||||
}
|
||||
};
|
||||
Ok(Json(attestation))
|
||||
}
|
||||
|
||||
pub async fn get_all_validators_attestations(
|
||||
Path(epoch): Path<u64>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<Vec<WatchSuboptimalAttestation>>, Error> {
|
||||
let mut conn = get_connection(&pool).map_err(Error::Database)?;
|
||||
|
||||
let epoch_start_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch));
|
||||
|
||||
Ok(Json(get_all_suboptimal_attestations_for_epoch(
|
||||
&mut conn,
|
||||
epoch_start_slot,
|
||||
)?))
|
||||
}
|
||||
|
||||
pub async fn get_validators_missed_vote(
|
||||
Path((vote, epoch)): Path<(String, u64)>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<Vec<i32>>, Error> {
|
||||
let mut conn = get_connection(&pool).map_err(Error::Database)?;
|
||||
|
||||
let epoch_start_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch));
|
||||
match vote.to_lowercase().as_str() {
|
||||
"source" => Ok(Json(get_validators_missed_source(
|
||||
&mut conn,
|
||||
epoch_start_slot,
|
||||
)?)),
|
||||
"head" => Ok(Json(get_validators_missed_head(
|
||||
&mut conn,
|
||||
epoch_start_slot,
|
||||
)?)),
|
||||
"target" => Ok(Json(get_validators_missed_target(
|
||||
&mut conn,
|
||||
epoch_start_slot,
|
||||
)?)),
|
||||
_ => Err(Error::BadRequest),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_validators_missed_vote_graffiti(
|
||||
Path((vote, epoch)): Path<(String, u64)>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<HashMap<String, u64>>, Error> {
|
||||
let mut conn = get_connection(&pool).map_err(Error::Database)?;
|
||||
|
||||
let Json(indices) = get_validators_missed_vote(
|
||||
Path((vote, epoch)),
|
||||
Extension(pool),
|
||||
Extension(slots_per_epoch),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let graffitis = get_validators_latest_proposer_info(&mut conn, indices)?
|
||||
.values()
|
||||
.map(|info| info.graffiti.clone())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let mut result = HashMap::new();
|
||||
for graffiti in graffitis {
|
||||
if !result.contains_key(&graffiti) {
|
||||
result.insert(graffiti.clone(), 0);
|
||||
}
|
||||
*result
|
||||
.get_mut(&graffiti)
|
||||
.ok_or_else(|| Error::Other("An unexpected error occurred".to_string()))? += 1;
|
||||
}
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub fn attestation_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/v1/validators/:validator/attestation/:epoch",
|
||||
get(get_validator_attestation),
|
||||
)
|
||||
.route(
|
||||
"/v1/validators/all/attestation/:epoch",
|
||||
get(get_all_validators_attestations),
|
||||
)
|
||||
.route(
|
||||
"/v1/validators/missed/:vote/:epoch",
|
||||
get(get_validators_missed_vote),
|
||||
)
|
||||
.route(
|
||||
"/v1/validators/missed/:vote/:epoch/graffiti",
|
||||
get(get_validators_missed_vote_graffiti),
|
||||
)
|
||||
}
|
||||
|
||||
/// The functions below are dependent on Blockprint and if it is disabled, the endpoints will be
|
||||
/// disabled.
|
||||
pub async fn get_clients_missed_vote(
|
||||
Path((vote, epoch)): Path<(String, u64)>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<HashMap<String, u64>>, Error> {
|
||||
let mut conn = get_connection(&pool).map_err(Error::Database)?;
|
||||
|
||||
let Json(indices) = get_validators_missed_vote(
|
||||
Path((vote, epoch)),
|
||||
Extension(pool),
|
||||
Extension(slots_per_epoch),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// All validators which missed the vote.
|
||||
let indices_map = indices.into_iter().collect::<HashSet<i32>>();
|
||||
|
||||
let target_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch));
|
||||
|
||||
// All validators.
|
||||
let client_map =
|
||||
construct_validator_blockprints_at_slot(&mut conn, target_slot, slots_per_epoch)?;
|
||||
|
||||
let mut result = HashMap::new();
|
||||
|
||||
for index in indices_map {
|
||||
if let Some(print) = client_map.get(&index) {
|
||||
if !result.contains_key(print) {
|
||||
result.insert(print.clone(), 0);
|
||||
}
|
||||
*result
|
||||
.get_mut(print)
|
||||
.ok_or_else(|| Error::Other("An unexpected error occurred".to_string()))? += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn get_clients_missed_vote_percentages(
|
||||
Path((vote, epoch)): Path<(String, u64)>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<HashMap<String, f64>>, Error> {
|
||||
let Json(clients_counts) = get_clients_missed_vote(
|
||||
Path((vote, epoch)),
|
||||
Extension(pool.clone()),
|
||||
Extension(slots_per_epoch),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let target_slot = WatchSlot::from_slot(Epoch::new(epoch).start_slot(slots_per_epoch));
|
||||
|
||||
let mut conn = get_connection(&pool)?;
|
||||
let totals = get_validators_clients_at_slot(&mut conn, target_slot, slots_per_epoch)?;
|
||||
|
||||
let mut result = HashMap::new();
|
||||
for (client, count) in clients_counts.iter() {
|
||||
let client_total: f64 = *totals
|
||||
.get(client)
|
||||
.ok_or_else(|| Error::Other("Client type mismatch".to_string()))?
|
||||
as f64;
|
||||
// `client_total` should never be `0`, but if it is, return `0` instead of `inf`.
|
||||
if client_total == 0.0 {
|
||||
result.insert(client.to_string(), 0.0);
|
||||
} else {
|
||||
let percentage: f64 = *count as f64 / client_total * 100.0;
|
||||
result.insert(client.to_string(), percentage);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub async fn get_clients_missed_vote_percentages_relative(
|
||||
Path((vote, epoch)): Path<(String, u64)>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(slots_per_epoch): Extension<u64>,
|
||||
) -> Result<Json<HashMap<String, f64>>, Error> {
|
||||
let Json(clients_counts) = get_clients_missed_vote(
|
||||
Path((vote, epoch)),
|
||||
Extension(pool),
|
||||
Extension(slots_per_epoch),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut total: u64 = 0;
|
||||
for (_, count) in clients_counts.iter() {
|
||||
total += *count
|
||||
}
|
||||
|
||||
let mut result = HashMap::new();
|
||||
for (client, count) in clients_counts.iter() {
|
||||
// `total` should never be 0, but if it is, return `-` instead of `inf`.
|
||||
if total == 0 {
|
||||
result.insert(client.to_string(), 0.0);
|
||||
} else {
|
||||
let percentage: f64 = *count as f64 / total as f64 * 100.0;
|
||||
result.insert(client.to_string(), percentage);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
pub fn blockprint_attestation_routes() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/v1/clients/missed/:vote/:epoch",
|
||||
get(get_clients_missed_vote),
|
||||
)
|
||||
.route(
|
||||
"/v1/clients/missed/:vote/:epoch/percentages",
|
||||
get(get_clients_missed_vote_percentages),
|
||||
)
|
||||
.route(
|
||||
"/v1/clients/missed/:vote/:epoch/percentages/relative",
|
||||
get(get_clients_missed_vote_percentages_relative),
|
||||
)
|
||||
}
|
236
watch/src/suboptimal_attestations/updater.rs
Normal file
236
watch/src/suboptimal_attestations/updater.rs
Normal file
@ -0,0 +1,236 @@
|
||||
use crate::database::{self, Error as DbError};
|
||||
use crate::updater::{Error, UpdateHandler};
|
||||
|
||||
use crate::suboptimal_attestations::get_attestation_performances;
|
||||
|
||||
use eth2::types::EthSpec;
|
||||
use log::{debug, error, warn};
|
||||
|
||||
const MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS: u64 = 50;
|
||||
|
||||
impl<T: EthSpec> UpdateHandler<T> {
|
||||
/// Forward fills the `suboptimal_attestations` table starting from the entry with the highest
|
||||
/// slot.
|
||||
///
|
||||
/// It construts a request to the `attestation_performance` API endpoint with:
|
||||
/// `start_epoch` -> highest completely filled epoch + 1 (or epoch of lowest canonical slot)
|
||||
/// `end_epoch` -> epoch of highest canonical slot
|
||||
///
|
||||
/// It will resync the latest epoch if it is not fully filled but will not overwrite existing
|
||||
/// values unless there is a re-org.
|
||||
/// That is, `if highest_filled_slot % slots_per_epoch != 31`.
|
||||
///
|
||||
/// In the event the most recent epoch has no suboptimal attestations, it will attempt to
|
||||
/// resync that epoch. The odds of this occuring on mainnet are vanishingly small so it is not
|
||||
/// accounted for.
|
||||
///
|
||||
/// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`.
|
||||
pub async fn fill_suboptimal_attestations(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
let highest_filled_slot_opt = if self.config.attestations {
|
||||
database::get_highest_attestation(&mut conn)?
|
||||
.map(|attestation| attestation.epoch_start_slot.as_slot())
|
||||
} else {
|
||||
return Err(Error::NotEnabled("attestations".to_string()));
|
||||
};
|
||||
|
||||
let start_epoch = if let Some(highest_filled_slot) = highest_filled_slot_opt {
|
||||
if highest_filled_slot % self.slots_per_epoch == self.slots_per_epoch.saturating_sub(1)
|
||||
{
|
||||
// The whole epoch is filled so we can begin syncing the next one.
|
||||
highest_filled_slot.epoch(self.slots_per_epoch) + 1
|
||||
} else {
|
||||
// The epoch is only partially synced. Try to sync it fully.
|
||||
highest_filled_slot.epoch(self.slots_per_epoch)
|
||||
}
|
||||
} else {
|
||||
// No rows present in the `suboptimal_attestations` table. Use `canonical_slots`
|
||||
// instead.
|
||||
if let Some(lowest_canonical_slot) = database::get_lowest_canonical_slot(&mut conn)? {
|
||||
lowest_canonical_slot
|
||||
.slot
|
||||
.as_slot()
|
||||
.epoch(self.slots_per_epoch)
|
||||
} else {
|
||||
// There are no slots in the database, do not fill the `suboptimal_attestations`
|
||||
// table.
|
||||
warn!("Refusing to fill the `suboptimal_attestations` table as there are no slots in the database");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(highest_canonical_slot) =
|
||||
database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot())
|
||||
{
|
||||
let mut end_epoch = highest_canonical_slot.epoch(self.slots_per_epoch);
|
||||
|
||||
// The `lighthouse/analysis/attestation_performance` endpoint can only retrieve attestations
|
||||
// which are more than 1 epoch old.
|
||||
// We assume that `highest_canonical_slot` is near the head of the chain.
|
||||
end_epoch = end_epoch.saturating_sub(2_u64);
|
||||
|
||||
// If end_epoch == 0 then the chain just started so we need to wait until
|
||||
// `current_epoch >= 2`.
|
||||
if end_epoch == 0 {
|
||||
debug!("Chain just begun, refusing to sync attestations");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if start_epoch > end_epoch {
|
||||
debug!("Attestations are up to date with the head of the database");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure the size of the request does not exceed the maximum allowed value.
|
||||
if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS) {
|
||||
end_epoch = start_epoch + MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS
|
||||
}
|
||||
|
||||
if let Some(lowest_canonical_slot) =
|
||||
database::get_lowest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot())
|
||||
{
|
||||
let mut attestations = get_attestation_performances(
|
||||
&self.bn,
|
||||
start_epoch,
|
||||
end_epoch,
|
||||
self.slots_per_epoch,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Only insert attestations with corresponding `canonical_slot`s.
|
||||
attestations.retain(|attestation| {
|
||||
attestation.epoch_start_slot.as_slot() >= lowest_canonical_slot
|
||||
&& attestation.epoch_start_slot.as_slot() <= highest_canonical_slot
|
||||
});
|
||||
database::insert_batch_suboptimal_attestations(&mut conn, attestations)?;
|
||||
} else {
|
||||
return Err(Error::Database(DbError::Other(
|
||||
"Database did not return a lowest canonical slot when one exists".to_string(),
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// There are no slots in the `canonical_slots` table, but there are entries in the
|
||||
// `suboptimal_attestations` table. This is a critical failure. It usually means
|
||||
// someone has manually tampered with the database tables and should not occur during
|
||||
// normal operation.
|
||||
error!("Database is corrupted. Please re-sync the database");
|
||||
return Err(Error::Database(DbError::DatabaseCorrupted));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Backfill the `suboptimal_attestations` table starting from the entry with the lowest slot.
|
||||
///
|
||||
/// It constructs a request to the `attestation_performance` API endpoint with:
|
||||
/// `start_epoch` -> epoch of the lowest `canonical_slot`.
|
||||
/// `end_epoch` -> epoch of the lowest filled `suboptimal_attestation` - 1 (or epoch of highest
|
||||
/// canonical slot)
|
||||
///
|
||||
/// It will resync the lowest epoch if it is not fully filled.
|
||||
/// That is, `if lowest_filled_slot % slots_per_epoch != 0`
|
||||
///
|
||||
/// In the event there are no suboptimal attestations present in the lowest epoch, it will attempt to
|
||||
/// resync the epoch. The odds of this occuring on mainnet are vanishingly small so it is not
|
||||
/// accounted for.
|
||||
///
|
||||
/// Request range will not exceed `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`.
|
||||
pub async fn backfill_suboptimal_attestations(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
let max_attestation_backfill = self.config.max_backfill_size_epochs;
|
||||
|
||||
// Get the slot of the lowest entry in the `suboptimal_attestations` table.
|
||||
let lowest_filled_slot_opt = if self.config.attestations {
|
||||
database::get_lowest_attestation(&mut conn)?
|
||||
.map(|attestation| attestation.epoch_start_slot.as_slot())
|
||||
} else {
|
||||
return Err(Error::NotEnabled("attestations".to_string()));
|
||||
};
|
||||
|
||||
let end_epoch = if let Some(lowest_filled_slot) = lowest_filled_slot_opt {
|
||||
if lowest_filled_slot % self.slots_per_epoch == 0 {
|
||||
lowest_filled_slot
|
||||
.epoch(self.slots_per_epoch)
|
||||
.saturating_sub(1_u64)
|
||||
} else {
|
||||
// The epoch is only partially synced. Try to sync it fully.
|
||||
lowest_filled_slot.epoch(self.slots_per_epoch)
|
||||
}
|
||||
} else {
|
||||
// No entries in the `suboptimal_attestations` table. Use `canonical_slots` instead.
|
||||
if let Some(highest_canonical_slot) =
|
||||
database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot())
|
||||
{
|
||||
// Subtract 2 since `end_epoch` must be less than the current epoch - 1.
|
||||
// We assume that `highest_canonical_slot` is near the head of the chain.
|
||||
highest_canonical_slot
|
||||
.epoch(self.slots_per_epoch)
|
||||
.saturating_sub(2_u64)
|
||||
} else {
|
||||
// There are no slots in the database, do not backfill the
|
||||
// `suboptimal_attestations` table.
|
||||
warn!("Refusing to backfill attestations as there are no slots in the database");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
if end_epoch == 0 {
|
||||
debug!("Attestations backfill is complete");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(lowest_canonical_slot) =
|
||||
database::get_lowest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot())
|
||||
{
|
||||
let mut start_epoch = lowest_canonical_slot.epoch(self.slots_per_epoch);
|
||||
|
||||
if start_epoch > end_epoch {
|
||||
debug!("Attestations are up to date with the base of the database");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Ensure the request range does not exceed `max_attestation_backfill` or
|
||||
// `MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS`.
|
||||
if start_epoch < end_epoch.saturating_sub(max_attestation_backfill) {
|
||||
start_epoch = end_epoch.saturating_sub(max_attestation_backfill)
|
||||
}
|
||||
if start_epoch < end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS) {
|
||||
start_epoch = end_epoch.saturating_sub(MAX_SIZE_SINGLE_REQUEST_ATTESTATIONS)
|
||||
}
|
||||
|
||||
if let Some(highest_canonical_slot) =
|
||||
database::get_highest_canonical_slot(&mut conn)?.map(|slot| slot.slot.as_slot())
|
||||
{
|
||||
let mut attestations = get_attestation_performances(
|
||||
&self.bn,
|
||||
start_epoch,
|
||||
end_epoch,
|
||||
self.slots_per_epoch,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Only insert `suboptimal_attestations` with corresponding `canonical_slots`.
|
||||
attestations.retain(|attestation| {
|
||||
attestation.epoch_start_slot.as_slot() >= lowest_canonical_slot
|
||||
&& attestation.epoch_start_slot.as_slot() <= highest_canonical_slot
|
||||
});
|
||||
|
||||
database::insert_batch_suboptimal_attestations(&mut conn, attestations)?;
|
||||
} else {
|
||||
return Err(Error::Database(DbError::Other(
|
||||
"Database did not return a lowest slot when one exists".to_string(),
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// There are no slots in the `canonical_slot` table, but there are entries in the
|
||||
// `suboptimal_attestations` table. This is a critical failure. It usually means
|
||||
// someone has manually tampered with the database tables and should not occur during
|
||||
// normal operation.
|
||||
error!("Database is corrupted. Please re-sync the database");
|
||||
return Err(Error::Database(DbError::DatabaseCorrupted));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
65
watch/src/updater/config.rs
Normal file
65
watch/src/updater/config.rs
Normal file
@ -0,0 +1,65 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const BEACON_NODE_URL: &str = "http://127.0.0.1:5052";
|
||||
|
||||
pub const fn max_backfill_size_epochs() -> u64 {
|
||||
2
|
||||
}
|
||||
pub const fn backfill_stop_epoch() -> u64 {
|
||||
0
|
||||
}
|
||||
pub const fn attestations() -> bool {
|
||||
true
|
||||
}
|
||||
pub const fn proposer_info() -> bool {
|
||||
true
|
||||
}
|
||||
pub const fn block_rewards() -> bool {
|
||||
true
|
||||
}
|
||||
pub const fn block_packing() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn beacon_node_url() -> String {
|
||||
BEACON_NODE_URL.to_string()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// The URL of the beacon you wish to sync from.
|
||||
#[serde(default = "beacon_node_url")]
|
||||
pub beacon_node_url: String,
|
||||
/// The maximum size each backfill iteration will allow per request (in epochs).
|
||||
#[serde(default = "max_backfill_size_epochs")]
|
||||
pub max_backfill_size_epochs: u64,
|
||||
/// The epoch at which to never backfill past.
|
||||
#[serde(default = "backfill_stop_epoch")]
|
||||
pub backfill_stop_epoch: u64,
|
||||
/// Whether to sync the suboptimal_attestations table.
|
||||
#[serde(default = "attestations")]
|
||||
pub attestations: bool,
|
||||
/// Whether to sync the proposer_info table.
|
||||
#[serde(default = "proposer_info")]
|
||||
pub proposer_info: bool,
|
||||
/// Whether to sync the block_rewards table.
|
||||
#[serde(default = "block_rewards")]
|
||||
pub block_rewards: bool,
|
||||
/// Whether to sync the block_packing table.
|
||||
#[serde(default = "block_packing")]
|
||||
pub block_packing: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
beacon_node_url: beacon_node_url(),
|
||||
max_backfill_size_epochs: max_backfill_size_epochs(),
|
||||
backfill_stop_epoch: backfill_stop_epoch(),
|
||||
attestations: attestations(),
|
||||
proposer_info: proposer_info(),
|
||||
block_rewards: block_rewards(),
|
||||
block_packing: block_packing(),
|
||||
}
|
||||
}
|
||||
}
|
56
watch/src/updater/error.rs
Normal file
56
watch/src/updater/error.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use crate::blockprint::Error as BlockprintError;
|
||||
use crate::database::Error as DbError;
|
||||
use beacon_node::beacon_chain::BeaconChainError;
|
||||
use eth2::{Error as Eth2Error, SensitiveError};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
BeaconChain(BeaconChainError),
|
||||
Eth2(Eth2Error),
|
||||
SensitiveUrl(SensitiveError),
|
||||
Database(DbError),
|
||||
Blockprint(BlockprintError),
|
||||
UnableToGetRemoteHead,
|
||||
BeaconNodeSyncing,
|
||||
NotEnabled(String),
|
||||
NoValidatorsFound,
|
||||
BeaconNodeNotCompatible(String),
|
||||
InvalidConfig(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BeaconChainError> for Error {
|
||||
fn from(e: BeaconChainError) -> Self {
|
||||
Error::BeaconChain(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Eth2Error> for Error {
|
||||
fn from(e: Eth2Error) -> Self {
|
||||
Error::Eth2(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SensitiveError> for Error {
|
||||
fn from(e: SensitiveError) -> Self {
|
||||
Error::SensitiveUrl(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DbError> for Error {
|
||||
fn from(e: DbError) -> Self {
|
||||
Error::Database(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockprintError> for Error {
|
||||
fn from(e: BlockprintError) -> Self {
|
||||
Error::Blockprint(e)
|
||||
}
|
||||
}
|
471
watch/src/updater/handler.rs
Normal file
471
watch/src/updater/handler.rs
Normal file
@ -0,0 +1,471 @@
|
||||
use crate::blockprint::WatchBlockprintClient;
|
||||
use crate::config::Config as FullConfig;
|
||||
use crate::database::{self, PgPool, WatchCanonicalSlot, WatchHash, WatchSlot};
|
||||
use crate::updater::{Config, Error, WatchSpec};
|
||||
use beacon_node::beacon_chain::BeaconChainError;
|
||||
use eth2::{
|
||||
types::{BlockId, SyncingData},
|
||||
BeaconNodeHttpClient, SensitiveUrl,
|
||||
};
|
||||
use log::{debug, error, info, warn};
|
||||
use std::collections::HashSet;
|
||||
use std::iter::FromIterator;
|
||||
use types::{BeaconBlockHeader, EthSpec, Hash256, SignedBeaconBlock, Slot};
|
||||
|
||||
use crate::updater::{get_beacon_block, get_header, get_validators};
|
||||
|
||||
const MAX_EXPECTED_REORG_LENGTH: u64 = 32;
|
||||
|
||||
/// Ensure the existing database is valid for this run.
|
||||
pub async fn ensure_valid_database<T: EthSpec>(
|
||||
spec: &WatchSpec<T>,
|
||||
pool: &mut PgPool,
|
||||
) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(pool)?;
|
||||
|
||||
let bn_slots_per_epoch = spec.slots_per_epoch();
|
||||
let bn_config_name = spec.network.clone();
|
||||
|
||||
if let Some((db_config_name, db_slots_per_epoch)) = database::get_active_config(&mut conn)? {
|
||||
if db_config_name != bn_config_name || db_slots_per_epoch != bn_slots_per_epoch as i32 {
|
||||
Err(Error::InvalidConfig(
|
||||
"The config stored in the database does not match the beacon node.".to_string(),
|
||||
))
|
||||
} else {
|
||||
// Configs match.
|
||||
Ok(())
|
||||
}
|
||||
} else {
|
||||
// No config exists in the DB.
|
||||
database::insert_active_config(&mut conn, bn_config_name, bn_slots_per_epoch)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UpdateHandler<T: EthSpec> {
|
||||
pub pool: PgPool,
|
||||
pub bn: BeaconNodeHttpClient,
|
||||
pub blockprint: Option<WatchBlockprintClient>,
|
||||
pub config: Config,
|
||||
pub slots_per_epoch: u64,
|
||||
pub spec: WatchSpec<T>,
|
||||
}
|
||||
|
||||
impl<T: EthSpec> UpdateHandler<T> {
|
||||
pub async fn new(
|
||||
bn: BeaconNodeHttpClient,
|
||||
spec: WatchSpec<T>,
|
||||
config: FullConfig,
|
||||
) -> Result<UpdateHandler<T>, Error> {
|
||||
let blockprint = if config.blockprint.enabled {
|
||||
if let Some(server) = config.blockprint.url {
|
||||
let blockprint_url = SensitiveUrl::parse(&server).map_err(Error::SensitiveUrl)?;
|
||||
Some(WatchBlockprintClient {
|
||||
client: reqwest::Client::new(),
|
||||
server: blockprint_url,
|
||||
username: config.blockprint.username,
|
||||
password: config.blockprint.password,
|
||||
})
|
||||
} else {
|
||||
return Err(Error::NotEnabled(
|
||||
"blockprint was enabled but url was not set".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut pool = database::build_connection_pool(&config.database)?;
|
||||
|
||||
ensure_valid_database(&spec, &mut pool).await?;
|
||||
|
||||
Ok(Self {
|
||||
pool,
|
||||
bn,
|
||||
blockprint,
|
||||
config: config.updater,
|
||||
slots_per_epoch: spec.slots_per_epoch(),
|
||||
spec,
|
||||
})
|
||||
}
|
||||
|
||||
/// Gets the syncing status of the connected beacon node.
|
||||
pub async fn get_bn_syncing_status(&mut self) -> Result<SyncingData, Error> {
|
||||
Ok(self.bn.get_node_syncing().await?.data)
|
||||
}
|
||||
|
||||
/// Gets a list of block roots from the database which do not yet contain a corresponding
|
||||
/// entry in the `beacon_blocks` table and inserts them.
|
||||
pub async fn update_unknown_blocks(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
let roots = database::get_unknown_canonical_blocks(&mut conn)?;
|
||||
for root in roots {
|
||||
let block_opt: Option<SignedBeaconBlock<T>> =
|
||||
get_beacon_block(&self.bn, BlockId::Root(root.as_hash())).await?;
|
||||
if let Some(block) = block_opt {
|
||||
database::insert_beacon_block(&mut conn, block, root)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Performs a head update with the following steps:
|
||||
/// 1. Pull the latest header from the beacon node and the latest canonical slot from the
|
||||
/// database.
|
||||
/// 2. Loop back through the beacon node and database to find the first matching slot -> root
|
||||
/// pair.
|
||||
/// 3. Go back `MAX_EXPECTED_REORG_LENGTH` slots through the database ensuring it is
|
||||
/// consistent with the beacon node. If a re-org occurs beyond this range, we cannot recover.
|
||||
/// 4. Remove any invalid slots from the database.
|
||||
/// 5. Sync all blocks between the first valid block of the database and the head of the beacon
|
||||
/// chain.
|
||||
///
|
||||
/// In the event there are no slots present in the database, it will sync from the head block
|
||||
/// block back to the first slot of the epoch.
|
||||
/// This will ensure backfills are always done in full epochs (which helps keep certain syncing
|
||||
/// tasks efficient).
|
||||
pub async fn perform_head_update(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
// Load the head from the beacon node.
|
||||
let bn_header = get_header(&self.bn, BlockId::Head)
|
||||
.await?
|
||||
.ok_or(Error::UnableToGetRemoteHead)?;
|
||||
let header_root = bn_header.canonical_root();
|
||||
|
||||
if let Some(latest_matching_canonical_slot) =
|
||||
self.get_first_matching_block(bn_header.clone()).await?
|
||||
{
|
||||
// Check for reorgs.
|
||||
let latest_db_slot = self.check_for_reorg(latest_matching_canonical_slot).await?;
|
||||
|
||||
// Remove all slots above `latest_db_slot` from the database.
|
||||
let result = database::delete_canonical_slots_above(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(latest_db_slot),
|
||||
)?;
|
||||
info!("{result} old records removed during head update");
|
||||
|
||||
if result > 0 {
|
||||
// If slots were removed, we need to resync the suboptimal_attestations table for
|
||||
// the epoch since they will have changed and cannot be fixed by a simple update.
|
||||
let epoch = latest_db_slot
|
||||
.epoch(self.slots_per_epoch)
|
||||
.saturating_sub(1_u64);
|
||||
debug!("Preparing to resync attestations above epoch {epoch}");
|
||||
database::delete_suboptimal_attestations_above(
|
||||
&mut conn,
|
||||
WatchSlot::from_slot(epoch.start_slot(self.slots_per_epoch)),
|
||||
)?;
|
||||
}
|
||||
|
||||
// Since we are syncing backwards, `start_slot > `end_slot`.
|
||||
let start_slot = bn_header.slot;
|
||||
let end_slot = latest_db_slot + 1;
|
||||
self.reverse_fill_canonical_slots(bn_header, header_root, false, start_slot, end_slot)
|
||||
.await?;
|
||||
info!("Reverse sync begun at slot {start_slot} and stopped at slot {end_slot}");
|
||||
|
||||
// Attempt to sync new blocks with blockprint.
|
||||
//self.sync_blockprint_until(start_slot).await?;
|
||||
} else {
|
||||
// There are no matching parent blocks. Sync from the head block back until the first
|
||||
// block of the epoch.
|
||||
let start_slot = bn_header.slot;
|
||||
let end_slot = start_slot.saturating_sub(start_slot % self.slots_per_epoch);
|
||||
self.reverse_fill_canonical_slots(bn_header, header_root, false, start_slot, end_slot)
|
||||
.await?;
|
||||
info!("Reverse sync begun at slot {start_slot} and stopped at slot {end_slot}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Attempt to find a row in the `canonical_slots` table which matches the `canonical_root` of
|
||||
/// the block header as reported by the beacon node.
|
||||
///
|
||||
/// Any blocks above this value are not canonical according to the beacon node.
|
||||
///
|
||||
/// Note: In the event that there are skip slots above the slot returned by the function,
|
||||
/// they will not be returned, so may be pruned or re-synced by other code despite being
|
||||
/// canonical.
|
||||
pub async fn get_first_matching_block(
|
||||
&mut self,
|
||||
mut bn_header: BeaconBlockHeader,
|
||||
) -> Result<Option<WatchCanonicalSlot>, Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
// Load latest non-skipped canonical slot from database.
|
||||
if let Some(db_canonical_slot) =
|
||||
database::get_highest_non_skipped_canonical_slot(&mut conn)?
|
||||
{
|
||||
// Check if the header or parent root matches the entry in the database.
|
||||
if bn_header.parent_root == db_canonical_slot.root.as_hash()
|
||||
|| bn_header.canonical_root() == db_canonical_slot.root.as_hash()
|
||||
{
|
||||
Ok(Some(db_canonical_slot))
|
||||
} else {
|
||||
// Header is not the child of the highest entry in the database.
|
||||
// From here we need to iterate backwards through the database until we find
|
||||
// a slot -> root pair that matches the beacon node.
|
||||
loop {
|
||||
// Store working `parent_root`.
|
||||
let parent_root = bn_header.parent_root;
|
||||
|
||||
// Try the next header.
|
||||
let next_header = get_header(&self.bn, BlockId::Root(parent_root)).await?;
|
||||
if let Some(header) = next_header {
|
||||
bn_header = header.clone();
|
||||
if let Some(db_canonical_slot) = database::get_canonical_slot_by_root(
|
||||
&mut conn,
|
||||
WatchHash::from_hash(header.parent_root),
|
||||
)? {
|
||||
// Check if the entry in the database matches the parent of
|
||||
// the header.
|
||||
if header.parent_root == db_canonical_slot.root.as_hash() {
|
||||
return Ok(Some(db_canonical_slot));
|
||||
} else {
|
||||
// Move on to the next header.
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// Database does not have the referenced root. Try the next header.
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// If we get this error it means that the `parent_root` of the header
|
||||
// did not reference a canonical block.
|
||||
return Err(Error::BeaconChain(BeaconChainError::MissingBeaconBlock(
|
||||
parent_root,
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// There are no non-skipped blocks present in the database.
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the latest slot in the database which matches a root in the beacon node,
|
||||
/// traverse back through the database for `MAX_EXPECTED_REORG_LENGTH` slots to ensure the tip
|
||||
/// of the database is consistent with the beacon node (in the case that reorgs have occured).
|
||||
///
|
||||
/// Returns the slot before the oldest canonical_slot which has an invalid child.
|
||||
pub async fn check_for_reorg(
|
||||
&mut self,
|
||||
latest_canonical_slot: WatchCanonicalSlot,
|
||||
) -> Result<Slot, Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
let end_slot = latest_canonical_slot.slot.as_u64();
|
||||
let start_slot = end_slot.saturating_sub(MAX_EXPECTED_REORG_LENGTH);
|
||||
|
||||
for i in start_slot..end_slot {
|
||||
let slot = Slot::new(i);
|
||||
let db_canonical_slot_opt =
|
||||
database::get_canonical_slot(&mut conn, WatchSlot::from_slot(slot))?;
|
||||
if let Some(db_canonical_slot) = db_canonical_slot_opt {
|
||||
let header_opt = get_header(&self.bn, BlockId::Slot(slot)).await?;
|
||||
if let Some(header) = header_opt {
|
||||
if header.canonical_root() == db_canonical_slot.root.as_hash() {
|
||||
// The roots match (or are both skip slots).
|
||||
continue;
|
||||
} else {
|
||||
// The block roots do not match. We need to re-sync from here.
|
||||
warn!("Block {slot} does not match the beacon node. Resyncing");
|
||||
return Ok(slot.saturating_sub(1_u64));
|
||||
}
|
||||
} else if !db_canonical_slot.skipped {
|
||||
// The block exists in the database, but does not exist on the beacon node.
|
||||
// We need to re-sync from here.
|
||||
warn!("Block {slot} does not exist on the beacon node. Resyncing");
|
||||
return Ok(slot.saturating_sub(1_u64));
|
||||
}
|
||||
} else {
|
||||
// This slot does not exist in the database.
|
||||
let lowest_slot = database::get_lowest_canonical_slot(&mut conn)?
|
||||
.map(|canonical_slot| canonical_slot.slot.as_slot());
|
||||
if lowest_slot > Some(slot) {
|
||||
// The database has not back-filled this slot yet, so skip it.
|
||||
continue;
|
||||
} else {
|
||||
// The database does not contain this block, but has back-filled past it.
|
||||
// We need to resync from here.
|
||||
warn!("Slot {slot} missing from database. Resyncing");
|
||||
return Ok(slot.saturating_sub(1_u64));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The database is consistent with the beacon node, so return the head of the database.
|
||||
Ok(latest_canonical_slot.slot.as_slot())
|
||||
}
|
||||
|
||||
/// Fills the canonical slots table beginning from `start_slot` and ending at `end_slot`.
|
||||
/// It fills in reverse order, that is, `start_slot` is higher than `end_slot`.
|
||||
///
|
||||
/// Skip slots set `root` to the root of the previous non-skipped slot and also sets
|
||||
/// `skipped == true`.
|
||||
///
|
||||
/// Since it uses `insert_canonical_slot` to interact with the database, it WILL NOT overwrite
|
||||
/// existing rows. This means that any part of the chain within `end_slot..=start_slot` that
|
||||
/// needs to be resynced, must first be deleted from the database.
|
||||
pub async fn reverse_fill_canonical_slots(
|
||||
&mut self,
|
||||
mut header: BeaconBlockHeader,
|
||||
mut header_root: Hash256,
|
||||
mut skipped: bool,
|
||||
start_slot: Slot,
|
||||
end_slot: Slot,
|
||||
) -> Result<usize, Error> {
|
||||
let mut count = 0;
|
||||
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
// Iterate, descending from `start_slot` (higher) to `end_slot` (lower).
|
||||
for slot in (end_slot.as_u64()..=start_slot.as_u64()).rev() {
|
||||
// Insert header.
|
||||
database::insert_canonical_slot(
|
||||
&mut conn,
|
||||
WatchCanonicalSlot {
|
||||
slot: WatchSlot::new(slot),
|
||||
root: WatchHash::from_hash(header_root),
|
||||
skipped,
|
||||
beacon_block: None,
|
||||
},
|
||||
)?;
|
||||
count += 1;
|
||||
|
||||
// Load the next header:
|
||||
// We must use BlockId::Slot since we want to include skip slots.
|
||||
header = if let Some(new_header) = get_header(
|
||||
&self.bn,
|
||||
BlockId::Slot(Slot::new(slot.saturating_sub(1_u64))),
|
||||
)
|
||||
.await?
|
||||
{
|
||||
header_root = new_header.canonical_root();
|
||||
skipped = false;
|
||||
new_header
|
||||
} else {
|
||||
if header.slot == 0 {
|
||||
info!("Reverse fill exhausted at slot 0");
|
||||
break;
|
||||
}
|
||||
// Slot was skipped, so use the parent_root (most recent non-skipped block).
|
||||
skipped = true;
|
||||
header_root = header.parent_root;
|
||||
header
|
||||
};
|
||||
}
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Backfills the `canonical_slots` table starting from the lowest non-skipped slot and
|
||||
/// stopping after `max_backfill_size_epochs` epochs.
|
||||
pub async fn backfill_canonical_slots(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
let backfill_stop_slot = self.config.backfill_stop_epoch * self.slots_per_epoch;
|
||||
// Check to see if we have finished backfilling.
|
||||
if let Some(lowest_slot) = database::get_lowest_canonical_slot(&mut conn)? {
|
||||
if lowest_slot.slot.as_slot() == backfill_stop_slot {
|
||||
debug!("Backfill sync complete, all slots filled");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let backfill_slot_count = self.config.max_backfill_size_epochs * self.slots_per_epoch;
|
||||
|
||||
if let Some(lowest_non_skipped_canonical_slot) =
|
||||
database::get_lowest_non_skipped_canonical_slot(&mut conn)?
|
||||
{
|
||||
// Set `start_slot` equal to the lowest non-skipped slot in the database.
|
||||
// While this will attempt to resync some parts of the bottom of the chain, it reduces
|
||||
// complexity when dealing with skip slots.
|
||||
let start_slot = lowest_non_skipped_canonical_slot.slot.as_slot();
|
||||
let mut end_slot = lowest_non_skipped_canonical_slot
|
||||
.slot
|
||||
.as_slot()
|
||||
.saturating_sub(backfill_slot_count);
|
||||
|
||||
// Ensure end_slot doesn't go below `backfill_stop_epoch`
|
||||
if end_slot <= backfill_stop_slot {
|
||||
end_slot = Slot::new(backfill_stop_slot);
|
||||
}
|
||||
|
||||
let header_opt = get_header(&self.bn, BlockId::Slot(start_slot)).await?;
|
||||
|
||||
if let Some(header) = header_opt {
|
||||
let header_root = header.canonical_root();
|
||||
let count = self
|
||||
.reverse_fill_canonical_slots(header, header_root, false, start_slot, end_slot)
|
||||
.await?;
|
||||
|
||||
info!("Backfill completed to slot: {end_slot}, records added: {count}");
|
||||
} else {
|
||||
// The lowest slot of the database is inconsistent with the beacon node.
|
||||
// Currently we have no way to recover from this. The entire database will need to
|
||||
// be re-synced.
|
||||
error!(
|
||||
"Database is inconsistent with the beacon node. \
|
||||
Please ensure your beacon node is set to the right network, \
|
||||
otherwise you may need to resync"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// There are no blocks in the database. Forward sync needs to happen first.
|
||||
info!("Backfill was not performed since there are no blocks in the database");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Attempt to update the validator set.
|
||||
// This downloads the latest validator set from the beacon node, and pulls the known validator
|
||||
// set from the database.
|
||||
// We then take any new or updated validators and insert them into the database (overwriting
|
||||
// exiting validators).
|
||||
//
|
||||
// In the event there are no validators in the database, it will initialize the validator set.
|
||||
pub async fn update_validator_set(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
let current_validators = database::get_all_validators(&mut conn)?;
|
||||
|
||||
if !current_validators.is_empty() {
|
||||
let old_validators = HashSet::from_iter(current_validators);
|
||||
|
||||
// Pull the new validator set from the beacon node.
|
||||
let new_validators = get_validators(&self.bn).await?;
|
||||
|
||||
// The difference should only contain validators that contain either a new `exit_epoch` (implying an
|
||||
// exit) or a new `index` (implying a validator activation).
|
||||
let val_diff = new_validators.difference(&old_validators);
|
||||
|
||||
for diff in val_diff {
|
||||
database::insert_validator(&mut conn, diff.clone())?;
|
||||
}
|
||||
} else {
|
||||
info!("No validators present in database. Initializing the validator set");
|
||||
self.initialize_validator_set().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Initialize the validator set by downloading it from the beacon node, inserting blockprint
|
||||
// data (if required) and writing it to the database.
|
||||
pub async fn initialize_validator_set(&mut self) -> Result<(), Error> {
|
||||
let mut conn = database::get_connection(&self.pool)?;
|
||||
|
||||
// Pull all validators from the beacon node.
|
||||
let validators = Vec::from_iter(get_validators(&self.bn).await?);
|
||||
|
||||
database::insert_batch_validators(&mut conn, validators)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
234
watch/src/updater/mod.rs
Normal file
234
watch/src/updater/mod.rs
Normal file
@ -0,0 +1,234 @@
|
||||
use crate::config::Config as FullConfig;
|
||||
use crate::database::{WatchPK, WatchValidator};
|
||||
use eth2::{
|
||||
types::{BlockId, StateId},
|
||||
BeaconNodeHttpClient, SensitiveUrl, Timeouts,
|
||||
};
|
||||
use log::{debug, error, info};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::marker::PhantomData;
|
||||
use std::time::{Duration, Instant};
|
||||
use types::{BeaconBlockHeader, EthSpec, GnosisEthSpec, MainnetEthSpec, SignedBeaconBlock};
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::Error;
|
||||
pub use handler::UpdateHandler;
|
||||
|
||||
mod config;
|
||||
pub mod error;
|
||||
pub mod handler;
|
||||
|
||||
const FAR_FUTURE_EPOCH: u64 = u64::MAX;
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
const MAINNET: &str = "mainnet";
|
||||
const GNOSIS: &str = "gnosis";
|
||||
|
||||
pub struct WatchSpec<T: EthSpec> {
|
||||
network: String,
|
||||
spec: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: EthSpec> WatchSpec<T> {
|
||||
fn slots_per_epoch(&self) -> u64 {
|
||||
T::slots_per_epoch()
|
||||
}
|
||||
}
|
||||
|
||||
impl WatchSpec<MainnetEthSpec> {
|
||||
pub fn mainnet(network: String) -> Self {
|
||||
Self {
|
||||
network,
|
||||
spec: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WatchSpec<GnosisEthSpec> {
|
||||
fn gnosis(network: String) -> Self {
|
||||
Self {
|
||||
network,
|
||||
spec: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_updater(config: FullConfig) -> Result<(), Error> {
|
||||
let beacon_node_url =
|
||||
SensitiveUrl::parse(&config.updater.beacon_node_url).map_err(Error::SensitiveUrl)?;
|
||||
let bn = BeaconNodeHttpClient::new(beacon_node_url, Timeouts::set_all(DEFAULT_TIMEOUT));
|
||||
|
||||
let config_map = bn.get_config_spec::<HashMap<String, String>>().await?.data;
|
||||
|
||||
let config_name = config_map
|
||||
.get("CONFIG_NAME")
|
||||
.ok_or_else(|| {
|
||||
Error::BeaconNodeNotCompatible("No field CONFIG_NAME on beacon node spec".to_string())
|
||||
})?
|
||||
.clone();
|
||||
|
||||
match config_map
|
||||
.get("PRESET_BASE")
|
||||
.ok_or_else(|| {
|
||||
Error::BeaconNodeNotCompatible("No field PRESET_BASE on beacon node spec".to_string())
|
||||
})?
|
||||
.to_lowercase()
|
||||
.as_str()
|
||||
{
|
||||
MAINNET => {
|
||||
let spec = WatchSpec::mainnet(config_name);
|
||||
run_once(bn, spec, config).await
|
||||
}
|
||||
GNOSIS => {
|
||||
let spec = WatchSpec::gnosis(config_name);
|
||||
run_once(bn, spec, config).await
|
||||
}
|
||||
_ => unimplemented!("unsupported PRESET_BASE"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn run_once<T: EthSpec>(
|
||||
bn: BeaconNodeHttpClient,
|
||||
spec: WatchSpec<T>,
|
||||
config: FullConfig,
|
||||
) -> Result<(), Error> {
|
||||
let mut watch = UpdateHandler::new(bn, spec, config.clone()).await?;
|
||||
|
||||
let sync_data = watch.get_bn_syncing_status().await?;
|
||||
if sync_data.is_syncing {
|
||||
error!(
|
||||
"Connected beacon node is still syncing: head_slot => {:?}, distance => {}",
|
||||
sync_data.head_slot, sync_data.sync_distance
|
||||
);
|
||||
return Err(Error::BeaconNodeSyncing);
|
||||
}
|
||||
|
||||
info!("Performing head update");
|
||||
let head_timer = Instant::now();
|
||||
watch.perform_head_update().await?;
|
||||
let head_timer_elapsed = head_timer.elapsed();
|
||||
debug!("Head update complete, time taken: {head_timer_elapsed:?}");
|
||||
|
||||
info!("Performing block backfill");
|
||||
let block_backfill_timer = Instant::now();
|
||||
watch.backfill_canonical_slots().await?;
|
||||
let block_backfill_timer_elapsed = block_backfill_timer.elapsed();
|
||||
debug!("Block backfill complete, time taken: {block_backfill_timer_elapsed:?}");
|
||||
|
||||
info!("Updating validator set");
|
||||
let validator_timer = Instant::now();
|
||||
watch.update_validator_set().await?;
|
||||
let validator_timer_elapsed = validator_timer.elapsed();
|
||||
debug!("Validator update complete, time taken: {validator_timer_elapsed:?}");
|
||||
|
||||
// Update blocks after updating the validator set since the `proposer_index` must exist in the
|
||||
// `validators` table.
|
||||
info!("Updating unknown blocks");
|
||||
let unknown_block_timer = Instant::now();
|
||||
watch.update_unknown_blocks().await?;
|
||||
let unknown_block_timer_elapsed = unknown_block_timer.elapsed();
|
||||
debug!("Unknown block update complete, time taken: {unknown_block_timer_elapsed:?}");
|
||||
|
||||
// Run additional modules
|
||||
if config.updater.attestations {
|
||||
info!("Updating suboptimal attestations");
|
||||
let attestation_timer = Instant::now();
|
||||
watch.fill_suboptimal_attestations().await?;
|
||||
watch.backfill_suboptimal_attestations().await?;
|
||||
let attestation_timer_elapsed = attestation_timer.elapsed();
|
||||
debug!("Attestation update complete, time taken: {attestation_timer_elapsed:?}");
|
||||
}
|
||||
|
||||
if config.updater.block_rewards {
|
||||
info!("Updating block rewards");
|
||||
let rewards_timer = Instant::now();
|
||||
watch.fill_block_rewards().await?;
|
||||
watch.backfill_block_rewards().await?;
|
||||
let rewards_timer_elapsed = rewards_timer.elapsed();
|
||||
debug!("Block Rewards update complete, time taken: {rewards_timer_elapsed:?}");
|
||||
}
|
||||
|
||||
if config.updater.block_packing {
|
||||
info!("Updating block packing statistics");
|
||||
let packing_timer = Instant::now();
|
||||
watch.fill_block_packing().await?;
|
||||
watch.backfill_block_packing().await?;
|
||||
let packing_timer_elapsed = packing_timer.elapsed();
|
||||
debug!("Block packing update complete, time taken: {packing_timer_elapsed:?}");
|
||||
}
|
||||
|
||||
if config.blockprint.enabled {
|
||||
info!("Updating blockprint");
|
||||
let blockprint_timer = Instant::now();
|
||||
watch.fill_blockprint().await?;
|
||||
watch.backfill_blockprint().await?;
|
||||
let blockprint_timer_elapsed = blockprint_timer.elapsed();
|
||||
debug!("Blockprint update complete, time taken: {blockprint_timer_elapsed:?}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the beacon node for a given `BlockId` and returns the `BeaconBlockHeader` if it exists.
|
||||
pub async fn get_header(
|
||||
bn: &BeaconNodeHttpClient,
|
||||
block_id: BlockId,
|
||||
) -> Result<Option<BeaconBlockHeader>, Error> {
|
||||
let resp = bn
|
||||
.get_beacon_headers_block_id(block_id)
|
||||
.await?
|
||||
.map(|resp| (resp.data.root, resp.data.header.message));
|
||||
// When quering with root == 0x000... , slot 0 will be returned with parent_root == 0x0000...
|
||||
// This check escapes the loop.
|
||||
if let Some((root, header)) = resp {
|
||||
if root == header.parent_root {
|
||||
return Ok(None);
|
||||
} else {
|
||||
return Ok(Some(header));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn get_beacon_block<T: EthSpec>(
|
||||
bn: &BeaconNodeHttpClient,
|
||||
block_id: BlockId,
|
||||
) -> Result<Option<SignedBeaconBlock<T>>, Error> {
|
||||
let block = bn.get_beacon_blocks(block_id).await?.map(|resp| resp.data);
|
||||
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
/// Queries the beacon node for the current validator set.
|
||||
pub async fn get_validators(bn: &BeaconNodeHttpClient) -> Result<HashSet<WatchValidator>, Error> {
|
||||
let mut validator_map = HashSet::new();
|
||||
|
||||
let validators = bn
|
||||
.get_beacon_states_validators(StateId::Head, None, None)
|
||||
.await?
|
||||
.ok_or(Error::NoValidatorsFound)?
|
||||
.data;
|
||||
|
||||
for val in validators {
|
||||
// Only store `activation_epoch` if it not the `FAR_FUTURE_EPOCH`.
|
||||
let activation_epoch = if val.validator.activation_epoch.as_u64() == FAR_FUTURE_EPOCH {
|
||||
None
|
||||
} else {
|
||||
Some(val.validator.activation_epoch.as_u64() as i32)
|
||||
};
|
||||
// Only store `exit_epoch` if it is not the `FAR_FUTURE_EPOCH`.
|
||||
let exit_epoch = if val.validator.exit_epoch.as_u64() == FAR_FUTURE_EPOCH {
|
||||
None
|
||||
} else {
|
||||
Some(val.validator.exit_epoch.as_u64() as i32)
|
||||
};
|
||||
validator_map.insert(WatchValidator {
|
||||
index: val.index as i32,
|
||||
public_key: WatchPK::from_pubkey(val.validator.pubkey),
|
||||
status: val.status.to_string(),
|
||||
activation_epoch,
|
||||
exit_epoch,
|
||||
});
|
||||
}
|
||||
Ok(validator_map)
|
||||
}
|
1254
watch/tests/tests.rs
Normal file
1254
watch/tests/tests.rs
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user