diff --git a/Cargo.lock b/Cargo.lock index a5c245b6a69..1c515550c1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -604,6 +604,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.9.1" @@ -1784,6 +1793,7 @@ dependencies = [ "atomic_refcell", "base64 0.21.7", "bigdecimal 0.1.2", + "bs58 0.5.1", "bytes", "chrono", "cid", @@ -2072,7 +2082,7 @@ version = "0.35.0" dependencies = [ "anyhow", "async-trait", - "bs58", + "bs58 0.4.0", "ethabi", "graph", "graph-runtime-derive", @@ -4895,7 +4905,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9922f437e6cb86b62cfd8bdede93937def710616ac2825ffff06b8770bbd06df" dependencies = [ - "bs58", + "bs58 0.4.0", "prost 0.11.9", "prost-build 0.11.9", "prost-types 0.11.9", diff --git a/Cargo.toml b/Cargo.toml index 57eb13c4698..5c4a4023962 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ anyhow = "1.0" async-graphql = { version = "7.0.11", features = ["chrono", "uuid"] } async-graphql-axum = "7.0.11" axum = "0.7.5" +bs58 = "0.5.1" chrono = "0.4.38" clap = { version = "4.5.4", features = ["derive", "env"] } derivative = "2.2.0" diff --git a/chain/arweave/src/chain.rs b/chain/arweave/src/chain.rs index 8d40408a463..f49611ddf93 100644 --- a/chain/arweave/src/chain.rs +++ b/chain/arweave/src/chain.rs @@ -6,7 +6,7 @@ use graph::blockchain::{ EmptyNodeCapabilities, NoopDecoderHook, NoopRuntimeAdapter, }; use graph::cheap_clone::CheapClone; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::components::store::DeploymentCursorTracker; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::env::EnvVars; @@ -42,7 +42,7 @@ use graph::blockchain::block_stream::{ pub struct Chain { logger_factory: LoggerFactory, - name: ChainId, + name: ChainName, client: Arc>, chain_store: Arc, metrics_registry: Arc, diff --git a/chain/cosmos/src/chain.rs b/chain/cosmos/src/chain.rs index 955aa7efc3c..bd6b66e55c6 100644 --- a/chain/cosmos/src/chain.rs +++ b/chain/cosmos/src/chain.rs @@ -1,6 +1,6 @@ use graph::blockchain::firehose_block_ingestor::FirehoseBlockIngestor; use graph::blockchain::{BlockIngestor, NoopDecoderHook}; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::env::EnvVars; use graph::prelude::MetricsRegistry; use graph::substreams::Clock; @@ -37,7 +37,7 @@ use crate::{codec, TriggerFilter}; pub struct Chain { logger_factory: LoggerFactory, - name: ChainId, + name: ChainName, client: Arc>, chain_store: Arc, metrics_registry: Arc, diff --git a/chain/ethereum/examples/firehose.rs b/chain/ethereum/examples/firehose.rs index b49cb71ac31..e5f85964fe1 100644 --- a/chain/ethereum/examples/firehose.rs +++ b/chain/ethereum/examples/firehose.rs @@ -2,7 +2,7 @@ use anyhow::Error; use graph::{ endpoint::EndpointMetrics, env::env_var, - firehose::{self, FirehoseEndpoint, NoopGenesisDecoder, SubgraphLimit}, + firehose::{self, FirehoseEndpoint, SubgraphLimit}, log::logger, prelude::{prost, tokio, tonic, MetricsRegistry}, }; @@ -38,7 +38,6 @@ async fn main() -> Result<(), Error> { false, SubgraphLimit::Unlimited, metrics, - NoopGenesisDecoder::boxed(), )); loop { diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 1def8c483cc..cf46a675212 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -5,7 +5,7 @@ use graph::blockchain::firehose_block_ingestor::{FirehoseBlockIngestor, Transfor use graph::blockchain::{ BlockIngestor, BlockTime, BlockchainKind, ChainIdentifier, TriggersAdapterSelector, }; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::components::store::DeploymentCursorTracker; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::firehose::{FirehoseEndpoint, ForkStep}; @@ -288,7 +288,7 @@ impl RuntimeAdapterBuilder for EthereumRuntimeAdapterBuilder { pub struct Chain { logger_factory: LoggerFactory, - pub name: ChainId, + pub name: ChainName, node_id: NodeId, registry: Arc, client: Arc>, @@ -315,7 +315,7 @@ impl Chain { /// Creates a new Ethereum [`Chain`]. pub fn new( logger_factory: LoggerFactory, - name: ChainId, + name: ChainName, node_id: NodeId, registry: Arc, chain_store: Arc, diff --git a/chain/ethereum/src/ingestor.rs b/chain/ethereum/src/ingestor.rs index d22e08c4294..e0fc8c5becd 100644 --- a/chain/ethereum/src/ingestor.rs +++ b/chain/ethereum/src/ingestor.rs @@ -2,7 +2,7 @@ use crate::{chain::BlockFinality, ENV_VARS}; use crate::{EthereumAdapter, EthereumAdapterTrait as _}; use graph::blockchain::client::ChainClient; use graph::blockchain::BlockchainKind; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::futures03::compat::Future01CompatExt as _; use graph::slog::o; use graph::util::backoff::ExponentialBackoff; @@ -22,7 +22,7 @@ pub struct PollingBlockIngestor { chain_client: Arc>, chain_store: Arc, polling_interval: Duration, - network_name: ChainId, + network_name: ChainName, } impl PollingBlockIngestor { @@ -32,7 +32,7 @@ impl PollingBlockIngestor { chain_client: Arc>, chain_store: Arc, polling_interval: Duration, - network_name: ChainId, + network_name: ChainName, ) -> Result { Ok(PollingBlockIngestor { logger, @@ -266,7 +266,7 @@ impl BlockIngestor for PollingBlockIngestor { } } - fn network_name(&self) -> ChainId { + fn network_name(&self) -> ChainName { self.network_name.clone() } diff --git a/chain/ethereum/src/network.rs b/chain/ethereum/src/network.rs index 0c236df33b0..43b5a04b63a 100644 --- a/chain/ethereum/src/network.rs +++ b/chain/ethereum/src/network.rs @@ -1,10 +1,14 @@ use anyhow::{anyhow, bail}; use graph::blockchain::ChainIdentifier; -use graph::components::adapter::{ChainId, NetIdentifiable, ProviderManager, ProviderName}; +use graph::components::network_provider::ChainName; +use graph::components::network_provider::NetworkDetails; +use graph::components::network_provider::ProviderManager; +use graph::components::network_provider::ProviderName; use graph::endpoint::EndpointMetrics; use graph::firehose::{AvailableCapacity, SubgraphLimit}; use graph::prelude::rand::seq::IteratorRandom; use graph::prelude::rand::{self, Rng}; +use itertools::Itertools; use std::sync::Arc; pub use graph::impl_slog_value; @@ -29,13 +33,18 @@ pub struct EthereumNetworkAdapter { } #[async_trait] -impl NetIdentifiable for EthereumNetworkAdapter { - async fn net_identifiers(&self) -> Result { - self.adapter.net_identifiers().await - } +impl NetworkDetails for EthereumNetworkAdapter { fn provider_name(&self) -> ProviderName { self.adapter.provider().into() } + + async fn chain_identifier(&self) -> Result { + self.adapter.net_identifiers().await + } + + async fn provides_extended_blocks(&self) -> Result { + Ok(true) + } } impl EthereumNetworkAdapter { @@ -72,7 +81,7 @@ impl EthereumNetworkAdapter { #[derive(Debug, Clone)] pub struct EthereumNetworkAdapters { - chain_id: ChainId, + chain_id: ChainName, manager: ProviderManager, call_only_adapters: Vec, // Percentage of request that should be used to retest errored adapters. @@ -96,10 +105,10 @@ impl EthereumNetworkAdapters { ) -> Self { use std::cmp::Ordering; + use graph::components::network_provider::ProviderCheckStrategy; use graph::slog::{o, Discard, Logger}; - use graph::components::adapter::NoopIdentValidator; - let chain_id: ChainId = "testing".into(); + let chain_id: ChainName = "testing".into(); adapters.sort_by(|a, b| { a.capabilities .partial_cmp(&b.capabilities) @@ -109,15 +118,14 @@ impl EthereumNetworkAdapters { let provider = ProviderManager::new( Logger::root(Discard, o!()), vec![(chain_id.clone(), adapters)].into_iter(), - Arc::new(NoopIdentValidator), + ProviderCheckStrategy::MarkAsValid, ); - provider.mark_all_valid().await; Self::new(chain_id, provider, call_only, None) } pub fn new( - chain_id: ChainId, + chain_id: ChainName, manager: ProviderManager, call_only_adapters: Vec, retest_percent: Option, @@ -159,8 +167,9 @@ impl EthereumNetworkAdapters { ) -> impl Iterator + '_ { let all = self .manager - .get_all(&self.chain_id) + .providers(&self.chain_id) .await + .map(|adapters| adapters.collect_vec()) .unwrap_or_default(); Self::available_with_capabilities(all, required_capabilities) @@ -172,7 +181,10 @@ impl EthereumNetworkAdapters { &self, required_capabilities: &NodeCapabilities, ) -> impl Iterator + '_ { - let all = self.manager.get_all_unverified(&self.chain_id); + let all = self + .manager + .providers_unchecked(&self.chain_id) + .collect_vec(); Self::available_with_capabilities(all, required_capabilities) } @@ -242,10 +254,10 @@ impl EthereumNetworkAdapters { // EthereumAdapters are sorted by their NodeCapabilities when the EthereumNetworks // struct is instantiated so they do not need to be sorted here self.manager - .get_all(&self.chain_id) + .providers(&self.chain_id) .await + .map(|mut adapters| adapters.next()) .unwrap_or_default() - .first() .map(|ethereum_network_adapter| ethereum_network_adapter.adapter.clone()) } @@ -299,7 +311,9 @@ impl EthereumNetworkAdapters { #[cfg(test)] mod tests { use graph::cheap_clone::CheapClone; - use graph::components::adapter::{NoopIdentValidator, ProviderManager, ProviderName}; + use graph::components::network_provider::ProviderCheckStrategy; + use graph::components::network_provider::ProviderManager; + use graph::components::network_provider::ProviderName; use graph::data::value::Word; use graph::http::HeaderMap; use graph::{ @@ -746,18 +760,14 @@ mod tests { .collect(), )] .into_iter(), - Arc::new(NoopIdentValidator), + ProviderCheckStrategy::MarkAsValid, ); - manager.mark_all_valid().await; - let no_retest_adapters = EthereumNetworkAdapters::new( - chain_id.clone(), - manager.cheap_clone(), - vec![], - Some(0f64), - ); + let no_retest_adapters = + EthereumNetworkAdapters::new(chain_id.clone(), manager.clone(), vec![], Some(0f64)); + let always_retest_adapters = - EthereumNetworkAdapters::new(chain_id, manager.cheap_clone(), vec![], Some(1f64)); + EthereumNetworkAdapters::new(chain_id, manager.clone(), vec![], Some(1f64)); assert_eq!( no_retest_adapters @@ -842,16 +852,12 @@ mod tests { .iter() .cloned() .map(|a| (chain_id.clone(), vec![a])), - Arc::new(NoopIdentValidator), + ProviderCheckStrategy::MarkAsValid, ); - manager.mark_all_valid().await; - let always_retest_adapters = EthereumNetworkAdapters::new( - chain_id.clone(), - manager.cheap_clone(), - vec![], - Some(1f64), - ); + let always_retest_adapters = + EthereumNetworkAdapters::new(chain_id.clone(), manager.clone(), vec![], Some(1f64)); + assert_eq!( always_retest_adapters .cheapest_with(&NodeCapabilities { @@ -870,9 +876,8 @@ mod tests { .iter() .cloned() .map(|a| (chain_id.clone(), vec![a])), - Arc::new(NoopIdentValidator), + ProviderCheckStrategy::MarkAsValid, ); - manager.mark_all_valid().await; let no_retest_adapters = EthereumNetworkAdapters::new(chain_id.clone(), manager, vec![], Some(0f64)); @@ -912,9 +917,8 @@ mod tests { no_available_adapter.iter().cloned().collect(), )] .into_iter(), - Arc::new(NoopIdentValidator), + ProviderCheckStrategy::MarkAsValid, ); - manager.mark_all_valid().await; let no_available_adapter = EthereumNetworkAdapters::new(chain_id, manager, vec![], None); let res = no_available_adapter diff --git a/chain/ethereum/src/transport.rs b/chain/ethereum/src/transport.rs index 1698b18e4ed..a90a6b9720b 100644 --- a/chain/ethereum/src/transport.rs +++ b/chain/ethereum/src/transport.rs @@ -1,4 +1,4 @@ -use graph::components::adapter::ProviderName; +use graph::components::network_provider::ProviderName; use graph::endpoint::{EndpointMetrics, RequestLabels}; use jsonrpc_core::types::Call; use jsonrpc_core::Value; diff --git a/chain/near/src/chain.rs b/chain/near/src/chain.rs index 283552e7f33..02c8e57d6a0 100644 --- a/chain/near/src/chain.rs +++ b/chain/near/src/chain.rs @@ -7,7 +7,7 @@ use graph::blockchain::{ NoopRuntimeAdapter, }; use graph::cheap_clone::CheapClone; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::components::store::DeploymentCursorTracker; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::env::EnvVars; @@ -161,7 +161,7 @@ impl BlockStreamBuilder for NearStreamBuilder { pub struct Chain { logger_factory: LoggerFactory, - name: ChainId, + name: ChainName, client: Arc>, chain_store: Arc, metrics_registry: Arc, diff --git a/chain/starknet/src/chain.rs b/chain/starknet/src/chain.rs index cd10af5f965..69406b7b1f6 100644 --- a/chain/starknet/src/chain.rs +++ b/chain/starknet/src/chain.rs @@ -1,3 +1,4 @@ +use graph::components::network_provider::ChainName; use graph::{ anyhow::Result, blockchain::{ @@ -14,10 +15,7 @@ use graph::{ RuntimeAdapter as RuntimeAdapterTrait, }, cheap_clone::CheapClone, - components::{ - adapter::ChainId, - store::{DeploymentCursorTracker, DeploymentLocator}, - }, + components::store::{DeploymentCursorTracker, DeploymentLocator}, data::subgraph::UnifiedMappingApiVersion, env::EnvVars, firehose::{self, FirehoseEndpoint, ForkStep}, @@ -43,7 +41,7 @@ use crate::{ pub struct Chain { logger_factory: LoggerFactory, - name: ChainId, + name: ChainName, client: Arc>, chain_store: Arc, metrics_registry: Arc, diff --git a/chain/substreams/examples/substreams.rs b/chain/substreams/examples/substreams.rs index 7377ed8585d..eac9397b893 100644 --- a/chain/substreams/examples/substreams.rs +++ b/chain/substreams/examples/substreams.rs @@ -3,7 +3,7 @@ use graph::blockchain::block_stream::{BlockStreamEvent, FirehoseCursor}; use graph::blockchain::client::ChainClient; use graph::blockchain::substreams_block_stream::SubstreamsBlockStream; use graph::endpoint::EndpointMetrics; -use graph::firehose::{FirehoseEndpoints, NoopGenesisDecoder, SubgraphLimit}; +use graph::firehose::{FirehoseEndpoints, SubgraphLimit}; use graph::prelude::{info, tokio, DeploymentHash, MetricsRegistry, Registry}; use graph::tokio_stream::StreamExt; use graph::{env::env_var, firehose::FirehoseEndpoint, log::logger, substreams}; @@ -57,7 +57,6 @@ async fn main() -> Result<(), Error> { false, SubgraphLimit::Unlimited, Arc::new(endpoint_metrics), - NoopGenesisDecoder::boxed(), )); let client = Arc::new(ChainClient::new_firehose(FirehoseEndpoints::for_testing( diff --git a/chain/substreams/src/block_ingestor.rs b/chain/substreams/src/block_ingestor.rs index eee86b21299..69b16ecc869 100644 --- a/chain/substreams/src/block_ingestor.rs +++ b/chain/substreams/src/block_ingestor.rs @@ -7,7 +7,7 @@ use graph::blockchain::BlockchainKind; use graph::blockchain::{ client::ChainClient, substreams_block_stream::SubstreamsBlockStream, BlockIngestor, }; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::prelude::MetricsRegistry; use graph::slog::trace; use graph::substreams::Package; @@ -29,7 +29,7 @@ pub struct SubstreamsBlockIngestor { chain_store: Arc, client: Arc>, logger: Logger, - chain_name: ChainId, + chain_name: ChainName, metrics: Arc, } @@ -38,7 +38,7 @@ impl SubstreamsBlockIngestor { chain_store: Arc, client: Arc>, logger: Logger, - chain_name: ChainId, + chain_name: ChainName, metrics: Arc, ) -> SubstreamsBlockIngestor { SubstreamsBlockIngestor { @@ -194,7 +194,7 @@ impl BlockIngestor for SubstreamsBlockIngestor { } } - fn network_name(&self) -> ChainId { + fn network_name(&self) -> ChainName { self.chain_name.clone() } fn kind(&self) -> BlockchainKind { diff --git a/chain/substreams/src/chain.rs b/chain/substreams/src/chain.rs index 28ef4bdc38b..d2efe6dec91 100644 --- a/chain/substreams/src/chain.rs +++ b/chain/substreams/src/chain.rs @@ -6,7 +6,7 @@ use graph::blockchain::{ BasicBlockchainBuilder, BlockIngestor, BlockTime, EmptyNodeCapabilities, NoopDecoderHook, NoopRuntimeAdapter, }; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::components::store::DeploymentCursorTracker; use graph::env::EnvVars; use graph::prelude::{BlockHash, CheapClone, Entity, LoggerFactory, MetricsRegistry}; @@ -67,7 +67,7 @@ impl blockchain::Block for Block { pub struct Chain { chain_store: Arc, block_stream_builder: Arc>, - chain_id: ChainId, + chain_id: ChainName, pub(crate) logger_factory: LoggerFactory, pub(crate) client: Arc>, @@ -81,7 +81,7 @@ impl Chain { metrics_registry: Arc, chain_store: Arc, block_stream_builder: Arc>, - chain_id: ChainId, + chain_id: ChainName, ) -> Self { Self { logger_factory, diff --git a/graph/Cargo.toml b/graph/Cargo.toml index 089d46bd9ec..3ea0c0bf349 100644 --- a/graph/Cargo.toml +++ b/graph/Cargo.toml @@ -14,6 +14,7 @@ atomic_refcell = "0.1.13" # make sure that it does not cause PoI changes old_bigdecimal = { version = "=0.1.2", features = ["serde"], package = "bigdecimal" } bytes = "1.0.1" +bs58 = { workspace = true } cid = "0.11.1" derivative = { workspace = true } graph_derive = { path = "./derive" } diff --git a/graph/proto/firehose.proto b/graph/proto/firehose.proto index a4101a83e18..5938737e2a1 100644 --- a/graph/proto/firehose.proto +++ b/graph/proto/firehose.proto @@ -14,28 +14,32 @@ service Fetch { rpc Block(SingleBlockRequest) returns (SingleBlockResponse); } +service EndpointInfo { + rpc Info(InfoRequest) returns (InfoResponse); +} + message SingleBlockRequest { // Get the current known canonical version of a block at with this number message BlockNumber{ - uint64 num=1; + uint64 num = 1; } // Get the current block with specific hash and number message BlockHashAndNumber{ - uint64 num=1; - string hash=2; + uint64 num = 1; + string hash = 2; } // Get the block that generated a specific cursor message Cursor{ - string cursor=1; + string cursor = 1; } oneof reference{ - BlockNumber block_number=3; - BlockHashAndNumber block_hash_and_number=4; - Cursor cursor=5; + BlockNumber block_number = 3; + BlockHashAndNumber block_hash_and_number = 4; + Cursor cursor = 5; } repeated google.protobuf.Any transforms = 6; @@ -108,3 +112,35 @@ enum ForkStep { // see chain documentation for more details) STEP_FINAL = 3; } + +message InfoRequest {} + +message InfoResponse { + // Canonical chain name from https://thegraph.com/docs/en/developing/supported-networks/ (ex: matic, mainnet ...). + string chain_name = 1; + + // Alternate names for the chain. + repeated string chain_name_aliases = 2; + + // First block that is served by this endpoint. + // This should usually be the genesis block, but some providers may have truncated history. + uint64 first_streamable_block_num = 3; + string first_streamable_block_id = 4; + + enum BlockIdEncoding { + BLOCK_ID_ENCODING_UNSET = 0; + BLOCK_ID_ENCODING_HEX = 1; + BLOCK_ID_ENCODING_0X_HEX = 2; + BLOCK_ID_ENCODING_BASE58 = 3; + BLOCK_ID_ENCODING_BASE64 = 4; + BLOCK_ID_ENCODING_BASE64URL = 5; + } + + // This informs the client on how to decode the `block_id` field inside the `Block` message + // as well as the `first_streamable_block_id` above. + BlockIdEncoding block_id_encoding = 5; + + // Features describes the blocks. + // Popular values for EVM chains include "base", "extended" or "hybrid". + repeated string block_features = 10; +} diff --git a/graph/src/blockchain/firehose_block_ingestor.rs b/graph/src/blockchain/firehose_block_ingestor.rs index b691179116d..026b83018d4 100644 --- a/graph/src/blockchain/firehose_block_ingestor.rs +++ b/graph/src/blockchain/firehose_block_ingestor.rs @@ -2,7 +2,7 @@ use std::{marker::PhantomData, sync::Arc, time::Duration}; use crate::{ blockchain::Block as BlockchainBlock, - components::{adapter::ChainId, store::ChainStore}, + components::store::ChainStore, firehose::{self, decode_firehose_block, HeaderOnly}, prelude::{error, info, Logger}, util::backoff::ExponentialBackoff, @@ -16,6 +16,7 @@ use slog::{o, trace}; use tonic::Streaming; use super::{client::ChainClient, BlockIngestor, Blockchain, BlockchainKind}; +use crate::components::network_provider::ChainName; const TRANSFORM_ETHEREUM_HEADER_ONLY: &str = "type.googleapis.com/sf.ethereum.transform.v1.HeaderOnly"; @@ -43,7 +44,7 @@ where client: Arc>, logger: Logger, default_transforms: Vec, - chain_name: ChainId, + chain_name: ChainName, phantom: PhantomData, } @@ -56,7 +57,7 @@ where chain_store: Arc, client: Arc>, logger: Logger, - chain_name: ChainId, + chain_name: ChainName, ) -> FirehoseBlockIngestor { FirehoseBlockIngestor { chain_store, @@ -226,7 +227,7 @@ where } } - fn network_name(&self) -> ChainId { + fn network_name(&self) -> ChainName { self.chain_name.clone() } diff --git a/graph/src/blockchain/mod.rs b/graph/src/blockchain/mod.rs index 73cac816728..d100decb9f0 100644 --- a/graph/src/blockchain/mod.rs +++ b/graph/src/blockchain/mod.rs @@ -18,7 +18,6 @@ mod types; use crate::{ cheap_clone::CheapClone, components::{ - adapter::ChainId, metrics::subgraph::SubgraphInstanceMetrics, store::{DeploymentCursorTracker, DeploymentLocator, StoredDynamicDataSource}, subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError}, @@ -58,11 +57,12 @@ use self::{ block_stream::{BlockStream, FirehoseCursor}, client::ChainClient, }; +use crate::components::network_provider::ChainName; #[async_trait] pub trait BlockIngestor: 'static + Send + Sync { async fn run(self: Box); - fn network_name(&self) -> ChainId; + fn network_name(&self) -> ChainName; fn kind(&self) -> BlockchainKind; } @@ -516,7 +516,7 @@ impl BlockchainKind { /// A collection of blockchains, keyed by `BlockchainKind` and network. #[derive(Default, Debug, Clone)] -pub struct BlockchainMap(HashMap<(BlockchainKind, ChainId), Arc>); +pub struct BlockchainMap(HashMap<(BlockchainKind, ChainName), Arc>); impl BlockchainMap { pub fn new() -> Self { @@ -525,11 +525,11 @@ impl BlockchainMap { pub fn iter( &self, - ) -> impl Iterator)> { + ) -> impl Iterator)> { self.0.iter() } - pub fn insert(&mut self, network: ChainId, chain: Arc) { + pub fn insert(&mut self, network: ChainName, chain: Arc) { self.0.insert((C::KIND, network), chain); } @@ -551,7 +551,7 @@ impl BlockchainMap { .collect::>, Error>>() } - pub fn get(&self, network: ChainId) -> Result, Error> { + pub fn get(&self, network: ChainName) -> Result, Error> { self.0 .get(&(C::KIND, network.clone())) .with_context(|| format!("no network {} found on chain {}", network, C::KIND))? diff --git a/graph/src/components/adapter.rs b/graph/src/components/adapter.rs deleted file mode 100644 index aaae5a89518..00000000000 --- a/graph/src/components/adapter.rs +++ /dev/null @@ -1,898 +0,0 @@ -use core::time; -use std::{ - collections::HashMap, - ops::{Add, Deref}, - sync::Arc, -}; - -use async_trait::async_trait; -use chrono::{DateTime, Duration, Utc}; - -use itertools::Itertools; -use slog::{o, warn, Discard, Logger}; -use thiserror::Error; - -use crate::{ - blockchain::{BlockHash, ChainIdentifier}, - cheap_clone::CheapClone, - data::value::Word, - prelude::error, - tokio::sync::RwLock, -}; - -use crate::components::store::{BlockStore as BlockStoreTrait, ChainStore as ChainStoreTrait}; - -const VALIDATION_ATTEMPT_TTL: Duration = Duration::minutes(5); - -#[derive(Debug, Error)] -pub enum ProviderManagerError { - #[error("unknown error {0}")] - Unknown(#[from] anyhow::Error), - #[error("provider {provider} on chain {chain_id} failed verification, expected ident {expected}, got {actual}")] - ProviderFailedValidation { - chain_id: ChainId, - provider: ProviderName, - expected: ChainIdentifier, - actual: ChainIdentifier, - }, - #[error("no providers available for chain {0}")] - NoProvidersAvailable(ChainId), - #[error("all providers for chain_id {0} have failed")] - AllProvidersFailed(ChainId), -} - -#[async_trait] -pub trait NetIdentifiable: Sync + Send { - async fn net_identifiers_with_timeout( - &self, - timeout: time::Duration, - ) -> Result { - tokio::time::timeout(timeout, async move { self.net_identifiers().await }).await? - } - async fn net_identifiers(&self) -> Result; - fn provider_name(&self) -> ProviderName; -} - -#[async_trait] -impl NetIdentifiable for Arc { - async fn net_identifiers(&self) -> Result { - self.as_ref().net_identifiers().await - } - fn provider_name(&self) -> ProviderName { - self.as_ref().provider_name() - } -} - -pub type ProviderName = Word; -pub type ChainId = Word; - -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)] -struct Ident { - provider: ProviderName, - chain_id: ChainId, -} - -#[derive(Error, Debug, Clone, PartialEq)] -pub enum IdentValidatorError { - #[error("database error: {0}")] - UnknownError(String), - #[error("Store ident wasn't set")] - UnsetIdent, - #[error("the net version for chain {chain_id} has changed from {store_net_version} to {chain_net_version} since the last time we ran")] - ChangedNetVersion { - chain_id: ChainId, - store_net_version: String, - chain_net_version: String, - }, - #[error("the genesis block hash for chain {chain_id} has changed from {store_hash} to {chain_hash} since the last time we ran")] - ChangedHash { - chain_id: ChainId, - store_hash: BlockHash, - chain_hash: BlockHash, - }, - #[error("unable to get store for chain {0}")] - UnavailableStore(ChainId), -} - -impl From for IdentValidatorError { - fn from(value: anyhow::Error) -> Self { - Self::from(&value) - } -} - -impl From<&anyhow::Error> for IdentValidatorError { - fn from(value: &anyhow::Error) -> Self { - IdentValidatorError::UnknownError(value.to_string()) - } -} - -#[async_trait] -/// IdentValidator validates that the provided chain ident matches the expected value for a certain -/// chain_id. This is probably only going to matter for the ChainStore but this allows us to decouple -/// the all the trait bounds and database integration from the ProviderManager and tests. -pub trait IdentValidator: Sync + Send { - fn check_ident( - &self, - chain_id: &ChainId, - ident: &ChainIdentifier, - ) -> Result<(), IdentValidatorError>; - - fn update_ident( - &self, - chain_id: &ChainId, - ident: &ChainIdentifier, - ) -> Result<(), anyhow::Error>; -} - -impl> IdentValidator for B { - fn check_ident( - &self, - chain_id: &ChainId, - ident: &ChainIdentifier, - ) -> Result<(), IdentValidatorError> { - let network_chain = self - .chain_store(&chain_id) - .ok_or_else(|| IdentValidatorError::UnavailableStore(chain_id.clone()))?; - let store_ident = network_chain - .chain_identifier() - .map_err(IdentValidatorError::from)?; - - if store_ident == ChainIdentifier::default() { - return Err(IdentValidatorError::UnsetIdent); - } - - if store_ident.net_version != ident.net_version { - // This behavior is preserved from the previous implementation, firehose does not provide - // a net_version so switching to and from firehose will cause this value to be different. - // we prioritise rpc when creating the chain but it's possible that it is created by firehose - // firehose always return 0 on net_version so we need to allow switching between the two. - if store_ident.net_version != "0" && ident.net_version != "0" { - return Err(IdentValidatorError::ChangedNetVersion { - chain_id: chain_id.clone(), - store_net_version: store_ident.net_version.clone(), - chain_net_version: ident.net_version.clone(), - }); - } - } - - let store_hash = &store_ident.genesis_block_hash; - let chain_hash = &ident.genesis_block_hash; - if store_hash != chain_hash { - return Err(IdentValidatorError::ChangedHash { - chain_id: chain_id.clone(), - store_hash: store_hash.clone(), - chain_hash: chain_hash.clone(), - }); - } - - return Ok(()); - } - - fn update_ident( - &self, - chain_id: &ChainId, - ident: &ChainIdentifier, - ) -> Result<(), anyhow::Error> { - let network_chain = self - .chain_store(&chain_id) - .ok_or_else(|| IdentValidatorError::UnavailableStore(chain_id.clone()))?; - - network_chain.set_chain_identifier(ident)?; - - Ok(()) - } -} - -/// This is mostly used for testing or for running with disabled genesis validation. -pub struct NoopIdentValidator; - -impl IdentValidator for NoopIdentValidator { - fn check_ident( - &self, - _chain_id: &ChainId, - _ident: &ChainIdentifier, - ) -> Result<(), IdentValidatorError> { - Ok(()) - } - - fn update_ident( - &self, - _chain_id: &ChainId, - _ident: &ChainIdentifier, - ) -> Result<(), anyhow::Error> { - Ok(()) - } -} - -/// ProviderCorrectness will maintain a list of providers which have had their -/// ChainIdentifiers checked. The first identifier is considered correct, if a later -/// provider for the same chain offers a different ChainIdentifier, this will be considered a -/// failed validation and it will be disabled. -#[derive(Clone, Debug)] -pub struct ProviderManager { - inner: Arc>, -} - -impl CheapClone for ProviderManager { - fn cheap_clone(&self) -> Self { - Self { - inner: self.inner.cheap_clone(), - } - } -} - -impl Default for ProviderManager { - fn default() -> Self { - Self { - inner: Arc::new(Inner { - logger: Logger::root(Discard, o!()), - adapters: HashMap::default(), - status: vec![], - validator: Arc::new(NoopIdentValidator {}), - }), - } - } -} - -impl ProviderManager { - pub fn new( - logger: Logger, - adapters: impl Iterator)>, - validator: Arc, - ) -> Self { - let mut status: Vec<(Ident, RwLock)> = Vec::new(); - - let adapters = HashMap::from_iter(adapters.map(|(chain_id, adapters)| { - let adapters = adapters - .into_iter() - .map(|adapter| { - let name = adapter.provider_name(); - - // Get status index or add new status. - let index = match status - .iter() - .find_position(|(ident, _)| ident.provider.eq(&name)) - { - Some((index, _)) => index, - None => { - status.push(( - Ident { - provider: name, - chain_id: chain_id.clone(), - }, - RwLock::new(GenesisCheckStatus::NotChecked), - )); - status.len() - 1 - } - }; - (index, adapter) - }) - .collect_vec(); - - (chain_id, adapters) - })); - - Self { - inner: Arc::new(Inner { - logger, - adapters, - status, - validator, - }), - } - } - - pub fn len(&self, chain_id: &ChainId) -> usize { - self.inner - .adapters - .get(chain_id) - .map(|a| a.len()) - .unwrap_or_default() - } - - pub async fn mark_all_valid(&self) { - for (_, status) in self.inner.status.iter() { - let mut s = status.write().await; - *s = GenesisCheckStatus::Valid; - } - } - - async fn verify(&self, adapters: &Vec<(usize, T)>) -> Result<(), ProviderManagerError> { - let mut tasks = vec![]; - - for (index, adapter) in adapters.into_iter() { - let inner = self.inner.cheap_clone(); - let adapter = adapter.clone(); - let index = *index; - tasks.push(inner.verify_provider(index, adapter)); - } - - crate::futures03::future::join_all(tasks) - .await - .into_iter() - .collect::, ProviderManagerError>>()?; - - Ok(()) - } - - /// get_all_unverified it's an escape hatch for places where checking the adapter status is - /// undesirable or just can't be done because async can't be used. This function just returns - /// the stored adapters and doesn't try to perform any verification. It will also return - /// adapters that failed verification. For the most part this should be fine since ideally - /// get_all would have been used before. Nevertheless, it is possible that a misconfigured - /// adapter is returned from this list even after validation. - pub fn get_all_unverified(&self, chain_id: &ChainId) -> Vec<&T> { - self.inner - .adapters - .get(chain_id) - .map(|v| v.iter().map(|v| &v.1).collect()) - .unwrap_or_default() - } - - /// get_all will trigger the verification of the endpoints for the provided chain_id, hence the - /// async. If this is undesirable, check `get_all_unverified` as an alternatives that does not - /// cause the validation but also doesn't not guaratee any adapters have been validated. - pub async fn get_all(&self, chain_id: &ChainId) -> Result, ProviderManagerError> { - tokio::time::timeout(std::time::Duration::from_secs(5), async move { - let adapters = match self.inner.adapters.get(chain_id) { - Some(adapters) if !adapters.is_empty() => adapters, - _ => return Ok(vec![]), - }; - - // Optimistic check - if self.inner.is_all_verified(&adapters).await { - return Ok(adapters.iter().map(|v| &v.1).collect()); - } - - match self.verify(adapters).await { - Ok(_) => {} - Err(error) => error!( - self.inner.logger, - "unable to verify genesis for adapter: {}", - error.to_string() - ), - } - - self.inner.get_verified_for_chain(&chain_id).await - }) - .await - .map_err(|_| crate::anyhow::anyhow!("timed out, validation took too long"))? - } -} - -struct Inner { - logger: Logger, - // Most operations start by getting the value so we keep track of the index to minimize the - // locked surface. - adapters: HashMap>, - // Status per (ChainId, ProviderName) pair. The RwLock here helps prevent multiple concurrent - // checks for the same provider, when one provider is being checked, all other uses will wait, - // this is correct because no provider should be used until they have been validated. - // There shouldn't be many values here so Vec is fine even if less ergonomic, because we track - // the index alongside the adapter it should be O(1) after initialization. - status: Vec<(Ident, RwLock)>, - // Validator used to compare the existing identifier to the one returned by an adapter. - validator: Arc, -} - -impl std::fmt::Debug for Inner { - fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) - } -} - -impl Inner { - async fn is_all_verified(&self, adapters: &Vec<(usize, T)>) -> bool { - for (index, _) in adapters.iter() { - let status = self.status.get(*index).unwrap().1.read().await; - if *status != GenesisCheckStatus::Valid { - return false; - } - } - - true - } - - /// Returns any adapters that have been validated, empty if none are defined or an error if - /// all adapters have failed or are unavailable, returns different errors for these use cases - /// so that that caller can handle the different situations, as one is permanent and the other - /// is retryable. - async fn get_verified_for_chain( - &self, - chain_id: &ChainId, - ) -> Result, ProviderManagerError> { - let mut out = vec![]; - let adapters = match self.adapters.get(chain_id) { - Some(adapters) if !adapters.is_empty() => adapters, - _ => return Ok(vec![]), - }; - - let mut failed = 0; - for (index, adapter) in adapters.iter() { - let status = self.status.get(*index).unwrap().1.read().await; - match status.deref() { - GenesisCheckStatus::Valid => {} - GenesisCheckStatus::Failed => { - failed += 1; - continue; - } - GenesisCheckStatus::NotChecked | GenesisCheckStatus::TemporaryFailure { .. } => { - continue - } - } - out.push(adapter); - } - - if out.is_empty() { - if failed == adapters.len() { - return Err(ProviderManagerError::AllProvidersFailed(chain_id.clone())); - } - - return Err(ProviderManagerError::NoProvidersAvailable(chain_id.clone())); - } - - Ok(out) - } - - async fn get_ident_status(&self, index: usize) -> (Ident, GenesisCheckStatus) { - match self.status.get(index) { - Some(status) => (status.0.clone(), status.1.read().await.clone()), - None => (Ident::default(), GenesisCheckStatus::Failed), - } - } - - fn ttl_has_elapsed(checked_at: &DateTime) -> bool { - checked_at.add(VALIDATION_ATTEMPT_TTL) < Utc::now() - } - - fn should_verify(status: &GenesisCheckStatus) -> bool { - match status { - GenesisCheckStatus::TemporaryFailure { checked_at } - if Self::ttl_has_elapsed(checked_at) => - { - true - } - // Let check the provider - GenesisCheckStatus::NotChecked => true, - _ => false, - } - } - - async fn verify_provider( - self: Arc>, - index: usize, - adapter: T, - ) -> Result<(), ProviderManagerError> { - let (ident, status) = self.get_ident_status(index).await; - if !Self::should_verify(&status) { - return Ok(()); - } - - let mut status = self.status.get(index).unwrap().1.write().await; - // double check nothing has changed. - if !Self::should_verify(&status) { - return Ok(()); - } - - let chain_ident = match adapter.net_identifiers().await { - Ok(ident) => ident, - Err(err) => { - error!( - &self.logger, - "failed to get net identifiers: {}", - err.to_string() - ); - *status = GenesisCheckStatus::TemporaryFailure { - checked_at: Utc::now(), - }; - - return Err(err.into()); - } - }; - - match self.validator.check_ident(&ident.chain_id, &chain_ident) { - Ok(_) => { - *status = GenesisCheckStatus::Valid; - } - Err(err) => match err { - IdentValidatorError::UnsetIdent => { - self.validator - .update_ident(&ident.chain_id, &chain_ident) - .map_err(ProviderManagerError::from)?; - *status = GenesisCheckStatus::Valid; - } - IdentValidatorError::ChangedNetVersion { - chain_id, - store_net_version, - chain_net_version, - } if store_net_version == "0" => { - warn!(self.logger, - "the net version for chain {} has changed from 0 to {} since the last time we ran, ignoring difference because 0 means UNSET and firehose does not provide it", - chain_id, - chain_net_version, - ); - *status = GenesisCheckStatus::Valid; - } - IdentValidatorError::ChangedNetVersion { - store_net_version, - chain_net_version, - .. - } => { - *status = GenesisCheckStatus::Failed; - return Err(ProviderManagerError::ProviderFailedValidation { - provider: ident.provider, - expected: ChainIdentifier { - net_version: store_net_version, - genesis_block_hash: chain_ident.genesis_block_hash.clone(), - }, - actual: ChainIdentifier { - net_version: chain_net_version, - genesis_block_hash: chain_ident.genesis_block_hash, - }, - chain_id: ident.chain_id.clone(), - }); - } - IdentValidatorError::ChangedHash { - store_hash, - chain_hash, - .. - } => { - *status = GenesisCheckStatus::Failed; - return Err(ProviderManagerError::ProviderFailedValidation { - provider: ident.provider, - expected: ChainIdentifier { - net_version: chain_ident.net_version.clone(), - genesis_block_hash: store_hash, - }, - actual: ChainIdentifier { - net_version: chain_ident.net_version, - genesis_block_hash: chain_hash, - }, - chain_id: ident.chain_id.clone(), - }); - } - e @ IdentValidatorError::UnavailableStore(_) - | e @ IdentValidatorError::UnknownError(_) => { - *status = GenesisCheckStatus::TemporaryFailure { - checked_at: Utc::now(), - }; - - return Err(ProviderManagerError::Unknown(crate::anyhow::anyhow!( - e.to_string() - ))); - } - }, - } - - Ok(()) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum GenesisCheckStatus { - NotChecked, - TemporaryFailure { checked_at: DateTime }, - Valid, - Failed, -} - -#[cfg(test)] -mod test { - use std::{ - ops::Sub, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - }; - - use crate::{ - bail, - blockchain::BlockHash, - components::adapter::{ChainId, GenesisCheckStatus, NoopIdentValidator}, - data::value::Word, - prelude::lazy_static, - }; - use async_trait::async_trait; - use chrono::{Duration, Utc}; - use ethabi::ethereum_types::H256; - use slog::{o, Discard, Logger}; - - use crate::{blockchain::ChainIdentifier, components::adapter::ProviderManagerError}; - - use super::{ - IdentValidator, IdentValidatorError, NetIdentifiable, ProviderManager, ProviderName, - VALIDATION_ATTEMPT_TTL, - }; - - const TEST_CHAIN_ID: &str = "valid"; - - lazy_static! { - static ref UNTESTABLE_ADAPTER: MockAdapter = - MockAdapter{ - provider: "untestable".into(), - status: GenesisCheckStatus::TemporaryFailure { checked_at: Utc::now()}, - }; - - // way past TTL, ready to check again - static ref TESTABLE_ADAPTER: MockAdapter = - MockAdapter{ - provider: "testable".into(), - status: GenesisCheckStatus::TemporaryFailure { checked_at: Utc::now().sub(Duration::seconds(10000000)) }, - }; - static ref VALID_ADAPTER: MockAdapter = MockAdapter {provider: "valid".into(), status: GenesisCheckStatus::Valid,}; - static ref FAILED_ADAPTER: MockAdapter = MockAdapter {provider: "FAILED".into(), status: GenesisCheckStatus::Failed,}; - static ref NEW_CHAIN_IDENT: ChainIdentifier =ChainIdentifier { net_version: "123".to_string(), genesis_block_hash: BlockHash::from( H256::repeat_byte(1))}; - } - - struct TestValidator { - check_result: Result<(), IdentValidatorError>, - expected_new_ident: Option, - } - - impl IdentValidator for TestValidator { - fn check_ident( - &self, - _chain_id: &ChainId, - _ident: &ChainIdentifier, - ) -> Result<(), IdentValidatorError> { - self.check_result.clone() - } - - fn update_ident( - &self, - _chain_id: &ChainId, - ident: &ChainIdentifier, - ) -> Result<(), anyhow::Error> { - match self.expected_new_ident.as_ref() { - None => unreachable!("unexpected call to update_ident"), - Some(ident_expected) if ident_expected.eq(ident) => Ok(()), - Some(_) => bail!("update_ident called with unexpected value"), - } - } - } - - #[derive(Clone, PartialEq, Eq, Debug)] - struct MockAdapter { - provider: Word, - status: GenesisCheckStatus, - } - - #[async_trait] - impl NetIdentifiable for MockAdapter { - async fn net_identifiers(&self) -> Result { - match self.status { - GenesisCheckStatus::TemporaryFailure { checked_at } - if checked_at > Utc::now().sub(VALIDATION_ATTEMPT_TTL) => - { - unreachable!("should never check if ttl has not elapsed"); - } - _ => Ok(NEW_CHAIN_IDENT.clone()), - } - } - - fn provider_name(&self) -> ProviderName { - self.provider.clone() - } - } - - #[tokio::test] - async fn test_provider_manager() { - struct Case<'a> { - name: &'a str, - chain_id: &'a str, - adapters: Vec<(ChainId, Vec)>, - validator: Option, - expected: Result, ProviderManagerError>, - } - - let cases = vec![ - Case { - name: "no adapters", - chain_id: TEST_CHAIN_ID, - adapters: vec![], - validator: None, - expected: Ok(vec![]), - }, - Case { - name: "no adapters", - chain_id: TEST_CHAIN_ID, - adapters: vec![(TEST_CHAIN_ID.into(), vec![TESTABLE_ADAPTER.clone()])], - validator: Some(TestValidator { - check_result: Err(IdentValidatorError::UnsetIdent), - expected_new_ident: Some(NEW_CHAIN_IDENT.clone()), - }), - expected: Ok(vec![&TESTABLE_ADAPTER]), - }, - Case { - name: "adapter temporary failure with Ident unset", - chain_id: TEST_CHAIN_ID, - // UNTESTABLE_ADAPTER has failed ident, will be valid cause idents has None value - adapters: vec![(TEST_CHAIN_ID.into(), vec![UNTESTABLE_ADAPTER.clone()])], - validator: None, - expected: Err(ProviderManagerError::NoProvidersAvailable( - TEST_CHAIN_ID.into(), - )), - }, - Case { - name: "adapter temporary failure", - chain_id: TEST_CHAIN_ID, - adapters: vec![(TEST_CHAIN_ID.into(), vec![UNTESTABLE_ADAPTER.clone()])], - validator: None, - expected: Err(ProviderManagerError::NoProvidersAvailable( - TEST_CHAIN_ID.into(), - )), - }, - Case { - name: "wrong chain ident", - chain_id: TEST_CHAIN_ID, - adapters: vec![(TEST_CHAIN_ID.into(), vec![FAILED_ADAPTER.clone()])], - validator: Some(TestValidator { - check_result: Err(IdentValidatorError::ChangedNetVersion { - chain_id: TEST_CHAIN_ID.into(), - store_net_version: "".to_string(), - chain_net_version: "".to_string(), - }), - expected_new_ident: None, - }), - expected: Err(ProviderManagerError::AllProvidersFailed( - TEST_CHAIN_ID.into(), - )), - }, - Case { - name: "all adapters ok or not checkable yet", - chain_id: TEST_CHAIN_ID, - adapters: vec![( - TEST_CHAIN_ID.into(), - vec![VALID_ADAPTER.clone(), FAILED_ADAPTER.clone()], - )], - // if a check is performed (which it shouldn't) the test will fail - validator: Some(TestValidator { - check_result: Err(IdentValidatorError::ChangedNetVersion { - chain_id: TEST_CHAIN_ID.into(), - store_net_version: "".to_string(), - chain_net_version: "".to_string(), - }), - expected_new_ident: None, - }), - expected: Ok(vec![&VALID_ADAPTER]), - }, - Case { - name: "all adapters ok or checkable", - chain_id: TEST_CHAIN_ID, - adapters: vec![( - TEST_CHAIN_ID.into(), - vec![VALID_ADAPTER.clone(), TESTABLE_ADAPTER.clone()], - )], - validator: None, - expected: Ok(vec![&VALID_ADAPTER, &TESTABLE_ADAPTER]), - }, - ]; - - for case in cases.into_iter() { - let Case { - name, - chain_id, - adapters, - validator, - expected, - } = case; - - let logger = Logger::root(Discard, o!()); - let chain_id = chain_id.into(); - - let validator: Arc = match validator { - None => Arc::new(NoopIdentValidator {}), - Some(validator) => Arc::new(validator), - }; - - let manager = ProviderManager::new(logger, adapters.clone().into_iter(), validator); - - for (_, adapters) in adapters.iter() { - for adapter in adapters.iter() { - let provider = adapter.provider.clone(); - let slot = manager - .inner - .status - .iter() - .find(|(ident, _)| ident.provider.eq(&provider)) - .expect(&format!( - "case: {} - there should be a status for provider \"{}\"", - name, provider - )); - let mut s = slot.1.write().await; - *s = adapter.status.clone(); - } - } - - let result = manager.get_all(&chain_id).await; - match (expected, result) { - (Ok(expected), Ok(result)) => assert_eq!( - expected, result, - "case {} failed. Result: {:?}", - name, result - ), - (Err(expected), Err(result)) => assert_eq!( - expected.to_string(), - result.to_string(), - "case {} failed. Result: {:?}", - name, - result - ), - (Ok(expected), Err(result)) => panic!( - "case {} failed. Result: {}, Expected: {:?}", - name, result, expected - ), - (Err(expected), Ok(result)) => panic!( - "case {} failed. Result: {:?}, Expected: {}", - name, result, expected - ), - } - } - } - - #[tokio::test] - async fn test_provider_manager_updates_on_unset() { - #[derive(Clone, Debug, Eq, PartialEq)] - struct MockAdapter {} - - #[async_trait] - impl NetIdentifiable for MockAdapter { - async fn net_identifiers(&self) -> Result { - Ok(NEW_CHAIN_IDENT.clone()) - } - fn provider_name(&self) -> ProviderName { - TEST_CHAIN_ID.into() - } - } - - struct TestValidator { - called: AtomicBool, - err: IdentValidatorError, - } - - impl IdentValidator for TestValidator { - fn check_ident( - &self, - _chain_id: &ChainId, - _ident: &ChainIdentifier, - ) -> Result<(), IdentValidatorError> { - Err(self.err.clone()) - } - - fn update_ident( - &self, - _chain_id: &ChainId, - ident: &ChainIdentifier, - ) -> Result<(), anyhow::Error> { - if NEW_CHAIN_IDENT.eq(ident) { - self.called.store(true, Ordering::SeqCst); - return Ok(()); - } - - unreachable!("unexpected call to update_ident ot unexpected ident passed"); - } - } - - let logger = Logger::root(Discard, o!()); - let chain_id = TEST_CHAIN_ID.into(); - - // Ensure the provider updates the chain ident when it wasn't set yet. - let validator = Arc::new(TestValidator { - called: AtomicBool::default(), - err: IdentValidatorError::UnsetIdent, - }); - let adapter = MockAdapter {}; - - let manager = ProviderManager::new( - logger, - vec![(TEST_CHAIN_ID.into(), vec![adapter.clone()])].into_iter(), - validator.clone(), - ); - - let mut result = manager.get_all(&chain_id).await.unwrap(); - assert_eq!(result.len(), 1); - assert_eq!(&adapter, result.pop().unwrap()); - assert_eq!(validator.called.load(Ordering::SeqCst), true); - } -} diff --git a/graph/src/components/mod.rs b/graph/src/components/mod.rs index ad6480d1d0e..8abdc96f0b0 100644 --- a/graph/src/components/mod.rs +++ b/graph/src/components/mod.rs @@ -60,8 +60,6 @@ pub mod metrics; /// Components dealing with versioning pub mod versions; -pub mod adapter; - /// A component that receives events of type `T`. pub trait EventConsumer { /// Get the event sink. @@ -80,4 +78,5 @@ pub trait EventProducer { fn take_event_stream(&mut self) -> Option + Send>>; } +pub mod network_provider; pub mod transaction_receipt; diff --git a/graph/src/components/network_provider/chain_identifier_store.rs b/graph/src/components/network_provider/chain_identifier_store.rs new file mode 100644 index 00000000000..e6a4f916206 --- /dev/null +++ b/graph/src/components/network_provider/chain_identifier_store.rs @@ -0,0 +1,121 @@ +use anyhow::anyhow; +use thiserror::Error; + +use crate::blockchain::BlockHash; +use crate::blockchain::ChainIdentifier; +use crate::components::network_provider::ChainName; +use crate::components::store::BlockStore; +use crate::components::store::ChainStore; + +/// Additional requirements for stores that are necessary for provider checks. +pub trait ChainIdentifierStore: Send + Sync + 'static { + /// Verifies that the chain identifier returned by the network provider + /// matches the previously stored value. + /// + /// Fails if the identifiers do not match or if something goes wrong. + fn validate_identifier( + &self, + chain_name: &ChainName, + chain_identifier: &ChainIdentifier, + ) -> Result<(), ChainIdentifierStoreError>; + + /// Saves the provided identifier that will be used as the source of truth + /// for future validations. + fn update_identifier( + &self, + chain_name: &ChainName, + chain_identifier: &ChainIdentifier, + ) -> Result<(), ChainIdentifierStoreError>; +} + +#[derive(Debug, Error)] +pub enum ChainIdentifierStoreError { + #[error("identifier not set for chain '{0}'")] + IdentifierNotSet(ChainName), + + #[error("net version mismatch on chain '{chain_name}'; expected '{store_net_version}', found '{chain_net_version}'")] + NetVersionMismatch { + chain_name: ChainName, + store_net_version: String, + chain_net_version: String, + }, + + #[error("genesis block hash mismatch on chain '{chain_name}'; expected '{store_genesis_block_hash}', found '{chain_genesis_block_hash}'")] + GenesisBlockHashMismatch { + chain_name: ChainName, + store_genesis_block_hash: BlockHash, + chain_genesis_block_hash: BlockHash, + }, + + #[error("store error: {0:#}")] + Store(#[source] anyhow::Error), +} + +impl ChainIdentifierStore for B +where + C: ChainStore, + B: BlockStore, +{ + fn validate_identifier( + &self, + chain_name: &ChainName, + chain_identifier: &ChainIdentifier, + ) -> Result<(), ChainIdentifierStoreError> { + let chain_store = self.chain_store(&chain_name).ok_or_else(|| { + ChainIdentifierStoreError::Store(anyhow!( + "unable to get store for chain '{chain_name}'" + )) + })?; + + let store_identifier = chain_store + .chain_identifier() + .map_err(|err| ChainIdentifierStoreError::Store(err))?; + + if store_identifier.is_default() { + return Err(ChainIdentifierStoreError::IdentifierNotSet( + chain_name.clone(), + )); + } + + if store_identifier.net_version != chain_identifier.net_version { + // This behavior is carried over from the previous implementation. + // Firehose does not provide a `net_version`, so switching to and from Firehose will + // cause this value to be different. We prioritize RPC when creating the chain, + // but it's possible that it will be created by Firehose. Firehose always returns "0" + // for `net_version`, so we need to allow switching between the two. + if store_identifier.net_version != "0" && chain_identifier.net_version != "0" { + return Err(ChainIdentifierStoreError::NetVersionMismatch { + chain_name: chain_name.clone(), + store_net_version: store_identifier.net_version, + chain_net_version: chain_identifier.net_version.clone(), + }); + } + } + + if store_identifier.genesis_block_hash != chain_identifier.genesis_block_hash { + return Err(ChainIdentifierStoreError::GenesisBlockHashMismatch { + chain_name: chain_name.clone(), + store_genesis_block_hash: store_identifier.genesis_block_hash, + chain_genesis_block_hash: chain_identifier.genesis_block_hash.clone(), + }); + } + + Ok(()) + } + + fn update_identifier( + &self, + chain_name: &ChainName, + chain_identifier: &ChainIdentifier, + ) -> Result<(), ChainIdentifierStoreError> { + let chain_store = self.chain_store(&chain_name).ok_or_else(|| { + ChainIdentifierStoreError::Store(anyhow!( + "unable to get store for chain '{chain_name}'" + )) + })?; + + chain_store + .set_chain_identifier(chain_identifier) + .map_err(|err| ChainIdentifierStoreError::Store(err)) + } +} diff --git a/graph/src/components/network_provider/extended_blocks_check.rs b/graph/src/components/network_provider/extended_blocks_check.rs new file mode 100644 index 00000000000..059cc43fa08 --- /dev/null +++ b/graph/src/components/network_provider/extended_blocks_check.rs @@ -0,0 +1,235 @@ +use std::collections::HashSet; +use std::time::Instant; + +use async_trait::async_trait; +use slog::error; +use slog::warn; +use slog::Logger; + +use crate::components::network_provider::ChainName; +use crate::components::network_provider::NetworkDetails; +use crate::components::network_provider::ProviderCheck; +use crate::components::network_provider::ProviderCheckStatus; +use crate::components::network_provider::ProviderName; + +/// Requires providers to support extended block details. +pub struct ExtendedBlocksCheck { + disabled_for_chains: HashSet, +} + +impl ExtendedBlocksCheck { + pub fn new(disabled_for_chains: impl IntoIterator) -> Self { + Self { + disabled_for_chains: disabled_for_chains.into_iter().collect(), + } + } +} + +#[async_trait] +impl ProviderCheck for ExtendedBlocksCheck { + fn name(&self) -> &'static str { + "ExtendedBlocksCheck" + } + + async fn check( + &self, + logger: &Logger, + chain_name: &ChainName, + provider_name: &ProviderName, + adapter: &dyn NetworkDetails, + ) -> ProviderCheckStatus { + if self.disabled_for_chains.contains(chain_name) { + warn!( + logger, + "Extended blocks check for provider '{}' was disabled on chain '{}'", + provider_name, + chain_name, + ); + + return ProviderCheckStatus::Valid; + } + + match adapter.provides_extended_blocks().await { + Ok(true) => ProviderCheckStatus::Valid, + Ok(false) => { + let message = format!( + "Provider '{}' does not support extended blocks on chain '{}'", + provider_name, chain_name, + ); + + error!(logger, "{}", message); + + ProviderCheckStatus::Failed { message } + } + Err(err) => { + let message = format!( + "Failed to check if provider '{}' supports extended blocks on chain '{}': {:#}", + provider_name, chain_name, err, + ); + + error!(logger, "{}", message); + + ProviderCheckStatus::TemporaryFailure { + checked_at: Instant::now(), + message, + } + } + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Mutex; + + use anyhow::anyhow; + use anyhow::Result; + + use super::*; + use crate::blockchain::ChainIdentifier; + use crate::log::discard; + + #[derive(Default)] + struct TestAdapter { + provides_extended_blocks_calls: Mutex>>, + } + + impl TestAdapter { + fn provides_extended_blocks_call(&self, x: Result) { + self.provides_extended_blocks_calls.lock().unwrap().push(x) + } + } + + impl Drop for TestAdapter { + fn drop(&mut self) { + assert!(self + .provides_extended_blocks_calls + .lock() + .unwrap() + .is_empty()); + } + } + + #[async_trait] + impl NetworkDetails for TestAdapter { + fn provider_name(&self) -> ProviderName { + unimplemented!(); + } + + async fn chain_identifier(&self) -> Result { + unimplemented!(); + } + + async fn provides_extended_blocks(&self) -> Result { + self.provides_extended_blocks_calls + .lock() + .unwrap() + .remove(0) + } + } + + #[tokio::test] + async fn check_valid_when_disabled_for_chain() { + let check = ExtendedBlocksCheck::new(["chain-1".into()]); + let adapter = TestAdapter::default(); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert_eq!(status, ProviderCheckStatus::Valid); + } + + #[tokio::test] + async fn check_valid_when_disabled_for_multiple_chains() { + let check = ExtendedBlocksCheck::new(["chain-1".into(), "chain-2".into()]); + let adapter = TestAdapter::default(); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert_eq!(status, ProviderCheckStatus::Valid); + + let status = check + .check( + &discard(), + &("chain-2".into()), + &("provider-2".into()), + &adapter, + ) + .await; + + assert_eq!(status, ProviderCheckStatus::Valid); + } + + #[tokio::test] + async fn check_valid_when_extended_blocks_are_supported() { + let check = ExtendedBlocksCheck::new([]); + + let adapter = TestAdapter::default(); + adapter.provides_extended_blocks_call(Ok(true)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert_eq!(status, ProviderCheckStatus::Valid); + } + + #[tokio::test] + async fn check_fails_when_extended_blocks_are_not_supported() { + let check = ExtendedBlocksCheck::new([]); + + let adapter = TestAdapter::default(); + adapter.provides_extended_blocks_call(Ok(false)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert!(matches!(status, ProviderCheckStatus::Failed { .. })); + } + + #[tokio::test] + async fn check_temporary_failure_when_provider_request_fails() { + let check = ExtendedBlocksCheck::new([]); + + let adapter = TestAdapter::default(); + adapter.provides_extended_blocks_call(Err(anyhow!("error"))); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert!(matches!( + status, + ProviderCheckStatus::TemporaryFailure { .. } + )) + } +} diff --git a/graph/src/components/network_provider/genesis_hash_check.rs b/graph/src/components/network_provider/genesis_hash_check.rs new file mode 100644 index 00000000000..a8d547e79c0 --- /dev/null +++ b/graph/src/components/network_provider/genesis_hash_check.rs @@ -0,0 +1,473 @@ +use std::sync::Arc; +use std::time::Instant; + +use async_trait::async_trait; +use slog::error; +use slog::warn; +use slog::Logger; + +use crate::components::network_provider::ChainIdentifierStore; +use crate::components::network_provider::ChainIdentifierStoreError; +use crate::components::network_provider::ChainName; +use crate::components::network_provider::NetworkDetails; +use crate::components::network_provider::ProviderCheck; +use crate::components::network_provider::ProviderCheckStatus; +use crate::components::network_provider::ProviderName; + +/// Requires providers to have the same network version and genesis hash as one +/// previously stored in the database. +pub struct GenesisHashCheck { + chain_identifier_store: Arc, +} + +impl GenesisHashCheck { + pub fn new(chain_identifier_store: Arc) -> Self { + Self { + chain_identifier_store, + } + } +} + +#[async_trait] +impl ProviderCheck for GenesisHashCheck { + fn name(&self) -> &'static str { + "GenesisHashCheck" + } + + async fn check( + &self, + logger: &Logger, + chain_name: &ChainName, + provider_name: &ProviderName, + adapter: &dyn NetworkDetails, + ) -> ProviderCheckStatus { + let chain_identifier = match adapter.chain_identifier().await { + Ok(chain_identifier) => chain_identifier, + Err(err) => { + let message = format!( + "Failed to get chain identifier from the provider '{}' on chain '{}': {:#}", + provider_name, chain_name, err, + ); + + error!(logger, "{}", message); + + return ProviderCheckStatus::TemporaryFailure { + checked_at: Instant::now(), + message, + }; + } + }; + + let check_result = self + .chain_identifier_store + .validate_identifier(chain_name, &chain_identifier); + + use ChainIdentifierStoreError::*; + + match check_result { + Ok(()) => ProviderCheckStatus::Valid, + Err(IdentifierNotSet(_)) => { + let update_result = self + .chain_identifier_store + .update_identifier(chain_name, &chain_identifier); + + if let Err(err) = update_result { + let message = format!( + "Failed to store chain identifier for chain '{}' using provider '{}': {:#}", + chain_name, provider_name, err, + ); + + error!(logger, "{}", message); + + return ProviderCheckStatus::TemporaryFailure { + checked_at: Instant::now(), + message, + }; + } + + ProviderCheckStatus::Valid + } + Err(NetVersionMismatch { + store_net_version, + chain_net_version, + .. + }) if store_net_version == "0" => { + warn!( + logger, + "The net version for chain '{}' has changed from '0' to '{}' while using provider '{}'; \ + The difference is probably caused by Firehose, since it does not provide the net version, and the default value was stored", + chain_name, + chain_net_version, + provider_name, + ); + + ProviderCheckStatus::Valid + } + Err(err @ NetVersionMismatch { .. }) => { + let message = format!( + "Genesis hash validation failed on provider '{}': {:#}", + provider_name, err, + ); + + error!(logger, "{}", message); + + ProviderCheckStatus::Failed { message } + } + Err(err @ GenesisBlockHashMismatch { .. }) => { + let message = format!( + "Genesis hash validation failed on provider '{}': {:#}", + provider_name, err, + ); + + error!(logger, "{}", message); + + ProviderCheckStatus::Failed { message } + } + Err(err @ Store(_)) => { + let message = format!( + "Genesis hash validation failed on provider '{}': {:#}", + provider_name, err, + ); + + error!(logger, "{}", message); + + ProviderCheckStatus::TemporaryFailure { + checked_at: Instant::now(), + message, + } + } + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::sync::Mutex; + + use anyhow::anyhow; + use anyhow::Result; + + use super::*; + use crate::blockchain::ChainIdentifier; + use crate::log::discard; + + #[derive(Default)] + struct TestChainIdentifierStore { + validate_identifier_calls: Mutex>>, + update_identifier_calls: Mutex>>, + } + + impl TestChainIdentifierStore { + fn validate_identifier_call(&self, x: Result<(), ChainIdentifierStoreError>) { + self.validate_identifier_calls.lock().unwrap().push(x) + } + + fn update_identifier_call(&self, x: Result<(), ChainIdentifierStoreError>) { + self.update_identifier_calls.lock().unwrap().push(x) + } + } + + impl Drop for TestChainIdentifierStore { + fn drop(&mut self) { + let Self { + validate_identifier_calls, + update_identifier_calls, + } = self; + + assert!(validate_identifier_calls.lock().unwrap().is_empty()); + assert!(update_identifier_calls.lock().unwrap().is_empty()); + } + } + + #[async_trait] + impl ChainIdentifierStore for TestChainIdentifierStore { + fn validate_identifier( + &self, + _chain_name: &ChainName, + _chain_identifier: &ChainIdentifier, + ) -> Result<(), ChainIdentifierStoreError> { + self.validate_identifier_calls.lock().unwrap().remove(0) + } + + fn update_identifier( + &self, + _chain_name: &ChainName, + _chain_identifier: &ChainIdentifier, + ) -> Result<(), ChainIdentifierStoreError> { + self.update_identifier_calls.lock().unwrap().remove(0) + } + } + + #[derive(Default)] + struct TestAdapter { + chain_identifier_calls: Mutex>>, + } + + impl TestAdapter { + fn chain_identifier_call(&self, x: Result) { + self.chain_identifier_calls.lock().unwrap().push(x) + } + } + + impl Drop for TestAdapter { + fn drop(&mut self) { + let Self { + chain_identifier_calls, + } = self; + + assert!(chain_identifier_calls.lock().unwrap().is_empty()); + } + } + + #[async_trait] + impl NetworkDetails for TestAdapter { + fn provider_name(&self) -> ProviderName { + unimplemented!(); + } + + async fn chain_identifier(&self) -> Result { + self.chain_identifier_calls.lock().unwrap().remove(0) + } + + async fn provides_extended_blocks(&self) -> Result { + unimplemented!(); + } + } + + #[tokio::test] + async fn check_temporary_failure_when_network_provider_request_fails() { + let store = Arc::new(TestChainIdentifierStore::default()); + let check = GenesisHashCheck::new(store); + + let adapter = TestAdapter::default(); + adapter.chain_identifier_call(Err(anyhow!("error"))); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert!(matches!( + status, + ProviderCheckStatus::TemporaryFailure { .. } + )); + } + + #[tokio::test] + async fn check_valid_when_store_successfully_validates_chain_identifier() { + let store = Arc::new(TestChainIdentifierStore::default()); + store.validate_identifier_call(Ok(())); + + let check = GenesisHashCheck::new(store); + + let chain_identifier = ChainIdentifier { + net_version: "1".to_owned(), + genesis_block_hash: vec![1].into(), + }; + + let adapter = TestAdapter::default(); + adapter.chain_identifier_call(Ok(chain_identifier)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert_eq!(status, ProviderCheckStatus::Valid); + } + + #[tokio::test] + async fn check_temporary_failure_on_initial_chain_identifier_update_error() { + let store = Arc::new(TestChainIdentifierStore::default()); + store.validate_identifier_call(Err(ChainIdentifierStoreError::IdentifierNotSet( + "chain-1".into(), + ))); + store.update_identifier_call(Err(ChainIdentifierStoreError::Store(anyhow!("error")))); + + let check = GenesisHashCheck::new(store); + + let chain_identifier = ChainIdentifier { + net_version: "1".to_owned(), + genesis_block_hash: vec![1].into(), + }; + + let adapter = TestAdapter::default(); + adapter.chain_identifier_call(Ok(chain_identifier)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert!(matches!( + status, + ProviderCheckStatus::TemporaryFailure { .. } + )); + } + + #[tokio::test] + async fn check_valid_on_initial_chain_identifier_update() { + let store = Arc::new(TestChainIdentifierStore::default()); + store.validate_identifier_call(Err(ChainIdentifierStoreError::IdentifierNotSet( + "chain-1".into(), + ))); + store.update_identifier_call(Ok(())); + + let check = GenesisHashCheck::new(store); + + let chain_identifier = ChainIdentifier { + net_version: "1".to_owned(), + genesis_block_hash: vec![1].into(), + }; + + let adapter = TestAdapter::default(); + adapter.chain_identifier_call(Ok(chain_identifier)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert_eq!(status, ProviderCheckStatus::Valid); + } + + #[tokio::test] + async fn check_valid_when_stored_identifier_network_version_is_zero() { + let store = Arc::new(TestChainIdentifierStore::default()); + store.validate_identifier_call(Err(ChainIdentifierStoreError::NetVersionMismatch { + chain_name: "chain-1".into(), + store_net_version: "0".to_owned(), + chain_net_version: "1".to_owned(), + })); + + let check = GenesisHashCheck::new(store); + + let chain_identifier = ChainIdentifier { + net_version: "1".to_owned(), + genesis_block_hash: vec![1].into(), + }; + + let adapter = TestAdapter::default(); + adapter.chain_identifier_call(Ok(chain_identifier)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert_eq!(status, ProviderCheckStatus::Valid); + } + + #[tokio::test] + async fn check_fails_on_identifier_network_version_mismatch() { + let store = Arc::new(TestChainIdentifierStore::default()); + store.validate_identifier_call(Err(ChainIdentifierStoreError::NetVersionMismatch { + chain_name: "chain-1".into(), + store_net_version: "2".to_owned(), + chain_net_version: "1".to_owned(), + })); + + let check = GenesisHashCheck::new(store); + + let chain_identifier = ChainIdentifier { + net_version: "1".to_owned(), + genesis_block_hash: vec![1].into(), + }; + + let adapter = TestAdapter::default(); + adapter.chain_identifier_call(Ok(chain_identifier)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert!(matches!(status, ProviderCheckStatus::Failed { .. })); + } + + #[tokio::test] + async fn check_fails_on_identifier_genesis_hash_mismatch() { + let store = Arc::new(TestChainIdentifierStore::default()); + store.validate_identifier_call(Err(ChainIdentifierStoreError::GenesisBlockHashMismatch { + chain_name: "chain-1".into(), + store_genesis_block_hash: vec![2].into(), + chain_genesis_block_hash: vec![1].into(), + })); + + let check = GenesisHashCheck::new(store); + + let chain_identifier = ChainIdentifier { + net_version: "1".to_owned(), + genesis_block_hash: vec![1].into(), + }; + + let adapter = TestAdapter::default(); + adapter.chain_identifier_call(Ok(chain_identifier)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert!(matches!(status, ProviderCheckStatus::Failed { .. })); + } + + #[tokio::test] + async fn check_temporary_failure_on_store_errors() { + let store = Arc::new(TestChainIdentifierStore::default()); + store.validate_identifier_call(Err(ChainIdentifierStoreError::Store(anyhow!("error")))); + + let check = GenesisHashCheck::new(store); + + let chain_identifier = ChainIdentifier { + net_version: "1".to_owned(), + genesis_block_hash: vec![1].into(), + }; + + let adapter = TestAdapter::default(); + adapter.chain_identifier_call(Ok(chain_identifier)); + + let status = check + .check( + &discard(), + &("chain-1".into()), + &("provider-1".into()), + &adapter, + ) + .await; + + assert!(matches!( + status, + ProviderCheckStatus::TemporaryFailure { .. } + )); + } +} diff --git a/graph/src/components/network_provider/mod.rs b/graph/src/components/network_provider/mod.rs new file mode 100644 index 00000000000..6ca27bc86d3 --- /dev/null +++ b/graph/src/components/network_provider/mod.rs @@ -0,0 +1,24 @@ +mod chain_identifier_store; +mod extended_blocks_check; +mod genesis_hash_check; +mod network_details; +mod provider_check; +mod provider_manager; + +pub use self::chain_identifier_store::ChainIdentifierStore; +pub use self::chain_identifier_store::ChainIdentifierStoreError; +pub use self::extended_blocks_check::ExtendedBlocksCheck; +pub use self::genesis_hash_check::GenesisHashCheck; +pub use self::network_details::NetworkDetails; +pub use self::provider_check::ProviderCheck; +pub use self::provider_check::ProviderCheckStatus; +pub use self::provider_manager::ProviderCheckStrategy; +pub use self::provider_manager::ProviderManager; + +// Used to increase memory efficiency. +// Currently, there is no need to create a separate type for this. +pub type ChainName = crate::data::value::Word; + +// Used to increase memory efficiency. +// Currently, there is no need to create a separate type for this. +pub type ProviderName = crate::data::value::Word; diff --git a/graph/src/components/network_provider/network_details.rs b/graph/src/components/network_provider/network_details.rs new file mode 100644 index 00000000000..a9ec5c2b58d --- /dev/null +++ b/graph/src/components/network_provider/network_details.rs @@ -0,0 +1,17 @@ +use anyhow::Result; +use async_trait::async_trait; + +use crate::blockchain::ChainIdentifier; +use crate::components::network_provider::ProviderName; + +/// Additional requirements for network providers that are necessary for provider checks. +#[async_trait] +pub trait NetworkDetails: Send + Sync + 'static { + fn provider_name(&self) -> ProviderName; + + /// Returns the data that helps to uniquely identify a chain. + async fn chain_identifier(&self) -> Result; + + /// Returns true if the provider supports extended block details. + async fn provides_extended_blocks(&self) -> Result; +} diff --git a/graph/src/components/network_provider/provider_check.rs b/graph/src/components/network_provider/provider_check.rs new file mode 100644 index 00000000000..115782cceb2 --- /dev/null +++ b/graph/src/components/network_provider/provider_check.rs @@ -0,0 +1,44 @@ +use std::time::Instant; + +use async_trait::async_trait; +use slog::Logger; + +use crate::components::network_provider::ChainName; +use crate::components::network_provider::NetworkDetails; +use crate::components::network_provider::ProviderName; + +#[async_trait] +pub trait ProviderCheck: Send + Sync + 'static { + fn name(&self) -> &'static str; + + async fn check( + &self, + logger: &Logger, + chain_name: &ChainName, + provider_name: &ProviderName, + adapter: &dyn NetworkDetails, + ) -> ProviderCheckStatus; +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ProviderCheckStatus { + NotChecked, + TemporaryFailure { + checked_at: Instant, + message: String, + }, + Valid, + Failed { + message: String, + }, +} + +impl ProviderCheckStatus { + pub fn is_valid(&self) -> bool { + matches!(self, ProviderCheckStatus::Valid) + } + + pub fn is_failed(&self) -> bool { + matches!(self, ProviderCheckStatus::Failed { .. }) + } +} diff --git a/graph/src/components/network_provider/provider_manager.rs b/graph/src/components/network_provider/provider_manager.rs new file mode 100644 index 00000000000..300d85118b6 --- /dev/null +++ b/graph/src/components/network_provider/provider_manager.rs @@ -0,0 +1,957 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::OnceLock; +use std::time::Duration; + +use derivative::Derivative; +use itertools::Itertools; +use slog::error; +use slog::info; +use slog::warn; +use slog::Logger; +use thiserror::Error; +use tokio::sync::RwLock; + +use crate::components::network_provider::ChainName; +use crate::components::network_provider::NetworkDetails; +use crate::components::network_provider::ProviderCheck; +use crate::components::network_provider::ProviderCheckStatus; +use crate::components::network_provider::ProviderName; + +/// The total time all providers have to perform all checks. +const VALIDATION_MAX_DURATION: Duration = Duration::from_secs(30); + +/// Providers that failed validation with a temporary failure are re-validated at this interval. +const VALIDATION_RETRY_INTERVAL: Duration = Duration::from_secs(300); + +/// ProviderManager is responsible for validating providers before they are returned to consumers. +#[derive(Clone, Derivative)] +#[derivative(Debug)] +pub struct ProviderManager { + #[derivative(Debug = "ignore")] + inner: Arc>, + + validation_max_duration: Duration, + validation_retry_interval: Duration, +} + +/// The strategy used by the [ProviderManager] when checking providers. +#[derive(Clone)] +pub enum ProviderCheckStrategy<'a> { + /// Marks a provider as valid without performing any checks on it. + MarkAsValid, + + /// Requires a provider to pass all specified checks to be considered valid. + RequireAll(&'a [Arc]), +} + +#[derive(Debug, Error)] +pub enum ProviderManagerError { + #[error("provider validation timed out on chain '{0}'")] + ProviderValidationTimeout(ChainName), + + #[error("no providers available for chain '{0}'")] + NoProvidersAvailable(ChainName), + + #[error("all providers failed for chain '{0}'")] + AllProvidersFailed(ChainName), +} + +struct Inner { + logger: Logger, + adapters: HashMap]>>, + validations: Box<[Validation]>, + enabled_checks: Box<[Arc]>, +} + +struct Adapter { + /// An index from the validations vector that is used to directly access the validation state + /// of the provider without additional checks or pointer dereferences. + /// + /// This is useful because the same provider can have multiple adapters to increase the number + /// of concurrent requests, but it does not make sense to perform multiple validations on + /// the same provider. + /// + /// It is guaranteed to be a valid index from the validations vector. + validation_index: usize, + + inner: T, +} + +/// Contains all the information needed to determine whether a provider is valid or not. +struct Validation { + chain_name: ChainName, + provider_name: ProviderName, + + /// Used to avoid acquiring the lock if possible. + /// + /// If it is not set, it means that validation is required. + /// If it is 'true', it means that the provider has passed all the checks. + /// If it is 'false', it means that the provider has failed at least one check. + is_valid: OnceLock, + + /// Contains the statuses resulting from performing provider checks on the provider. + /// It is guaranteed to have the same number of elements as the number of checks enabled. + check_results: RwLock>, +} + +impl ProviderManager { + /// Creates a new provider manager for the specified providers. + /// + /// Performs enabled provider checks on each provider when it is accessed. + pub fn new( + logger: Logger, + adapters: impl IntoIterator)>, + strategy: ProviderCheckStrategy<'_>, + ) -> Self { + let enabled_checks = match strategy { + ProviderCheckStrategy::MarkAsValid => { + warn!( + &logger, + "No network provider checks enabled. \ + This can cause data inconsistency and many other issues." + ); + + &[] + } + ProviderCheckStrategy::RequireAll(checks) => { + info!( + &logger, + "All network providers have checks enabled. \ + To be considered valid they will have to pass the following checks: [{}]", + checks.iter().map(|x| x.name()).join(",") + ); + + checks + } + }; + + let mut validations: Vec = Vec::new(); + let adapters = Self::adapters_by_chain_names(adapters, &mut validations, &enabled_checks); + + let inner = Inner { + logger, + adapters, + validations: validations.into(), + enabled_checks: enabled_checks.to_vec().into(), + }; + + Self { + inner: Arc::new(inner), + validation_max_duration: VALIDATION_MAX_DURATION, + validation_retry_interval: VALIDATION_RETRY_INTERVAL, + } + } + + /// Returns the total number of providers available for the chain. + /// + /// Does not take provider validation status into account. + pub fn len(&self, chain_name: &ChainName) -> usize { + self.inner + .adapters + .get(chain_name) + .map(|adapter| adapter.len()) + .unwrap_or_default() + } + + /// Returns all available providers for the chain. + /// + /// Does not perform any provider validation and does not guarantee that providers will be + /// accessible or return the expected data. + pub fn providers_unchecked(&self, chain_name: &ChainName) -> impl Iterator { + self.inner.adapters_unchecked(chain_name) + } + + /// Returns all valid providers for the chain. + /// + /// Performs all enabled provider checks for each available provider for the chain. + /// A provider is considered valid if it successfully passes all checks. + /// + /// Note: Provider checks may take some time to complete. + pub async fn providers( + &self, + chain_name: &ChainName, + ) -> Result, ProviderManagerError> { + tokio::time::timeout( + self.validation_max_duration, + self.inner + .adapters(chain_name, self.validation_retry_interval), + ) + .await + .map_err(|_| ProviderManagerError::ProviderValidationTimeout(chain_name.clone()))? + } + + fn adapters_by_chain_names( + adapters: impl IntoIterator)>, + validations: &mut Vec, + enabled_checks: &[Arc], + ) -> HashMap]>> { + adapters + .into_iter() + .map(|(chain_name, adapters)| { + let adapters = adapters + .into_iter() + .map(|adapter| { + let provider_name = adapter.provider_name(); + + let validation_index = Self::get_or_init_validation_index( + validations, + enabled_checks, + &chain_name, + &provider_name, + ); + + Adapter { + validation_index, + inner: adapter, + } + }) + .collect_vec(); + + (chain_name, adapters.into()) + }) + .collect() + } + + fn get_or_init_validation_index( + validations: &mut Vec, + enabled_checks: &[Arc], + chain_name: &ChainName, + provider_name: &ProviderName, + ) -> usize { + validations + .iter() + .position(|validation| { + validation.chain_name == *chain_name && validation.provider_name == *provider_name + }) + .unwrap_or_else(|| { + validations.push(Validation { + chain_name: chain_name.clone(), + provider_name: provider_name.clone(), + is_valid: if enabled_checks.is_empty() { + OnceLock::from(true) + } else { + OnceLock::new() + }, + check_results: RwLock::new( + vec![ProviderCheckStatus::NotChecked; enabled_checks.len()].into(), + ), + }); + + validations.len() - 1 + }) + } +} + +// Used to simplify some tests. +impl Default for ProviderManager { + fn default() -> Self { + Self { + inner: Arc::new(Inner { + logger: crate::log::discard(), + adapters: HashMap::new(), + validations: vec![].into(), + enabled_checks: vec![].into(), + }), + validation_max_duration: VALIDATION_MAX_DURATION, + validation_retry_interval: VALIDATION_RETRY_INTERVAL, + } + } +} + +impl Inner { + fn adapters_unchecked(&self, chain_name: &ChainName) -> impl Iterator { + match self.adapters.get(chain_name) { + Some(adapters) => adapters.iter(), + None => [].iter(), + } + .map(|adapter| &adapter.inner) + } + + async fn adapters( + &self, + chain_name: &ChainName, + validation_retry_interval: Duration, + ) -> Result, ProviderManagerError> { + use std::iter::once; + + let (initial_size, adapters) = match self.adapters.get(chain_name) { + Some(adapters) => { + if !self.enabled_checks.is_empty() { + self.validate_adapters(adapters, validation_retry_interval) + .await; + } + + (adapters.len(), adapters.iter()) + } + None => (0, [].iter()), + }; + + let mut valid_adapters = adapters + .clone() + .filter(|adapter| { + self.validations[adapter.validation_index].is_valid.get() == Some(&true) + }) + .map(|adapter| &adapter.inner); + + // A thread-safe and fast way to check if an iterator has elements. + // Note: Using `.peekable()` is not thread safe. + if let first_valid_adapter @ Some(_) = valid_adapters.next() { + return Ok(once(first_valid_adapter).flatten().chain(valid_adapters)); + } + + // This is done to maintain backward compatibility with the previous implementation, + // and to avoid breaking modules that may rely on empty results in some cases. + if initial_size == 0 { + // Even though we know there are no adapters at this point, + // we still need to return the same type. + return Ok(once(None).flatten().chain(valid_adapters)); + } + + let failed_count = adapters + .filter(|adapter| { + self.validations[adapter.validation_index].is_valid.get() == Some(&false) + }) + .count(); + + if failed_count == initial_size { + return Err(ProviderManagerError::AllProvidersFailed(chain_name.clone())); + } + + Err(ProviderManagerError::NoProvidersAvailable( + chain_name.clone(), + )) + } + + async fn validate_adapters( + &self, + adapters: &[Adapter], + validation_retry_interval: Duration, + ) { + let validation_futs = adapters + .iter() + .filter(|adapter| { + self.validations[adapter.validation_index] + .is_valid + .get() + .is_none() + }) + .map(|adapter| self.validate_adapter(adapter, validation_retry_interval)); + + let _outputs: Vec<()> = crate::futures03::future::join_all(validation_futs).await; + } + + async fn validate_adapter(&self, adapter: &Adapter, validation_retry_interval: Duration) { + let validation = &self.validations[adapter.validation_index]; + + let chain_name = &validation.chain_name; + let provider_name = &validation.provider_name; + let mut check_results = validation.check_results.write().await; + + // Make sure that when we get the lock, the adapter is still not validated. + if validation.is_valid.get().is_some() { + return; + } + + for (i, check_result) in check_results.iter_mut().enumerate() { + use ProviderCheckStatus::*; + + match check_result { + NotChecked => { + // Check is required; + } + TemporaryFailure { + checked_at, + message: _, + } => { + if checked_at.elapsed() < validation_retry_interval { + continue; + } + + // A new check is required; + } + Valid => continue, + Failed { message: _ } => continue, + } + + *check_result = self.enabled_checks[i] + .check(&self.logger, chain_name, provider_name, &adapter.inner) + .await; + + // One failure is enough to not even try to perform any further checks, + // because that adapter will never be considered valid. + if check_result.is_failed() { + validation.is_valid.get_or_init(|| false); + return; + } + } + + if check_results.iter().all(|x| x.is_valid()) { + validation.is_valid.get_or_init(|| true); + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Mutex; + use std::time::Instant; + + use anyhow::Result; + use async_trait::async_trait; + + use super::*; + use crate::blockchain::ChainIdentifier; + use crate::log::discard; + + struct TestAdapter { + id: usize, + provider_name_calls: Mutex>, + } + + impl TestAdapter { + fn new(id: usize) -> Self { + Self { + id, + provider_name_calls: Default::default(), + } + } + + fn provider_name_call(&self, x: ProviderName) { + self.provider_name_calls.lock().unwrap().push(x) + } + } + + impl Drop for TestAdapter { + fn drop(&mut self) { + let Self { + id: _, + provider_name_calls, + } = self; + + assert!(provider_name_calls.lock().unwrap().is_empty()); + } + } + + #[async_trait] + impl NetworkDetails for Arc { + fn provider_name(&self) -> ProviderName { + self.provider_name_calls.lock().unwrap().remove(0) + } + + async fn chain_identifier(&self) -> Result { + unimplemented!(); + } + + async fn provides_extended_blocks(&self) -> Result { + unimplemented!(); + } + } + + #[derive(Default)] + struct TestProviderCheck { + check_calls: Mutex ProviderCheckStatus + Send>>>, + } + + impl TestProviderCheck { + fn check_call(&self, x: Box ProviderCheckStatus + Send>) { + self.check_calls.lock().unwrap().push(x) + } + } + + impl Drop for TestProviderCheck { + fn drop(&mut self) { + assert!(self.check_calls.lock().unwrap().is_empty()); + } + } + + #[async_trait] + impl ProviderCheck for TestProviderCheck { + fn name(&self) -> &'static str { + "TestProviderCheck" + } + + async fn check( + &self, + _logger: &Logger, + _chain_name: &ChainName, + _provider_name: &ProviderName, + _adapter: &dyn NetworkDetails, + ) -> ProviderCheckStatus { + self.check_calls.lock().unwrap().remove(0)() + } + } + + fn chain_name() -> ChainName { + "test_chain".into() + } + + fn other_chain_name() -> ChainName { + "other_chain".into() + } + + fn ids<'a>(adapters: impl Iterator>) -> Vec { + adapters.map(|adapter| adapter.id).collect() + } + + #[tokio::test] + async fn no_providers() { + let manager: ProviderManager> = + ProviderManager::new(discard(), [], ProviderCheckStrategy::MarkAsValid); + + assert_eq!(manager.len(&chain_name()), 0); + assert_eq!(manager.providers_unchecked(&chain_name()).count(), 0); + assert_eq!(manager.providers(&chain_name()).await.unwrap().count(), 0); + } + + #[tokio::test] + async fn no_providers_for_chain() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(other_chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::MarkAsValid, + ); + + assert_eq!(manager.len(&chain_name()), 0); + assert_eq!(manager.len(&other_chain_name()), 1); + + assert_eq!(manager.providers_unchecked(&chain_name()).count(), 0); + + assert_eq!( + ids(manager.providers_unchecked(&other_chain_name())), + vec![1], + ); + + assert_eq!(manager.providers(&chain_name()).await.unwrap().count(), 0); + + assert_eq!( + ids(manager.providers(&other_chain_name()).await.unwrap()), + vec![1], + ); + } + + #[tokio::test] + async fn multiple_providers() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let adapter_2 = Arc::new(TestAdapter::new(2)); + adapter_2.provider_name_call("provider_2".into()); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone(), adapter_2.clone()])], + ProviderCheckStrategy::MarkAsValid, + ); + + assert_eq!(manager.len(&chain_name()), 2); + + assert_eq!(ids(manager.providers_unchecked(&chain_name())), vec![1, 2]); + + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1, 2], + ); + } + + #[tokio::test] + async fn providers_unchecked_skips_provider_checks() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + assert_eq!(ids(manager.providers_unchecked(&chain_name())), vec![1]); + } + + #[tokio::test] + async fn successful_provider_check() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1] + ); + + // Another call will not trigger a new validation. + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1] + ); + } + + #[tokio::test] + async fn multiple_successful_provider_checks() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let check_2 = Arc::new(TestProviderCheck::default()); + check_2.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone(), check_2.clone()]), + ); + + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1] + ); + + // Another call will not trigger a new validation. + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1] + ); + } + + #[tokio::test] + async fn multiple_successful_provider_checks_on_multiple_adapters() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let adapter_2 = Arc::new(TestAdapter::new(2)); + adapter_2.provider_name_call("provider_2".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let check_2 = Arc::new(TestProviderCheck::default()); + check_2.check_call(Box::new(|| ProviderCheckStatus::Valid)); + check_2.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone(), adapter_2.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone(), check_2.clone()]), + ); + + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1, 2], + ); + + // Another call will not trigger a new validation. + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1, 2], + ); + } + + #[tokio::test] + async fn successful_provider_check_for_a_pool_of_adapters_for_a_provider() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let adapter_2 = Arc::new(TestAdapter::new(2)); + adapter_2.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone(), adapter_2.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1, 2], + ); + + // Another call will not trigger a new validation. + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1, 2], + ); + } + + #[tokio::test] + async fn multiple_successful_provider_checks_for_a_pool_of_adapters_for_a_provider() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let adapter_2 = Arc::new(TestAdapter::new(2)); + adapter_2.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let check_2 = Arc::new(TestProviderCheck::default()); + check_2.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone(), adapter_2.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone(), check_2.clone()]), + ); + + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1, 2], + ); + + // Another call will not trigger a new validation. + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1, 2], + ); + } + + #[tokio::test] + async fn provider_validation_timeout() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| { + std::thread::sleep(Duration::from_millis(200)); + ProviderCheckStatus::Valid + })); + + let mut manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + manager.validation_max_duration = Duration::from_millis(100); + + match manager.providers(&chain_name()).await { + Ok(_) => {} + Err(err) => { + assert_eq!( + err.to_string(), + ProviderManagerError::ProviderValidationTimeout(chain_name()).to_string(), + ); + } + }; + } + + #[tokio::test] + async fn no_providers_available() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::TemporaryFailure { + checked_at: Instant::now(), + message: "error".to_owned(), + })); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + match manager.providers(&chain_name()).await { + Ok(_) => {} + Err(err) => { + assert_eq!( + err.to_string(), + ProviderManagerError::NoProvidersAvailable(chain_name()).to_string(), + ); + } + }; + } + + #[tokio::test] + async fn all_providers_failed() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Failed { + message: "error".to_owned(), + })); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + match manager.providers(&chain_name()).await { + Ok(_) => {} + Err(err) => { + assert_eq!( + err.to_string(), + ProviderManagerError::AllProvidersFailed(chain_name()).to_string(), + ); + } + }; + } + + #[tokio::test] + async fn temporary_provider_check_failures_are_retried() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::TemporaryFailure { + checked_at: Instant::now(), + message: "error".to_owned(), + })); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let mut manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + manager.validation_retry_interval = Duration::from_millis(100); + + assert!(manager.providers(&chain_name()).await.is_err()); + + tokio::time::sleep(Duration::from_millis(200)).await; + + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1] + ); + } + + #[tokio::test] + async fn final_provider_check_failures_are_not_retried() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Failed { + message: "error".to_owned(), + })); + + let mut manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + manager.validation_retry_interval = Duration::from_millis(100); + + assert!(manager.providers(&chain_name()).await.is_err()); + + tokio::time::sleep(Duration::from_millis(200)).await; + + assert!(manager.providers(&chain_name()).await.is_err()); + } + + #[tokio::test] + async fn mix_valid_and_invalid_providers() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let adapter_2 = Arc::new(TestAdapter::new(2)); + adapter_2.provider_name_call("provider_2".into()); + + let adapter_3 = Arc::new(TestAdapter::new(3)); + adapter_3.provider_name_call("provider_3".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + check_1.check_call(Box::new(|| ProviderCheckStatus::Failed { + message: "error".to_owned(), + })); + check_1.check_call(Box::new(|| ProviderCheckStatus::TemporaryFailure { + checked_at: Instant::now(), + message: "error".to_owned(), + })); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [( + chain_name(), + vec![adapter_1.clone(), adapter_2.clone(), adapter_3.clone()], + )], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + assert_eq!( + ids(manager.providers(&chain_name()).await.unwrap()), + vec![1] + ); + } + + #[tokio::test] + async fn one_provider_check_failure_is_enough_to_mark_an_provider_as_invalid() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let check_2 = Arc::new(TestProviderCheck::default()); + check_2.check_call(Box::new(|| ProviderCheckStatus::Failed { + message: "error".to_owned(), + })); + + let check_3 = Arc::new(TestProviderCheck::default()); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone(), check_2.clone(), check_3.clone()]), + ); + + assert!(manager.providers(&chain_name()).await.is_err()); + } + + #[tokio::test(flavor = "multi_thread")] + async fn concurrent_providers_access_does_not_trigger_multiple_validations() { + let adapter_1 = Arc::new(TestAdapter::new(1)); + adapter_1.provider_name_call("provider_1".into()); + + let check_1 = Arc::new(TestProviderCheck::default()); + check_1.check_call(Box::new(|| ProviderCheckStatus::Valid)); + + let manager: ProviderManager> = ProviderManager::new( + discard(), + [(chain_name(), vec![adapter_1.clone()])], + ProviderCheckStrategy::RequireAll(&[check_1.clone()]), + ); + + let fut = || { + let manager = manager.clone(); + + async move { + let chain_name = chain_name(); + + ids(manager.providers(&chain_name).await.unwrap()) + } + }; + + let results = crate::futures03::future::join_all([fut(), fut(), fut(), fut()]).await; + + assert_eq!( + results.into_iter().flatten().collect_vec(), + vec![1, 1, 1, 1], + ); + } +} diff --git a/graph/src/endpoint.rs b/graph/src/endpoint.rs index 82a69398446..bdff8dc8135 100644 --- a/graph/src/endpoint.rs +++ b/graph/src/endpoint.rs @@ -9,10 +9,8 @@ use std::{ use prometheus::IntCounterVec; use slog::{warn, Logger}; -use crate::{ - components::{adapter::ProviderName, metrics::MetricsRegistry}, - data::value::Word, -}; +use crate::components::network_provider::ProviderName; +use crate::{components::metrics::MetricsRegistry, data::value::Word}; /// ProviderCount is the underlying structure to keep the count, /// we require that all the hosts are known ahead of time, this way we can diff --git a/graph/src/env/mod.rs b/graph/src/env/mod.rs index c6e180ea428..f1533afad99 100644 --- a/graph/src/env/mod.rs +++ b/graph/src/env/mod.rs @@ -226,6 +226,18 @@ pub struct EnvVars { /// /// If not specified, the graphman server will not start. pub graphman_server_auth_token: Option, + + /// By default, all providers are required to support extended block details, + /// as this is the safest option for a graph-node operator. + /// + /// Providers that do not support extended block details for enabled chains + /// are considered invalid and will not be used. + /// + /// To disable checks for one or more chains, simply specify their names + /// in this configuration option. + /// + /// Defaults to an empty list, which means that this feature is enabled for all chains; + pub firehose_disable_extended_blocks_for_chains: Vec, } impl EnvVars { @@ -311,6 +323,10 @@ impl EnvVars { genesis_validation_enabled: inner.genesis_validation_enabled.0, genesis_validation_timeout: Duration::from_secs(inner.genesis_validation_timeout), graphman_server_auth_token: inner.graphman_server_auth_token, + firehose_disable_extended_blocks_for_chains: + Self::firehose_disable_extended_blocks_for_chains( + inner.firehose_disable_extended_blocks_for_chains, + ), }) } @@ -335,6 +351,14 @@ impl EnvVars { pub fn log_gql_cache_timing(&self) -> bool { self.log_query_timing_contains("cache") && self.log_gql_timing() } + + fn firehose_disable_extended_blocks_for_chains(s: Option) -> Vec { + s.unwrap_or_default() + .split(",") + .map(|x| x.trim().to_string()) + .filter(|x| !x.is_empty()) + .collect() + } } impl Default for EnvVars { @@ -462,6 +486,8 @@ struct Inner { genesis_validation_timeout: u64, #[envconfig(from = "GRAPHMAN_SERVER_AUTH_TOKEN")] graphman_server_auth_token: Option, + #[envconfig(from = "GRAPH_NODE_FIREHOSE_DISABLE_EXTENDED_BLOCKS_FOR_CHAINS")] + firehose_disable_extended_blocks_for_chains: Option, } #[derive(Clone, Debug)] diff --git a/graph/src/firehose/endpoint_info/client.rs b/graph/src/firehose/endpoint_info/client.rs new file mode 100644 index 00000000000..658406672a6 --- /dev/null +++ b/graph/src/firehose/endpoint_info/client.rs @@ -0,0 +1,46 @@ +use anyhow::Context; +use anyhow::Result; +use tonic::codec::CompressionEncoding; +use tonic::service::interceptor::InterceptedService; +use tonic::transport::Channel; + +use super::info_response::InfoResponse; +use crate::firehose::codec; +use crate::firehose::interceptors::AuthInterceptor; +use crate::firehose::interceptors::MetricsInterceptor; + +pub struct Client { + inner: codec::endpoint_info_client::EndpointInfoClient< + InterceptedService, AuthInterceptor>, + >, +} + +impl Client { + pub fn new(metrics: MetricsInterceptor, auth: AuthInterceptor) -> Self { + let mut inner = + codec::endpoint_info_client::EndpointInfoClient::with_interceptor(metrics, auth); + + inner = inner.accept_compressed(CompressionEncoding::Gzip); + + Self { inner } + } + + pub fn with_compression(mut self) -> Self { + self.inner = self.inner.send_compressed(CompressionEncoding::Gzip); + self + } + + pub fn with_max_message_size(mut self, size: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(size); + self + } + + pub async fn info(&mut self) -> Result { + let req = codec::InfoRequest {}; + let resp = self.inner.info(req).await?.into_inner(); + + resp.clone() + .try_into() + .with_context(|| format!("received response: {resp:?}")) + } +} diff --git a/graph/src/firehose/endpoint_info/info_response.rs b/graph/src/firehose/endpoint_info/info_response.rs new file mode 100644 index 00000000000..56f431452c4 --- /dev/null +++ b/graph/src/firehose/endpoint_info/info_response.rs @@ -0,0 +1,96 @@ +use anyhow::anyhow; +use anyhow::Context; +use anyhow::Result; + +use crate::blockchain::BlockHash; +use crate::blockchain::BlockPtr; +use crate::components::network_provider::ChainName; +use crate::firehose::codec; + +#[derive(Clone, Debug)] +pub struct InfoResponse { + pub chain_name: ChainName, + pub block_features: Vec, + + first_streamable_block_num: u64, + first_streamable_block_hash: BlockHash, +} + +impl InfoResponse { + /// Returns the ptr of the genesis block from the perspective of the Firehose. + /// It is not guaranteed to be the genesis block ptr of the chain. + /// + /// There is currently no better way to get the genesis block ptr from Firehose. + pub fn genesis_block_ptr(&self) -> Result { + let hash = self.first_streamable_block_hash.clone(); + let number = self.first_streamable_block_num; + + Ok(BlockPtr { + hash, + number: number + .try_into() + .with_context(|| format!("'{number}' is not a valid `BlockNumber`"))?, + }) + } +} + +impl TryFrom for InfoResponse { + type Error = anyhow::Error; + + fn try_from(resp: codec::InfoResponse) -> Result { + let codec::InfoResponse { + chain_name, + chain_name_aliases: _, + first_streamable_block_num, + first_streamable_block_id, + block_id_encoding, + block_features, + } = resp; + + let encoding = codec::info_response::BlockIdEncoding::try_from(block_id_encoding)?; + + Ok(Self { + chain_name: chain_name_checked(chain_name)?, + block_features: block_features_checked(block_features)?, + first_streamable_block_num, + first_streamable_block_hash: parse_block_hash(first_streamable_block_id, encoding)?, + }) + } +} + +fn chain_name_checked(chain_name: String) -> Result { + if chain_name.is_empty() { + return Err(anyhow!("`chain_name` is empty")); + } + + Ok(chain_name.into()) +} + +fn block_features_checked(block_features: Vec) -> Result> { + if block_features.iter().any(|x| x.is_empty()) { + return Err(anyhow!("`block_features` contains empty features")); + } + + Ok(block_features) +} + +fn parse_block_hash( + s: String, + encoding: codec::info_response::BlockIdEncoding, +) -> Result { + use base64::engine::general_purpose::STANDARD; + use base64::engine::general_purpose::URL_SAFE; + use base64::Engine; + use codec::info_response::BlockIdEncoding::*; + + let block_hash = match encoding { + Unset => return Err(anyhow!("`block_id_encoding` is not set")), + Hex => hex::decode(s)?.into(), + BlockIdEncoding0xHex => hex::decode(s.trim_start_matches("0x"))?.into(), + Base58 => bs58::decode(s).into_vec()?.into(), + Base64 => STANDARD.decode(s)?.into(), + Base64url => URL_SAFE.decode(s)?.into(), + }; + + Ok(block_hash) +} diff --git a/graph/src/firehose/endpoint_info/mod.rs b/graph/src/firehose/endpoint_info/mod.rs new file mode 100644 index 00000000000..cb2c8fa7817 --- /dev/null +++ b/graph/src/firehose/endpoint_info/mod.rs @@ -0,0 +1,5 @@ +mod client; +mod info_response; + +pub use client::Client; +pub use info_response::InfoResponse; diff --git a/graph/src/firehose/endpoints.rs b/graph/src/firehose/endpoints.rs index ef00ec53c03..72d3f986c9c 100644 --- a/graph/src/firehose/endpoints.rs +++ b/graph/src/firehose/endpoints.rs @@ -1,35 +1,24 @@ +use crate::firehose::fetch_client::FetchClient; +use crate::firehose::interceptors::AuthInterceptor; use crate::{ - bail, blockchain::{ - block_stream::FirehoseCursor, Block as BlockchainBlock, BlockHash, BlockPtr, - ChainIdentifier, + block_stream::FirehoseCursor, Block as BlockchainBlock, BlockPtr, ChainIdentifier, }, cheap_clone::CheapClone, - components::{ - adapter::{ChainId, NetIdentifiable, ProviderManager, ProviderName}, - store::BlockNumber, - }, - data::value::Word, + components::store::BlockNumber, endpoint::{ConnectionType, EndpointMetrics, RequestLabels}, env::ENV_VARS, firehose::decode_firehose_block, - prelude::{anyhow, debug, info, DeploymentHash}, - substreams::Package, - substreams_rpc::{self, response, BlockScopedData, Response}, + prelude::{anyhow, debug, DeploymentHash}, + substreams_rpc, }; - -use crate::firehose::fetch_client::FetchClient; -use crate::firehose::interceptors::AuthInterceptor; use async_trait::async_trait; use futures03::StreamExt; use http0::uri::{Scheme, Uri}; use itertools::Itertools; -use prost::Message; use slog::Logger; -use std::{ - collections::HashMap, fmt::Display, marker::PhantomData, ops::ControlFlow, str::FromStr, - sync::Arc, time::Duration, -}; +use std::{collections::HashMap, fmt::Display, ops::ControlFlow, sync::Arc, time::Duration}; +use tokio::sync::OnceCell; use tonic::codegen::InterceptedService; use tonic::{ codegen::CompressionEncoding, @@ -39,159 +28,21 @@ use tonic::{ }; use super::{codec as firehose, interceptors::MetricsInterceptor, stream_client::StreamClient}; +use crate::components::network_provider::ChainName; +use crate::components::network_provider::NetworkDetails; +use crate::components::network_provider::ProviderCheckStrategy; +use crate::components::network_provider::ProviderManager; +use crate::components::network_provider::ProviderName; /// This is constant because we found this magic number of connections after /// which the grpc connections start to hang. /// For more details see: https://github.com/graphprotocol/graph-node/issues/3879 pub const SUBGRAPHS_PER_CONN: usize = 100; -/// Substreams does not provide a simpler way to get the chain identity so we use this package -/// to obtain the genesis hash. -const SUBSTREAMS_HEAD_TRACKER_BYTES: &[u8; 89935] = include_bytes!( - "../../../substreams/substreams-head-tracker/substreams-head-tracker-v1.0.0.spkg" -); - const LOW_VALUE_THRESHOLD: usize = 10; const LOW_VALUE_USED_PERCENTAGE: usize = 50; const HIGH_VALUE_USED_PERCENTAGE: usize = 80; -/// Firehose endpoints do not currently provide a chain agnostic way of getting the genesis block. -/// In order to get the genesis hash the block needs to be decoded and the graph crate has no -/// knowledge of specific chains so this abstracts the chain details from the FirehoseEndpoint. -#[async_trait] -pub trait GenesisDecoder: std::fmt::Debug + Sync + Send { - async fn get_genesis_block_ptr( - &self, - endpoint: &Arc, - ) -> Result; - fn box_clone(&self) -> Box; -} - -#[derive(Debug, Clone)] -pub struct FirehoseGenesisDecoder { - pub logger: Logger, - phantom: PhantomData, -} - -impl FirehoseGenesisDecoder { - pub fn new(logger: Logger) -> Box { - Box::new(Self { - logger, - phantom: PhantomData, - }) - } -} - -#[async_trait] -impl GenesisDecoder - for FirehoseGenesisDecoder -{ - async fn get_genesis_block_ptr( - &self, - endpoint: &Arc, - ) -> Result { - endpoint.genesis_block_ptr::(&self.logger).await - } - - fn box_clone(&self) -> Box { - Box::new(Self { - logger: self.logger.cheap_clone(), - phantom: PhantomData, - }) - } -} - -#[derive(Debug, Clone)] -pub struct SubstreamsGenesisDecoder {} - -#[async_trait] -impl GenesisDecoder for SubstreamsGenesisDecoder { - async fn get_genesis_block_ptr( - &self, - endpoint: &Arc, - ) -> Result { - let package = Package::decode(SUBSTREAMS_HEAD_TRACKER_BYTES.to_vec().as_ref()).unwrap(); - let headers = ConnectionHeaders::new(); - let endpoint = endpoint.cheap_clone(); - - let mut stream = endpoint - .substreams( - substreams_rpc::Request { - start_block_num: 0, - start_cursor: "".to_string(), - stop_block_num: 0, - final_blocks_only: true, - production_mode: true, - output_module: "map_blocks".to_string(), - modules: package.modules, - debug_initial_store_snapshot_for_modules: vec![], - }, - &headers, - ) - .await?; - - tokio::time::timeout(Duration::from_secs(30), async move { - loop { - let rsp = stream.next().await; - - match rsp { - Some(Ok(Response { message })) => match message { - Some(response::Message::BlockScopedData(BlockScopedData { - clock, .. - })) if clock.is_some() => { - // unwrap: the match guard ensures this is safe. - let clock = clock.unwrap(); - return Ok(BlockPtr { - number: clock.number.try_into()?, - hash: BlockHash::from_str(&clock.id)?, - }); - } - // most other messages are related to the protocol itself or debugging which are - // not relevant for this use case. - Some(_) => continue, - // No idea when this would happen - None => continue, - }, - Some(Err(status)) => bail!("unable to get genesis block, status: {}", status), - None => bail!("unable to get genesis block, stream ended"), - } - } - }) - .await - .map_err(|_| anyhow!("unable to get genesis block, timed out."))? - } - - fn box_clone(&self) -> Box { - Box::new(Self {}) - } -} - -#[derive(Debug, Clone)] -pub struct NoopGenesisDecoder; - -impl NoopGenesisDecoder { - pub fn boxed() -> Box { - Box::new(Self {}) - } -} - -#[async_trait] -impl GenesisDecoder for NoopGenesisDecoder { - async fn get_genesis_block_ptr( - &self, - _endpoint: &Arc, - ) -> Result { - Ok(BlockPtr { - hash: BlockHash::zero(), - number: 0, - }) - } - - fn box_clone(&self) -> Box { - Box::new(Self {}) - } -} - #[derive(Debug)] pub struct FirehoseEndpoint { pub provider: ProviderName, @@ -199,26 +50,36 @@ pub struct FirehoseEndpoint { pub filters_enabled: bool, pub compression_enabled: bool, pub subgraph_limit: SubgraphLimit, - genesis_decoder: Box, endpoint_metrics: Arc, channel: Channel, + + /// The endpoint info is not intended to change very often, as it only contains the + /// endpoint's metadata, so caching it avoids sending unnecessary network requests. + info_response: OnceCell, } #[derive(Debug)] pub struct ConnectionHeaders(HashMap, MetadataValue>); #[async_trait] -impl NetIdentifiable for Arc { - async fn net_identifiers(&self) -> Result { - let ptr: BlockPtr = self.genesis_decoder.get_genesis_block_ptr(self).await?; +impl NetworkDetails for Arc { + fn provider_name(&self) -> ProviderName { + self.provider.clone() + } + + async fn chain_identifier(&self) -> anyhow::Result { + let genesis_block_ptr = self.clone().info().await?.genesis_block_ptr()?; Ok(ChainIdentifier { net_version: "0".to_string(), - genesis_block_hash: ptr.hash, + genesis_block_hash: genesis_block_ptr.hash, }) } - fn provider_name(&self) -> ProviderName { - self.provider.clone() + + async fn provides_extended_blocks(&self) -> anyhow::Result { + let info = self.clone().info().await?; + + Ok(info.block_features.iter().all(|x| x == "extended")) } } @@ -313,7 +174,6 @@ impl FirehoseEndpoint { compression_enabled: bool, subgraph_limit: SubgraphLimit, endpoint_metrics: Arc, - genesis_decoder: Box, ) -> Self { let uri = url .as_ref() @@ -376,7 +236,7 @@ impl FirehoseEndpoint { compression_enabled, subgraph_limit, endpoint_metrics, - genesis_decoder, + info_response: OnceCell::new(), } } @@ -391,12 +251,8 @@ impl FirehoseEndpoint { .get_capacity(Arc::strong_count(self).saturating_sub(1)) } - fn new_client( - &self, - ) -> FetchClient< - InterceptedService, impl tonic::service::Interceptor>, - > { - let metrics = MetricsInterceptor { + fn metrics_interceptor(&self) -> MetricsInterceptor { + MetricsInterceptor { metrics: self.endpoint_metrics.cheap_clone(), service: self.channel.cheap_clone(), labels: RequestLabels { @@ -404,18 +260,28 @@ impl FirehoseEndpoint { req_type: "unknown".into(), conn_type: ConnectionType::Firehose, }, - }; + } + } + + fn max_message_size(&self) -> usize { + 1024 * 1024 * ENV_VARS.firehose_grpc_max_decode_size_mb + } - let mut client: FetchClient< - InterceptedService, AuthInterceptor>, - > = FetchClient::with_interceptor(metrics, self.auth.clone()) + fn new_fetch_client( + &self, + ) -> FetchClient< + InterceptedService, impl tonic::service::Interceptor>, + > { + let metrics = self.metrics_interceptor(); + + let mut client = FetchClient::with_interceptor(metrics, self.auth.clone()) .accept_compressed(CompressionEncoding::Gzip); if self.compression_enabled { client = client.send_compressed(CompressionEncoding::Gzip); } - client = client - .max_decoding_message_size(1024 * 1024 * ENV_VARS.firehose_grpc_max_decode_size_mb); + + client = client.max_decoding_message_size(self.max_message_size()); client } @@ -425,15 +291,7 @@ impl FirehoseEndpoint { ) -> StreamClient< InterceptedService, impl tonic::service::Interceptor>, > { - let metrics = MetricsInterceptor { - metrics: self.endpoint_metrics.cheap_clone(), - service: self.channel.cheap_clone(), - labels: RequestLabels { - provider: self.provider.clone().into(), - req_type: "unknown".into(), - conn_type: ConnectionType::Firehose, - }, - }; + let metrics = self.metrics_interceptor(); let mut client = StreamClient::with_interceptor(metrics, self.auth.clone()) .accept_compressed(CompressionEncoding::Gzip); @@ -441,8 +299,8 @@ impl FirehoseEndpoint { if self.compression_enabled { client = client.send_compressed(CompressionEncoding::Gzip); } - client = client - .max_decoding_message_size(1024 * 1024 * ENV_VARS.firehose_grpc_max_decode_size_mb); + + client = client.max_decoding_message_size(self.max_message_size()); client } @@ -452,15 +310,7 @@ impl FirehoseEndpoint { ) -> substreams_rpc::stream_client::StreamClient< InterceptedService, impl tonic::service::Interceptor>, > { - let metrics = MetricsInterceptor { - metrics: self.endpoint_metrics.cheap_clone(), - service: self.channel.cheap_clone(), - labels: RequestLabels { - provider: self.provider.clone().into(), - req_type: "unknown".into(), - conn_type: ConnectionType::Substreams, - }, - }; + let metrics = self.metrics_interceptor(); let mut client = substreams_rpc::stream_client::StreamClient::with_interceptor( metrics, @@ -471,8 +321,8 @@ impl FirehoseEndpoint { if self.compression_enabled { client = client.send_compressed(CompressionEncoding::Gzip); } - client = client - .max_decoding_message_size(1024 * 1024 * ENV_VARS.firehose_grpc_max_decode_size_mb); + + client = client.max_decoding_message_size(self.max_message_size()); client } @@ -500,7 +350,7 @@ impl FirehoseEndpoint { )), }; - let mut client = self.new_client(); + let mut client = self.new_fetch_client(); match client.block(req).await { Ok(v) => Ok(M::decode( v.get_ref().block.as_ref().unwrap().value.as_ref(), @@ -509,20 +359,6 @@ impl FirehoseEndpoint { } } - pub async fn genesis_block_ptr(&self, logger: &Logger) -> Result - where - M: prost::Message + BlockchainBlock + Default + 'static, - { - info!(logger, "Requesting genesis block from firehose"; - "provider" => self.provider.as_str()); - - // We use 0 here to mean the genesis block of the chain. Firehose - // when seeing start block number 0 will always return the genesis - // block of the chain, even if the chain's start block number is - // not starting at block #0. - self.block_ptr_for_number::(logger, 0).await - } - pub async fn block_ptr_for_number( &self, logger: &Logger, @@ -629,33 +465,54 @@ impl FirehoseEndpoint { Ok(block_stream) } + + pub async fn info( + self: Arc, + ) -> Result { + let endpoint = self.cheap_clone(); + + self.info_response + .get_or_try_init(move || async move { + let metrics = endpoint.metrics_interceptor(); + let auth = endpoint.auth.clone(); + + let mut client = crate::firehose::endpoint_info::Client::new(metrics, auth); + + if endpoint.compression_enabled { + client = client.with_compression(); + } + + client = client.with_max_message_size(endpoint.max_message_size()); + + client.info().await + }) + .await + .map(ToOwned::to_owned) + } } -#[derive(Clone, Debug, Default)] -pub struct FirehoseEndpoints(ChainId, ProviderManager>); +#[derive(Debug)] +pub struct FirehoseEndpoints(ChainName, ProviderManager>); impl FirehoseEndpoints { pub fn for_testing(adapters: Vec>) -> Self { - use slog::{o, Discard}; - - use crate::components::adapter::NoopIdentValidator; - let chain_id: Word = "testing".into(); + let chain_name: ChainName = "testing".into(); Self( - chain_id.clone(), + chain_name.clone(), ProviderManager::new( - Logger::root(Discard, o!()), - vec![(chain_id, adapters)].into_iter(), - Arc::new(NoopIdentValidator), + crate::log::discard(), + [(chain_name, adapters)], + ProviderCheckStrategy::MarkAsValid, ), ) } pub fn new( - chain_id: ChainId, + chain_name: ChainName, provider_manager: ProviderManager>, ) -> Self { - Self(chain_id, provider_manager) + Self(chain_name, provider_manager) } pub fn len(&self) -> usize { @@ -668,9 +525,8 @@ impl FirehoseEndpoints { pub async fn endpoint(&self) -> anyhow::Result> { let endpoint = self .1 - .get_all(&self.0) + .providers(&self.0) .await? - .into_iter() .sorted_by_key(|x| x.current_error_count()) .try_fold(None, |acc, adapter| { match adapter.get_capacity() { @@ -700,13 +556,10 @@ mod test { use slog::{o, Discard, Logger}; - use crate::{ - components::{adapter::NetIdentifiable, metrics::MetricsRegistry}, - endpoint::EndpointMetrics, - firehose::{NoopGenesisDecoder, SubgraphLimit}, - }; - - use super::{AvailableCapacity, FirehoseEndpoint, FirehoseEndpoints, SUBGRAPHS_PER_CONN}; + use super::*; + use crate::components::metrics::MetricsRegistry; + use crate::endpoint::EndpointMetrics; + use crate::firehose::SubgraphLimit; #[tokio::test] async fn firehose_endpoint_errors() { @@ -719,7 +572,6 @@ mod test { false, SubgraphLimit::Unlimited, Arc::new(EndpointMetrics::mock()), - NoopGenesisDecoder::boxed(), ))]; let endpoints = FirehoseEndpoints::for_testing(endpoint); @@ -752,7 +604,6 @@ mod test { false, SubgraphLimit::Limit(2), Arc::new(EndpointMetrics::mock()), - NoopGenesisDecoder::boxed(), ))]; let endpoints = FirehoseEndpoints::for_testing(endpoint); @@ -780,7 +631,6 @@ mod test { false, SubgraphLimit::Disabled, Arc::new(EndpointMetrics::mock()), - NoopGenesisDecoder::boxed(), ))]; let endpoints = FirehoseEndpoints::for_testing(endpoint); @@ -807,7 +657,6 @@ mod test { false, SubgraphLimit::Unlimited, endpoint_metrics.clone(), - NoopGenesisDecoder::boxed(), )); let high_error_adapter2 = Arc::new(FirehoseEndpoint::new( "high_error".to_string(), @@ -818,7 +667,6 @@ mod test { false, SubgraphLimit::Unlimited, endpoint_metrics.clone(), - NoopGenesisDecoder::boxed(), )); let low_availability = Arc::new(FirehoseEndpoint::new( "low availability".to_string(), @@ -829,7 +677,6 @@ mod test { false, SubgraphLimit::Limit(2), endpoint_metrics.clone(), - NoopGenesisDecoder::boxed(), )); let high_availability = Arc::new(FirehoseEndpoint::new( "high availability".to_string(), @@ -840,7 +687,6 @@ mod test { false, SubgraphLimit::Unlimited, endpoint_metrics.clone(), - NoopGenesisDecoder::boxed(), )); endpoint_metrics.report_for_test(&high_error_adapter1.provider, false); diff --git a/graph/src/firehose/mod.rs b/graph/src/firehose/mod.rs index 2930d1ee560..9f4e8510c3b 100644 --- a/graph/src/firehose/mod.rs +++ b/graph/src/firehose/mod.rs @@ -1,4 +1,5 @@ mod codec; +mod endpoint_info; mod endpoints; mod helpers; mod interceptors; diff --git a/graph/src/firehose/sf.firehose.v2.rs b/graph/src/firehose/sf.firehose.v2.rs index 7727749282a..b0980b35531 100644 --- a/graph/src/firehose/sf.firehose.v2.rs +++ b/graph/src/firehose/sf.firehose.v2.rs @@ -104,6 +104,84 @@ pub struct Response { #[prost(string, tag = "10")] pub cursor: ::prost::alloc::string::String, } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct InfoRequest {} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct InfoResponse { + /// Canonical chain name from (ex: matic, mainnet ...). + #[prost(string, tag = "1")] + pub chain_name: ::prost::alloc::string::String, + /// Alternate names for the chain. + #[prost(string, repeated, tag = "2")] + pub chain_name_aliases: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, + /// First block that is served by this endpoint. + /// This should usually be the genesis block, but some providers may have truncated history. + #[prost(uint64, tag = "3")] + pub first_streamable_block_num: u64, + #[prost(string, tag = "4")] + pub first_streamable_block_id: ::prost::alloc::string::String, + /// This informs the client on how to decode the `block_id` field inside the `Block` message + /// as well as the `first_streamable_block_id` above. + #[prost(enumeration = "info_response::BlockIdEncoding", tag = "5")] + pub block_id_encoding: i32, + /// Features describes the blocks. + /// Popular values for EVM chains include "base", "extended" or "hybrid". + #[prost(string, repeated, tag = "10")] + pub block_features: ::prost::alloc::vec::Vec<::prost::alloc::string::String>, +} +/// Nested message and enum types in `InfoResponse`. +pub mod info_response { + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum BlockIdEncoding { + Unset = 0, + Hex = 1, + BlockIdEncoding0xHex = 2, + Base58 = 3, + Base64 = 4, + Base64url = 5, + } + impl BlockIdEncoding { + /// String value of the enum field names used in the ProtoBuf definition. + /// + /// The values are not transformed in any way and thus are considered stable + /// (if the ProtoBuf definition does not change) and safe for programmatic use. + pub fn as_str_name(&self) -> &'static str { + match self { + BlockIdEncoding::Unset => "BLOCK_ID_ENCODING_UNSET", + BlockIdEncoding::Hex => "BLOCK_ID_ENCODING_HEX", + BlockIdEncoding::BlockIdEncoding0xHex => "BLOCK_ID_ENCODING_0X_HEX", + BlockIdEncoding::Base58 => "BLOCK_ID_ENCODING_BASE58", + BlockIdEncoding::Base64 => "BLOCK_ID_ENCODING_BASE64", + BlockIdEncoding::Base64url => "BLOCK_ID_ENCODING_BASE64URL", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "BLOCK_ID_ENCODING_UNSET" => Some(Self::Unset), + "BLOCK_ID_ENCODING_HEX" => Some(Self::Hex), + "BLOCK_ID_ENCODING_0X_HEX" => Some(Self::BlockIdEncoding0xHex), + "BLOCK_ID_ENCODING_BASE58" => Some(Self::Base58), + "BLOCK_ID_ENCODING_BASE64" => Some(Self::Base64), + "BLOCK_ID_ENCODING_BASE64URL" => Some(Self::Base64url), + _ => None, + } + } + } +} #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)] #[repr(i32)] pub enum ForkStep { @@ -364,6 +442,115 @@ pub mod fetch_client { } } } +/// Generated client implementations. +pub mod endpoint_info_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + #[derive(Debug, Clone)] + pub struct EndpointInfoClient { + inner: tonic::client::Grpc, + } + impl EndpointInfoClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl EndpointInfoClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> EndpointInfoClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + EndpointInfoClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + pub async fn info( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/sf.firehose.v2.EndpointInfo/Info", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("sf.firehose.v2.EndpointInfo", "Info")); + self.inner.unary(req, path, codec).await + } + } +} /// Generated server implementations. pub mod stream_server { #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] @@ -726,3 +913,179 @@ pub mod fetch_server { const NAME: &'static str = "sf.firehose.v2.Fetch"; } } +/// Generated server implementations. +pub mod endpoint_info_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with EndpointInfoServer. + #[async_trait] + pub trait EndpointInfo: Send + Sync + 'static { + async fn info( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + } + #[derive(Debug)] + pub struct EndpointInfoServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl EndpointInfoServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for EndpointInfoServer + where + T: EndpointInfo, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/sf.firehose.v2.EndpointInfo/Info" => { + #[allow(non_camel_case_types)] + struct InfoSvc(pub Arc); + impl tonic::server::UnaryService + for InfoSvc { + type Response = super::InfoResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::info(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = InfoSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for EndpointInfoServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for EndpointInfoServer { + const NAME: &'static str = "sf.firehose.v2.EndpointInfo"; + } +} diff --git a/graph/src/lib.rs b/graph/src/lib.rs index fe1b6949642..04872aab196 100644 --- a/graph/src/lib.rs +++ b/graph/src/lib.rs @@ -113,7 +113,6 @@ pub mod prelude { pub use crate::blockchain::{BlockHash, BlockPtr}; - pub use crate::components::adapter; pub use crate::components::ethereum::{ EthereumBlock, EthereumBlockWithCalls, EthereumCall, LightEthereumBlock, LightEthereumBlockExt, diff --git a/node/src/bin/manager.rs b/node/src/bin/manager.rs index 88979cd4413..afc09403357 100644 --- a/node/src/bin/manager.rs +++ b/node/src/bin/manager.rs @@ -4,7 +4,7 @@ use git_testament::{git_testament, render_testament}; use graph::bail; use graph::blockchain::BlockHash; use graph::cheap_clone::CheapClone; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::endpoint::EndpointMetrics; use graph::env::ENV_VARS; use graph::log::logger_with_levels; @@ -440,9 +440,13 @@ pub enum ConfigCommand { network: String, }, - /// Compare the NetIdentifier of all defined adapters with the existing - /// identifiers on the ChainStore. - CheckProviders {}, + /// Run all available provider checks against all providers. + CheckProviders { + /// Maximum duration of all provider checks for a provider. + /// + /// Defaults to 60 seconds. + timeout_seconds: Option, + }, /// Show subgraph-specific settings /// @@ -1006,11 +1010,12 @@ impl Context { )) } - async fn networks(&self, block_store: Arc) -> anyhow::Result { + async fn networks(&self) -> anyhow::Result { let logger = self.logger.clone(); let registry = self.metrics_registry(); let metrics = Arc::new(EndpointMetrics::mock()); - Networks::from_config(logger, &self.config, registry, metrics, block_store, false).await + + Networks::from_config(logger, &self.config, registry, metrics, &[]).await } fn chain_store(self, chain_name: &str) -> anyhow::Result> { @@ -1025,8 +1030,7 @@ impl Context { self, chain_name: &str, ) -> anyhow::Result<(Arc, Arc)> { - let block_store = self.store().block_store(); - let networks = self.networks(block_store).await?; + let networks = self.networks().await?; let chain_store = self.chain_store(chain_name)?; let ethereum_adapter = networks .ethereum_rpcs(chain_name.into()) @@ -1167,10 +1171,15 @@ async fn main() -> anyhow::Result<()> { use ConfigCommand::*; match cmd { - CheckProviders {} => { + CheckProviders { timeout_seconds } => { + let logger = ctx.logger.clone(); + let networks = ctx.networks().await?; let store = ctx.store().block_store(); - let networks = ctx.networks(store.cheap_clone()).await?; - Ok(commands::config::check_provider_genesis(&networks, store).await) + let timeout = Duration::from_secs(timeout_seconds.unwrap_or(60)); + + commands::provider_checks::execute(&logger, &networks, store, timeout).await; + + Ok(()) } Place { name, network } => { commands::config::place(&ctx.config.deployment, &name, &network) @@ -1367,8 +1376,8 @@ async fn main() -> anyhow::Result<()> { } => { let store_builder = ctx.store_builder().await; let store = ctx.store().block_store(); - let networks = ctx.networks(store.cheap_clone()).await?; - let chain_id = ChainId::from(chain_name); + let networks = ctx.networks().await?; + let chain_id = ChainName::from(chain_name); let block_hash = BlockHash::from_str(&block_hash)?; commands::chain::update_chain_genesis( &networks, diff --git a/node/src/chain.rs b/node/src/chain.rs index 1be5761e77e..6de493631cd 100644 --- a/node/src/chain.rs +++ b/node/src/chain.rs @@ -15,15 +15,12 @@ use graph::blockchain::{ ChainIdentifier, }; use graph::cheap_clone::CheapClone; -use graph::components::adapter::ChainId; +use graph::components::network_provider::ChainName; use graph::components::store::{BlockStore as _, ChainStore}; use graph::data::store::NodeId; use graph::endpoint::EndpointMetrics; use graph::env::{EnvVars, ENV_VARS}; -use graph::firehose::{ - FirehoseEndpoint, FirehoseGenesisDecoder, GenesisDecoder, SubgraphLimit, - SubstreamsGenesisDecoder, -}; +use graph::firehose::{FirehoseEndpoint, SubgraphLimit}; use graph::futures03::future::try_join_all; use graph::itertools::Itertools; use graph::log::factory::LoggerFactory; @@ -63,11 +60,11 @@ pub fn create_substreams_networks( config.chains.ingestor, ); - let mut networks_by_kind: BTreeMap<(BlockchainKind, ChainId), Vec>> = + let mut networks_by_kind: BTreeMap<(BlockchainKind, ChainName), Vec>> = BTreeMap::new(); for (name, chain) in &config.chains.chains { - let name: ChainId = name.as_str().into(); + let name: ChainName = name.as_str().into(); for provider in &chain.providers { if let ProviderDetails::Substreams(ref firehose) = provider.details { info!( @@ -93,7 +90,6 @@ pub fn create_substreams_networks( firehose.compression_enabled(), SubgraphLimit::Unlimited, endpoint_metrics.clone(), - Box::new(SubstreamsGenesisDecoder {}), ))); } } @@ -124,11 +120,11 @@ pub fn create_firehose_networks( config.chains.ingestor, ); - let mut networks_by_kind: BTreeMap<(BlockchainKind, ChainId), Vec>> = + let mut networks_by_kind: BTreeMap<(BlockchainKind, ChainName), Vec>> = BTreeMap::new(); for (name, chain) in &config.chains.chains { - let name: ChainId = name.as_str().into(); + let name: ChainName = name.as_str().into(); for provider in &chain.providers { let logger = logger.cheap_clone(); if let ProviderDetails::Firehose(ref firehose) = provider.details { @@ -143,27 +139,6 @@ pub fn create_firehose_networks( .entry((chain.protocol, name.clone())) .or_insert_with(Vec::new); - let decoder: Box = match chain.protocol { - BlockchainKind::Arweave => { - FirehoseGenesisDecoder::::new(logger) - } - BlockchainKind::Ethereum => { - FirehoseGenesisDecoder::::new(logger) - } - BlockchainKind::Near => { - FirehoseGenesisDecoder::::new(logger) - } - BlockchainKind::Cosmos => { - FirehoseGenesisDecoder::::new(logger) - } - BlockchainKind::Substreams => { - unreachable!("Substreams configuration should not be handled here"); - } - BlockchainKind::Starknet => { - FirehoseGenesisDecoder::::new(logger) - } - }; - // Create n FirehoseEndpoints where n is the size of the pool. If a // subgraph limit is defined for this endpoint then each endpoint // instance will have their own subgraph limit. @@ -182,7 +157,6 @@ pub fn create_firehose_networks( firehose.compression_enabled(), firehose.limit_for(&config.node), endpoint_metrics.cheap_clone(), - decoder.box_clone(), ))); } } @@ -384,7 +358,7 @@ pub async fn networks_as_chains( async fn add_substreams( networks: &Networks, config: &Arc, - chain_id: ChainId, + chain_id: ChainName, blockchain_map: &mut BlockchainMap, logger_factory: LoggerFactory, chain_store: Arc, @@ -608,7 +582,7 @@ pub async fn networks_as_chains( mod test { use crate::config::{Config, Opt}; use crate::network_setup::{AdapterConfiguration, Networks}; - use graph::components::adapter::{ChainId, NoopIdentValidator}; + use graph::components::network_provider::ChainName; use graph::endpoint::EndpointMetrics; use graph::log::logger; use graph::prelude::{tokio, MetricsRegistry}; @@ -641,23 +615,15 @@ mod test { let metrics = Arc::new(EndpointMetrics::mock()); let config = Config::load(&logger, &opt).expect("can create config"); let metrics_registry = Arc::new(MetricsRegistry::mock()); - let ident_validator = Arc::new(NoopIdentValidator); - let networks = Networks::from_config( - logger, - &config, - metrics_registry, - metrics, - ident_validator, - false, - ) - .await - .expect("can parse config"); + let networks = Networks::from_config(logger, &config, metrics_registry, metrics, &[]) + .await + .expect("can parse config"); let mut network_names = networks .adapters .iter() .map(|a| a.chain_id()) - .collect::>(); + .collect::>(); network_names.sort(); let traces = NodeCapabilities { diff --git a/node/src/config.rs b/node/src/config.rs index 93aab34ee8c..8006b8efef7 100644 --- a/node/src/config.rs +++ b/node/src/config.rs @@ -1,7 +1,7 @@ use graph::{ anyhow::Error, blockchain::BlockchainKind, - components::adapter::ChainId, + components::network_provider::ChainName, env::ENV_VARS, firehose::{SubgraphLimit, SUBGRAPHS_PER_CONN}, itertools::Itertools, @@ -104,7 +104,7 @@ fn validate_name(s: &str) -> Result<()> { } impl Config { - pub fn chain_ids(&self) -> Vec { + pub fn chain_ids(&self) -> Vec { self.chains .chains .keys() diff --git a/node/src/main.rs b/node/src/main.rs index 80622fdbf61..870cce97318 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,6 +1,5 @@ use clap::Parser as _; use git_testament::{git_testament, render_testament}; -use graph::components::adapter::{IdentValidator, NoopIdentValidator}; use graph::futures01::Future as _; use graph::futures03::compat::Future01CompatExt; use graph::futures03::future::TryFutureExt; @@ -273,21 +272,31 @@ async fn main() { start_graphman_server(opt.graphman_port, graphman_server_config).await; let launch_services = |logger: Logger, env_vars: Arc| async move { + use graph::components::network_provider; + let block_store = network_store.block_store(); - let validator: Arc = if env_vars.genesis_validation_enabled { - network_store.block_store() - } else { - Arc::new(NoopIdentValidator {}) - }; + let mut provider_checks: Vec> = Vec::new(); + + if env_vars.genesis_validation_enabled { + provider_checks.push(Arc::new(network_provider::GenesisHashCheck::new( + block_store.clone(), + ))); + } + + provider_checks.push(Arc::new(network_provider::ExtendedBlocksCheck::new( + env_vars + .firehose_disable_extended_blocks_for_chains + .iter() + .map(|x| x.as_str().into()), + ))); let network_adapters = Networks::from_config( logger.cheap_clone(), &config, metrics_registry.cheap_clone(), endpoint_metrics, - validator, - env_vars.genesis_validation_enabled, + &provider_checks, ) .await .expect("unable to parse network configuration"); diff --git a/node/src/manager/commands/chain.rs b/node/src/manager/commands/chain.rs index b951f41f4b5..f1bdf7d39b9 100644 --- a/node/src/manager/commands/chain.rs +++ b/node/src/manager/commands/chain.rs @@ -7,8 +7,8 @@ use graph::blockchain::BlockHash; use graph::blockchain::BlockPtr; use graph::blockchain::ChainIdentifier; use graph::cheap_clone::CheapClone; -use graph::components::adapter::ChainId; -use graph::components::adapter::IdentValidator; +use graph::components::network_provider::ChainIdentifierStore; +use graph::components::network_provider::ChainName; use graph::components::store::StoreError; use graph::prelude::BlockNumber; use graph::prelude::ChainStore as _; @@ -161,7 +161,7 @@ pub async fn update_chain_genesis( coord: Arc, store: Arc, logger: &Logger, - chain_id: ChainId, + chain_id: ChainName, genesis_hash: BlockHash, force: bool, ) -> Result<(), Error> { @@ -183,7 +183,7 @@ pub async fn update_chain_genesis( // Update the local shard's genesis, whether or not it is the primary. // The chains table is replicated from the primary and keeps another genesis hash. // To keep those in sync we need to update the primary and then refresh the shard tables. - store.update_ident( + store.update_identifier( &chain_id, &ChainIdentifier { net_version: ident.net_version.clone(), diff --git a/node/src/manager/commands/config.rs b/node/src/manager/commands/config.rs index 0a80cc9fe22..8b6d36e9afa 100644 --- a/node/src/manager/commands/config.rs +++ b/node/src/manager/commands/config.rs @@ -1,11 +1,9 @@ use std::{collections::BTreeMap, sync::Arc}; +use graph::components::network_provider::ChainName; use graph::{ anyhow::{bail, Context}, - components::{ - adapter::{ChainId, IdentValidator, IdentValidatorError, NoopIdentValidator, ProviderName}, - subgraph::{Setting, Settings}, - }, + components::subgraph::{Setting, Settings}, endpoint::EndpointMetrics, env::EnvVars, itertools::Itertools, @@ -16,40 +14,10 @@ use graph::{ slog::Logger, }; use graph_chain_ethereum::NodeCapabilities; -use graph_store_postgres::{BlockStore, DeploymentPlacer}; +use graph_store_postgres::DeploymentPlacer; use crate::{config::Config, network_setup::Networks}; -/// Compare the NetIdentifier of all defined adapters with the existing -/// identifiers on the ChainStore. If a ChainStore doesn't exist it will be show -/// as an error. It's intended to be run again an environment that has already -/// been setup by graph-node. -pub async fn check_provider_genesis(networks: &Networks, store: Arc) { - println!("Checking providers"); - for (chain_id, ids) in networks.all_chain_identifiers().await.into_iter() { - let (_oks, errs): (Vec<_>, Vec<_>) = ids - .into_iter() - .map(|(provider, id)| { - id.map_err(IdentValidatorError::from) - .and_then(|id| store.check_ident(chain_id, &id)) - .map_err(|e| (provider, e)) - }) - .partition_result(); - let errs = errs - .into_iter() - .dedup_by(|e1, e2| e1.eq(e2)) - .collect::>(); - - if errs.is_empty() { - println!("chain_id: {}: status: OK", chain_id); - continue; - } - - println!("chain_id: {}: status: NOK", chain_id); - println!("errors: {:?}", errs); - } -} - pub fn place(placer: &dyn DeploymentPlacer, name: &str, network: &str) -> Result<(), Error> { match placer.place(name, network).map_err(|s| anyhow!(s))? { None => { @@ -171,16 +139,8 @@ pub async fn provider( let metrics = Arc::new(EndpointMetrics::mock()); let caps = caps_from_features(features)?; - let networks = Networks::from_config( - logger, - &config, - registry, - metrics, - Arc::new(NoopIdentValidator), - false, - ) - .await?; - let network: ChainId = network.into(); + let networks = Networks::from_config(logger, &config, registry, metrics, &[]).await?; + let network: ChainName = network.into(); let adapters = networks.ethereum_rpcs(network.clone()); let adapters = adapters.all_cheapest_with(&caps).await; diff --git a/node/src/manager/commands/mod.rs b/node/src/manager/commands/mod.rs index 127966879c9..cb81a19ecb3 100644 --- a/node/src/manager/commands/mod.rs +++ b/node/src/manager/commands/mod.rs @@ -10,6 +10,7 @@ pub mod deployment; pub mod drop; pub mod index; pub mod listen; +pub mod provider_checks; pub mod prune; pub mod query; pub mod remove; diff --git a/node/src/manager/commands/provider_checks.rs b/node/src/manager/commands/provider_checks.rs new file mode 100644 index 00000000000..7f4feca928e --- /dev/null +++ b/node/src/manager/commands/provider_checks.rs @@ -0,0 +1,146 @@ +use std::sync::Arc; +use std::time::Duration; + +use graph::components::network_provider::ChainIdentifierStore; +use graph::components::network_provider::ChainName; +use graph::components::network_provider::ExtendedBlocksCheck; +use graph::components::network_provider::GenesisHashCheck; +use graph::components::network_provider::NetworkDetails; +use graph::components::network_provider::ProviderCheck; +use graph::components::network_provider::ProviderCheckStatus; +use graph::prelude::tokio; +use graph::prelude::Logger; +use graph_store_postgres::BlockStore; +use itertools::Itertools; + +use crate::network_setup::Networks; + +pub async fn execute( + logger: &Logger, + networks: &Networks, + store: Arc, + timeout: Duration, +) { + let chain_name_iter = networks + .adapters + .iter() + .map(|a| a.chain_id()) + .sorted() + .dedup(); + + for chain_name in chain_name_iter { + let mut errors = Vec::new(); + + for adapter in networks + .rpc_provider_manager + .providers_unchecked(chain_name) + .unique_by(|x| x.provider_name()) + { + match tokio::time::timeout( + timeout, + run_checks(logger, chain_name, adapter, store.clone()), + ) + .await + { + Ok(result) => { + errors.extend(result); + } + Err(_) => { + errors.push("Timeout".to_owned()); + } + } + } + + for adapter in networks + .firehose_provider_manager + .providers_unchecked(chain_name) + .unique_by(|x| x.provider_name()) + { + match tokio::time::timeout( + timeout, + run_checks(logger, chain_name, adapter, store.clone()), + ) + .await + { + Ok(result) => { + errors.extend(result); + } + Err(_) => { + errors.push("Timeout".to_owned()); + } + } + } + + for adapter in networks + .substreams_provider_manager + .providers_unchecked(chain_name) + .unique_by(|x| x.provider_name()) + { + match tokio::time::timeout( + timeout, + run_checks(logger, chain_name, adapter, store.clone()), + ) + .await + { + Ok(result) => { + errors.extend(result); + } + Err(_) => { + errors.push("Timeout".to_owned()); + } + } + } + + if errors.is_empty() { + println!("Chain: {chain_name}; Status: OK"); + continue; + } + + println!("Chain: {chain_name}; Status: ERROR"); + for error in errors.into_iter().unique() { + println!("ERROR: {error}"); + } + } +} + +async fn run_checks( + logger: &Logger, + chain_name: &ChainName, + adapter: &dyn NetworkDetails, + store: Arc, +) -> Vec { + let provider_name = adapter.provider_name(); + + let mut errors = Vec::new(); + + let genesis_check = GenesisHashCheck::new(store); + + let status = genesis_check + .check(logger, chain_name, &provider_name, adapter) + .await; + + errors_from_status(status, &mut errors); + + let blocks_check = ExtendedBlocksCheck::new([]); + + let status = blocks_check + .check(logger, chain_name, &provider_name, adapter) + .await; + + errors_from_status(status, &mut errors); + + errors +} + +fn errors_from_status(status: ProviderCheckStatus, out: &mut Vec) { + match status { + ProviderCheckStatus::NotChecked => {} + ProviderCheckStatus::TemporaryFailure { message, .. } => { + out.push(message); + } + ProviderCheckStatus::Valid => {} + ProviderCheckStatus::Failed { message, .. } => { + out.push(message); + } + } +} diff --git a/node/src/manager/commands/run.rs b/node/src/manager/commands/run.rs index 1a9e7d4353b..2c6bfdcb148 100644 --- a/node/src/manager/commands/run.rs +++ b/node/src/manager/commands/run.rs @@ -9,8 +9,8 @@ use crate::store_builder::StoreBuilder; use crate::MetricsContext; use graph::anyhow::bail; use graph::cheap_clone::CheapClone; -use graph::components::adapter::IdentValidator; use graph::components::link_resolver::{ArweaveClient, FileSizeLimit}; +use graph::components::network_provider::ChainIdentifierStore; use graph::components::store::DeploymentLocator; use graph::components::subgraph::Settings; use graph::endpoint::EndpointMetrics; @@ -93,14 +93,33 @@ pub async fn run( let chain_head_update_listener = store_builder.chain_head_update_listener(); let network_store = store_builder.network_store(config.chain_ids()); let block_store = network_store.block_store(); - let ident_validator: Arc = network_store.block_store(); + + let mut provider_checks: Vec> = + Vec::new(); + + if env_vars.genesis_validation_enabled { + let store: Arc = network_store.block_store(); + + provider_checks.push(Arc::new( + graph::components::network_provider::GenesisHashCheck::new(store), + )); + } + + provider_checks.push(Arc::new( + graph::components::network_provider::ExtendedBlocksCheck::new( + env_vars + .firehose_disable_extended_blocks_for_chains + .iter() + .map(|x| x.as_str().into()), + ), + )); + let networks = Networks::from_config( logger.cheap_clone(), &config, metrics_registry.cheap_clone(), endpoint_metrics, - ident_validator, - env_vars.genesis_validation_enabled, + &provider_checks, ) .await .expect("unable to parse network configuration"); diff --git a/node/src/network_setup.rs b/node/src/network_setup.rs index d6717b850cf..6114b6eefd3 100644 --- a/node/src/network_setup.rs +++ b/node/src/network_setup.rs @@ -2,19 +2,18 @@ use ethereum::{ network::{EthereumNetworkAdapter, EthereumNetworkAdapters}, BlockIngestor, }; +use graph::components::network_provider::ChainName; +use graph::components::network_provider::NetworkDetails; +use graph::components::network_provider::ProviderCheck; +use graph::components::network_provider::ProviderCheckStrategy; +use graph::components::network_provider::ProviderManager; use graph::{ anyhow::{self, bail}, blockchain::{Blockchain, BlockchainKind, BlockchainMap, ChainIdentifier}, cheap_clone::CheapClone, - components::{ - adapter::{ - ChainId, IdentValidator, NetIdentifiable, NoopIdentValidator, ProviderManager, - ProviderName, - }, - metrics::MetricsRegistry, - }, + components::metrics::MetricsRegistry, endpoint::EndpointMetrics, - env::{EnvVars, ENV_VARS}, + env::EnvVars, firehose::{FirehoseEndpoint, FirehoseEndpoints}, futures03::future::TryFutureExt, itertools::Itertools, @@ -37,7 +36,7 @@ use crate::chain::{ #[derive(Debug, Clone)] pub struct EthAdapterConfig { - pub chain_id: ChainId, + pub chain_id: ChainName, pub adapters: Vec, pub call_only: Vec, // polling interval is set per chain so if set all adapter configuration will have @@ -47,7 +46,7 @@ pub struct EthAdapterConfig { #[derive(Debug, Clone)] pub struct FirehoseAdapterConfig { - pub chain_id: ChainId, + pub chain_id: ChainName, pub kind: BlockchainKind, pub adapters: Vec>, } @@ -66,7 +65,7 @@ impl AdapterConfiguration { AdapterConfiguration::Firehose(fh) | AdapterConfiguration::Substreams(fh) => &fh.kind, } } - pub fn chain_id(&self) -> &ChainId { + pub fn chain_id(&self) -> &ChainName { match self { AdapterConfiguration::Rpc(EthAdapterConfig { chain_id, .. }) | AdapterConfiguration::Firehose(FirehoseAdapterConfig { chain_id, .. }) @@ -106,9 +105,9 @@ impl AdapterConfiguration { pub struct Networks { pub adapters: Vec, - rpc_provider_manager: ProviderManager, - firehose_provider_manager: ProviderManager>, - substreams_provider_manager: ProviderManager>, + pub rpc_provider_manager: ProviderManager, + pub firehose_provider_manager: ProviderManager>, + pub substreams_provider_manager: ProviderManager>, } impl Networks { @@ -119,78 +118,34 @@ impl Networks { rpc_provider_manager: ProviderManager::new( Logger::root(Discard, o!()), vec![].into_iter(), - Arc::new(NoopIdentValidator), + ProviderCheckStrategy::MarkAsValid, ), firehose_provider_manager: ProviderManager::new( Logger::root(Discard, o!()), vec![].into_iter(), - Arc::new(NoopIdentValidator), + ProviderCheckStrategy::MarkAsValid, ), substreams_provider_manager: ProviderManager::new( Logger::root(Discard, o!()), vec![].into_iter(), - Arc::new(NoopIdentValidator), + ProviderCheckStrategy::MarkAsValid, ), } } - /// Gets the chain identifier from all providers for every chain. - /// This function is intended for checking the status of providers and - /// whether they match their store counterparts more than for general - /// graph-node use. It may trigger verification (which would add delays on hot paths) - /// and it will also make calls on potentially unveried providers (this means the providers - /// have not been checked for correct net_version and genesis block hash) - pub async fn all_chain_identifiers( - &self, - ) -> Vec<( - &ChainId, - Vec<(ProviderName, Result)>, - )> { - let timeout = ENV_VARS.genesis_validation_timeout; - let mut out = vec![]; - for chain_id in self.adapters.iter().map(|a| a.chain_id()).sorted().dedup() { - let mut inner = vec![]; - for adapter in self.rpc_provider_manager.get_all_unverified(chain_id) { - inner.push(( - adapter.provider_name(), - adapter.net_identifiers_with_timeout(timeout).await, - )); - } - for adapter in self.firehose_provider_manager.get_all_unverified(chain_id) { - inner.push(( - adapter.provider_name(), - adapter.net_identifiers_with_timeout(timeout).await, - )); - } - for adapter in self - .substreams_provider_manager - .get_all_unverified(chain_id) - { - inner.push(( - adapter.provider_name(), - adapter.net_identifiers_with_timeout(timeout).await, - )); - } - - out.push((chain_id, inner)); - } - - out - } - pub async fn chain_identifier( &self, logger: &Logger, - chain_id: &ChainId, + chain_id: &ChainName, ) -> Result { - async fn get_identifier( + async fn get_identifier( pm: ProviderManager, logger: &Logger, - chain_id: &ChainId, + chain_id: &ChainName, provider_type: &str, ) -> Result { - for adapter in pm.get_all_unverified(chain_id) { - match adapter.net_identifiers().await { + for adapter in pm.providers_unchecked(chain_id) { + match adapter.chain_identifier().await { Ok(ident) => return Ok(ident), Err(err) => { warn!( @@ -208,29 +163,24 @@ impl Networks { bail!("no working adapters for chain {}", chain_id); } - get_identifier( - self.rpc_provider_manager.cheap_clone(), - logger, - chain_id, - "rpc", - ) - .or_else(|_| { - get_identifier( - self.firehose_provider_manager.cheap_clone(), - logger, - chain_id, - "firehose", - ) - }) - .or_else(|_| { - get_identifier( - self.substreams_provider_manager.cheap_clone(), - logger, - chain_id, - "substreams", - ) - }) - .await + get_identifier(self.rpc_provider_manager.clone(), logger, chain_id, "rpc") + .or_else(|_| { + get_identifier( + self.firehose_provider_manager.clone(), + logger, + chain_id, + "firehose", + ) + }) + .or_else(|_| { + get_identifier( + self.substreams_provider_manager.clone(), + logger, + chain_id, + "substreams", + ) + }) + .await } pub async fn from_config( @@ -238,8 +188,7 @@ impl Networks { config: &crate::config::Config, registry: Arc, endpoint_metrics: Arc, - store: Arc, - genesis_validation_enabled: bool, + provider_checks: &[Arc], ) -> Result { if config.query_only(&config.node) { return Ok(Networks::noop()); @@ -265,19 +214,13 @@ impl Networks { .chain(substreams.into_iter()) .collect(); - Ok(Networks::new( - &logger, - adapters, - store, - genesis_validation_enabled, - )) + Ok(Networks::new(&logger, adapters, provider_checks)) } fn new( logger: &Logger, adapters: Vec, - validator: Arc, - genesis_validation_enabled: bool, + provider_checks: &[Arc], ) -> Self { let adapters2 = adapters.clone(); let eth_adapters = adapters.iter().flat_map(|a| a.as_rpc()).cloned().map( @@ -328,37 +271,24 @@ impl Networks { rpc_provider_manager: ProviderManager::new( logger.clone(), eth_adapters, - validator.cheap_clone(), + ProviderCheckStrategy::RequireAll(provider_checks), ), firehose_provider_manager: ProviderManager::new( logger.clone(), firehose_adapters .into_iter() .map(|(chain_id, endpoints)| (chain_id, endpoints)), - validator.cheap_clone(), + ProviderCheckStrategy::RequireAll(provider_checks), ), substreams_provider_manager: ProviderManager::new( logger.clone(), substreams_adapters .into_iter() .map(|(chain_id, endpoints)| (chain_id, endpoints)), - validator.cheap_clone(), + ProviderCheckStrategy::RequireAll(provider_checks), ), }; - if !genesis_validation_enabled { - let (r, f, s) = ( - s.rpc_provider_manager.clone(), - s.firehose_provider_manager.clone(), - s.substreams_provider_manager.clone(), - ); - graph::spawn(async move { - r.mark_all_valid().await; - f.mark_all_valid().await; - s.mark_all_valid().await; - }); - } - s } @@ -368,7 +298,7 @@ impl Networks { ) -> anyhow::Result>> { async fn block_ingestor( logger: &Logger, - chain_id: &ChainId, + chain_id: &ChainName, chain: &Arc, ingestors: &mut Vec>, ) -> anyhow::Result<()> { @@ -461,15 +391,15 @@ impl Networks { bm } - pub fn firehose_endpoints(&self, chain_id: ChainId) -> FirehoseEndpoints { - FirehoseEndpoints::new(chain_id, self.firehose_provider_manager.cheap_clone()) + pub fn firehose_endpoints(&self, chain_id: ChainName) -> FirehoseEndpoints { + FirehoseEndpoints::new(chain_id, self.firehose_provider_manager.clone()) } - pub fn substreams_endpoints(&self, chain_id: ChainId) -> FirehoseEndpoints { - FirehoseEndpoints::new(chain_id, self.substreams_provider_manager.cheap_clone()) + pub fn substreams_endpoints(&self, chain_id: ChainName) -> FirehoseEndpoints { + FirehoseEndpoints::new(chain_id, self.substreams_provider_manager.clone()) } - pub fn ethereum_rpcs(&self, chain_id: ChainId) -> EthereumNetworkAdapters { + pub fn ethereum_rpcs(&self, chain_id: ChainName) -> EthereumNetworkAdapters { let eth_adapters = self .adapters .iter() @@ -480,7 +410,7 @@ impl Networks { EthereumNetworkAdapters::new( chain_id, - self.rpc_provider_manager.cheap_clone(), + self.rpc_provider_manager.clone(), eth_adapters, None, ) diff --git a/store/postgres/src/block_store.rs b/store/postgres/src/block_store.rs index c80850ad005..efaca838d59 100644 --- a/store/postgres/src/block_store.rs +++ b/store/postgres/src/block_store.rs @@ -10,12 +10,10 @@ use diesel::{ r2d2::{ConnectionManager, PooledConnection}, sql_query, ExpressionMethods as _, PgConnection, RunQueryDsl, }; +use graph::components::network_provider::ChainName; use graph::{ blockchain::ChainIdentifier, - components::{ - adapter::ChainId, - store::{BlockStore as BlockStoreTrait, QueryPermit}, - }, + components::store::{BlockStore as BlockStoreTrait, QueryPermit}, prelude::{error, info, BlockNumber, BlockPtr, Logger, ENV_VARS}, slog::o, }; @@ -540,7 +538,7 @@ impl BlockStore { /// has to be refreshed afterwards for the update to be reflected. pub fn set_chain_identifier( &self, - chain_id: ChainId, + chain_id: ChainName, ident: &ChainIdentifier, ) -> Result<(), StoreError> { use primary::chains as c; diff --git a/tests/src/fixture/mod.rs b/tests/src/fixture/mod.rs index 6da24511615..4eb5fbb42b1 100644 --- a/tests/src/fixture/mod.rs +++ b/tests/src/fixture/mod.rs @@ -17,9 +17,9 @@ use graph::blockchain::{ TriggersAdapter, TriggersAdapterSelector, }; use graph::cheap_clone::CheapClone; -use graph::components::adapter::ChainId; use graph::components::link_resolver::{ArweaveClient, ArweaveResolver, FileSizeLimit}; use graph::components::metrics::MetricsRegistry; +use graph::components::network_provider::ChainName; use graph::components::store::{BlockStore, DeploymentLocator, EthereumCallCache}; use graph::components::subgraph::Settings; use graph::data::graphql::load_manager::LoadManager; @@ -27,7 +27,7 @@ use graph::data::query::{Query, QueryTarget}; use graph::data::subgraph::schema::{SubgraphError, SubgraphHealth}; use graph::endpoint::EndpointMetrics; use graph::env::EnvVars; -use graph::firehose::{FirehoseEndpoint, FirehoseEndpoints, NoopGenesisDecoder, SubgraphLimit}; +use graph::firehose::{FirehoseEndpoint, FirehoseEndpoints, SubgraphLimit}; use graph::futures03::{Stream, StreamExt}; use graph::http_body_util::Full; use graph::hyper::body::Bytes; @@ -107,7 +107,6 @@ impl CommonChainConfig { false, SubgraphLimit::Unlimited, Arc::new(EndpointMetrics::mock()), - NoopGenesisDecoder::boxed(), ))]); Self { @@ -361,7 +360,7 @@ impl Drop for TestContext { } pub struct Stores { - network_name: ChainId, + network_name: ChainName, chain_head_listener: Arc, pub network_store: Arc, chain_store: Arc, @@ -400,7 +399,7 @@ pub async fn stores(test_name: &str, store_config_path: &str) -> Stores { let store_builder = StoreBuilder::new(&logger, &node_id, &config, None, mock_registry.clone()).await; - let network_name: ChainId = config + let network_name: ChainName = config .chains .chains .iter() @@ -410,7 +409,7 @@ pub async fn stores(test_name: &str, store_config_path: &str) -> Stores { .as_str() .into(); let chain_head_listener = store_builder.chain_head_update_listener(); - let network_identifiers: Vec = vec![network_name.clone()].into_iter().collect(); + let network_identifiers: Vec = vec![network_name.clone()].into_iter().collect(); let network_store = store_builder.network_store(network_identifiers); let ident = ChainIdentifier { net_version: "".into(), diff --git a/tests/src/fixture/substreams.rs b/tests/src/fixture/substreams.rs index a050e68db4e..f94fdfa95ec 100644 --- a/tests/src/fixture/substreams.rs +++ b/tests/src/fixture/substreams.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use graph::{blockchain::client::ChainClient, components::adapter::ChainId}; +use graph::{blockchain::client::ChainClient, components::network_provider::ChainName}; use super::{CommonChainConfig, Stores, TestChainSubstreams}; @@ -24,7 +24,7 @@ pub async fn chain(test_name: &str, stores: &Stores) -> TestChainSubstreams { mock_registry, chain_store, block_stream_builder.clone(), - ChainId::from("test-chain"), + ChainName::from("test-chain"), )); TestChainSubstreams {