diff --git a/chain/ethereum/proto/ethereum.proto b/chain/ethereum/proto/ethereum.proto index 3c9f7378c7d..5327b851659 100644 --- a/chain/ethereum/proto/ethereum.proto +++ b/chain/ethereum/proto/ethereum.proto @@ -22,6 +22,31 @@ message Block { repeated TransactionTrace transaction_traces = 10; repeated BalanceChange balance_changes = 11; + + enum DetailLevel{ + DETAILLEVEL_EXTENDED = 0; + // DETAILLEVEL_TRACE = 1; // TBD + DETAILLEVEL_BASE = 2; + } + + // DetailLevel affects the data available in this block. + // + // ## DetailLevel_EXTENDED + // + // Describes the most complete block, with traces, balance changes, storage + // changes. It is extracted during the execution of the block. + // + // ## DetailLevel_BASE + // + // Describes a block that contains only the block header, transaction receipts + // and event logs: everything that can be extracted using the base JSON-RPC + // interface + // (https://ethereum.org/en/developers/docs/apis/json-rpc/#json-rpc-methods) + // Furthermore, the eth_getTransactionReceipt call has been avoided because it + // brings only minimal improvements at the cost of requiring an archive node + // or a full node with complete transaction index. + DetailLevel detail_level = 12; + repeated CodeChange code_changes = 20; reserved 40; // bool filtering_applied = 40 [deprecated = true]; @@ -505,4 +530,4 @@ message GasChange { } uint64 ordinal = 4; -} \ No newline at end of file +} diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index f78ff1b0bec..4fcc84603e9 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -1,6 +1,7 @@ use anyhow::Error; use ethabi::{Error as ABIError, Function, ParamType, Token}; use graph::blockchain::ChainIdentifier; +use graph::components::ethereum::BlockDetailLevel; use graph::components::subgraph::MappingError; use graph::data::store::ethereum::call; use graph::firehose::CallToFilter; @@ -262,6 +263,15 @@ impl TriggerFilter { pub fn block(&self) -> &EthereumBlockFilter { &self.block } + + /// Always require full blocks until we are able to strip down the block from the WASM ABI. + pub fn minimum_detail_level(&self) -> BlockDetailLevel { + if self.call.is_empty() && self.block.is_empty() { + return BlockDetailLevel::Light; + } + + BlockDetailLevel::Full + } } impl bc::TriggerFilter for TriggerFilter { diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 1def8c483cc..633f7264132 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -6,6 +6,7 @@ use graph::blockchain::{ BlockIngestor, BlockTime, BlockchainKind, ChainIdentifier, TriggersAdapterSelector, }; use graph::components::adapter::ChainId; +use graph::components::ethereum::BlockDetailLevel; use graph::components::store::DeploymentCursorTracker; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::firehose::{FirehoseEndpoint, ForkStep}; @@ -47,6 +48,7 @@ use crate::data_source::UnresolvedDataSourceTemplate; use crate::ingestor::PollingBlockIngestor; use crate::network::EthereumNetworkAdapters; use crate::runtime::runtime_adapter::eth_call_gas; +use crate::ENV_VARS; use crate::{ adapter::EthereumAdapter as _, codec, @@ -55,7 +57,7 @@ use crate::{ blocks_with_triggers, get_calls, parse_block_triggers, parse_call_triggers, parse_log_triggers, }, - SubgraphEthRpcMetrics, TriggerFilter, ENV_VARS, + SubgraphEthRpcMetrics, TriggerFilter, }; use crate::{BufferedCallCache, NodeCapabilities}; use crate::{EthereumAdapter, RuntimeAdapter}; @@ -66,7 +68,9 @@ use graph::blockchain::block_stream::{ /// Celo Mainnet: 42220, Testnet Alfajores: 44787, Testnet Baklava: 62320 const CELO_CHAIN_IDS: [u64; 3] = [42220, 44787, 62320]; -pub struct EthereumStreamBuilder {} +pub struct EthereumStreamBuilder { + pub firehose_chains_detail_level_check_disabled: Vec, +} #[async_trait] impl BlockStreamBuilder for EthereumStreamBuilder { @@ -95,7 +99,15 @@ impl BlockStreamBuilder for EthereumStreamBuilder { .subgraph_logger(&deployment) .new(o!("component" => "FirehoseBlockStream")); - let firehose_mapper = Arc::new(FirehoseMapper { adapter, filter }); + let block_level_check_enabled = !self + .firehose_chains_detail_level_check_disabled + .contains(&chain.name); + + let firehose_mapper = Arc::new(FirehoseMapper { + adapter, + filter, + block_level_check_enabled, + }); Ok(Box::new(FirehoseBlockStream::new( deployment.hash, @@ -665,6 +677,52 @@ pub struct TriggersAdapter { unified_api_version: UnifiedMappingApiVersion, } +#[cfg(debug_assertions)] +pub struct NoopTriggersAdapter {} + +#[cfg(debug_assertions)] +#[async_trait] +impl TriggersAdapterTrait for NoopTriggersAdapter { + async fn ancestor_block( + &self, + _ptr: BlockPtr, + _offset: BlockNumber, + _root: Option, + ) -> Result, Error> { + Ok(None) + } + + async fn scan_triggers( + &self, + _from: BlockNumber, + _to: BlockNumber, + _filter: &TriggerFilter, + ) -> Result<(Vec>, BlockNumber), Error> { + Ok((vec![], 0)) + } + + // Used for reprocessing blocks when creating a data source. + async fn triggers_in_block( + &self, + _logger: &Logger, + _block: BlockFinality, + _filter: &TriggerFilter, + ) -> Result, Error> { + unimplemented!() + } + + /// Return `true` if the block with the given hash and number is on the + /// main chain, i.e., the chain going back from the current chain head. + async fn is_on_main_chain(&self, _ptr: BlockPtr) -> Result { + Ok(true) + } + + /// Get pointer to parent of `block`. This is called when reverting `block`. + async fn parent_ptr(&self, _block: &BlockPtr) -> Result, Error> { + Ok(None) + } +} + #[async_trait] impl TriggersAdapterTrait for TriggersAdapter { async fn scan_triggers( @@ -768,6 +826,7 @@ impl TriggersAdapterTrait for TriggersAdapter { BlockFinality::NonFinal(EthereumBlockWithCalls { ethereum_block: block, calls: None, + detail_level: BlockDetailLevel::Light, }) })) } @@ -807,6 +866,7 @@ impl TriggersAdapterTrait for TriggersAdapter { pub struct FirehoseMapper { adapter: Arc>, filter: Arc, + block_level_check_enabled: bool, } #[async_trait] @@ -822,6 +882,16 @@ impl BlockStreamMapper for FirehoseMapper { ))?, }; + if self.block_level_check_enabled { + // If the block doesn't meet the minimum level just stop here. + let minimum_detail_level = self.filter.minimum_detail_level(); + if minimum_detail_level > block.detail_level().clone().into() { + return Err(BlockStreamError::BlockLevelRequirementUnet( + minimum_detail_level, + )); + } + } + // See comment(437a9f17-67cc-478f-80a3-804fe554b227) ethereum_block.calls is always Some even if calls // is empty let ethereum_block: EthereumBlockWithCalls = (&block).try_into()?; @@ -945,3 +1015,217 @@ impl FirehoseMapperTrait for FirehoseMapper { .await } } + +#[cfg(test)] +mod test { + use std::{ + collections::{HashMap, HashSet}, + sync::Arc, + }; + + use anyhow::Result; + use graph::{ + blockchain::{ + block_stream::{BlockStreamError, BlockStreamMapper as _}, + TriggersAdapter as TriggersAdapterTrait, + }, + components::ethereum::BlockDetailLevel, + prelude::ethabi::Address, + }; + use prost::Message; + + use crate::{ + adapter::{EthereumBlockFilter, EthereumCallFilter, EthereumLogFilter}, + codec::{block::DetailLevel, BlockHeader}, + Chain, TriggerFilter, + }; + + use super::{FirehoseMapper, NoopTriggersAdapter}; + + #[test] + fn check_default_detail_level_is_full_and_checked() -> Result<()> { + let adapter: Arc> = Arc::new(NoopTriggersAdapter {}); + let filter = Arc::new(TriggerFilter { + log: EthereumLogFilter::default(), + call: EthereumCallFilter::default(), + block: EthereumBlockFilter::default(), + }); + + let mapper = FirehoseMapper { + adapter, + filter, + block_level_check_enabled: true, + }; + + // Default is full block + let block = eth_block(None); + + let res = mapper.decode_block(Some(block.encode_to_vec().as_ref())); + assert_eq!(res.is_err(), true, "{:?}", res); + + // This error should mean that the minimum block detail is met and so it would normally + // decode the block. Since this block is a PITA to build and not that relevant to this test + // this will have to do. If you get a different error, maybe some order was changed, that's not + // great but relying on just an error could generate a false pass. + let res = res.unwrap_err(); + assert!(res.to_string().contains("invalid block hash"), "{:?}", res); + + Ok(()) + } + + #[test] + fn disabled_check_default_detail_level_is_full() -> Result<()> { + let adapter: Arc> = Arc::new(NoopTriggersAdapter {}); + let filter = Arc::new(TriggerFilter { + log: EthereumLogFilter::default(), + call: EthereumCallFilter::default(), + block: EthereumBlockFilter::default(), + }); + + let mapper = FirehoseMapper { + adapter, + filter, + block_level_check_enabled: false, + }; + + // Default is full block + let block = eth_block(None); + + let res = mapper.decode_block(Some(block.encode_to_vec().as_ref())); + assert_eq!(res.is_err(), true, "{:?}", res); + + // This error should mean that the minimum block detail is met and so it would normally + // decode the block. Since this block is a PITA to build and not that relevant to this test + // this will have to do. If you get a different error, maybe some order was changed, that's not + // great but relying on just an error could generate a false pass. + let res = res.unwrap_err(); + assert!(res.to_string().contains("invalid block hash"), "{:?}", res); + + Ok(()) + } + + #[test] + // detail level required when processing calls or blocks is full, since either of these can access the block details. + fn check_detail_level_full_if_call_block() { + let filter = Arc::new(TriggerFilter { + log: EthereumLogFilter::default(), + call: EthereumCallFilter { + contract_addresses_function_signatures: HashMap::from_iter(vec![( + Address::from_low_u64_be(0), + (0, HashSet::from_iter(vec![[0u8; 4]])), + )]), + wildcard_signatures: HashSet::new(), + }, + block: EthereumBlockFilter::default(), + }); + assert_eq!(filter.minimum_detail_level(), BlockDetailLevel::Full); + + let filter = Arc::new(TriggerFilter { + log: EthereumLogFilter::default(), + call: EthereumCallFilter::default(), + block: EthereumBlockFilter { + trigger_every_block: true, + ..Default::default() + }, + }); + assert_eq!(filter.minimum_detail_level(), BlockDetailLevel::Full); + + let filter = Arc::new(TriggerFilter { + log: EthereumLogFilter::default(), + call: EthereumCallFilter::default(), + block: EthereumBlockFilter::default(), + }); + assert_eq!(filter.minimum_detail_level(), BlockDetailLevel::Light); + } + + #[test] + fn ensure_full_strictly_gt_light() { + assert!(BlockDetailLevel::Full > BlockDetailLevel::Light); + } + + #[test] + fn check_default_detail_level_is_checked() -> Result<()> { + let adapter: Arc> = Arc::new(NoopTriggersAdapter {}); + let filter = Arc::new(TriggerFilter { + log: EthereumLogFilter::default(), + call: EthereumCallFilter::default(), + block: EthereumBlockFilter { + trigger_every_block: true, + ..Default::default() + }, + }); + assert_eq!(filter.minimum_detail_level(), BlockDetailLevel::Full); + + let mapper = FirehoseMapper { + adapter, + filter, + block_level_check_enabled: true, + }; + + // Default is full block + let light_block_with_tx = eth_block(Some(BlockDetailLevel::Light)); + + let res = mapper.decode_block(Some(light_block_with_tx.encode_to_vec().as_ref())); + assert_eq!(res.is_err(), true, "{:?}", res); + let res = res.unwrap_err(); + + assert_eq!( + res.to_string(), + BlockStreamError::BlockLevelRequirementUnet(BlockDetailLevel::Full).to_string(), + "{:?}", + res + ); + + Ok(()) + } + + #[test] + fn disabled_check_default_detail_level_is_checked() { + let adapter: Arc> = Arc::new(NoopTriggersAdapter {}); + let filter = Arc::new(TriggerFilter { + log: EthereumLogFilter::default(), + call: EthereumCallFilter::default(), + block: EthereumBlockFilter { + trigger_every_block: true, + ..Default::default() + }, + }); + assert_eq!(filter.minimum_detail_level(), BlockDetailLevel::Full); + + let mapper = FirehoseMapper { + adapter, + filter, + block_level_check_enabled: false, + }; + + // Default is full block + let block = eth_block(Some(BlockDetailLevel::Light)); + let res = mapper.decode_block(Some(block.encode_to_vec().as_ref())); + assert_eq!(res.is_err(), true, "{:?}", res); + + // This error should mean that the minimum block detail is met and so it would normally + // decode the block. Since this block is a PITA to build and not that relevant to this test + // this will have to do. If you get a different error, maybe some order was changed, that's not + // great but relying on just an error could generate a false pass. + let res = res.unwrap_err(); + assert!(res.to_string().contains("invalid block hash"), "{:?}", res); + } + + fn eth_block(detail_level: Option) -> crate::codec::Block { + let mut block = crate::codec::Block { + ..Default::default() + }; + match detail_level { + Some(BlockDetailLevel::Full) => { + block.set_detail_level(DetailLevel::DetaillevelExtended) + } + Some(BlockDetailLevel::Light) => block.set_detail_level(DetailLevel::DetaillevelBase), + None => {} + } + block.header = Some(BlockHeader { + ..Default::default() + }); + + block + } +} diff --git a/chain/ethereum/src/codec.rs b/chain/ethereum/src/codec.rs index 114982607ec..04e58419b0b 100644 --- a/chain/ethereum/src/codec.rs +++ b/chain/ethereum/src/codec.rs @@ -7,9 +7,12 @@ use graph::{ blockchain::{ self, Block as BlockchainBlock, BlockPtr, BlockTime, ChainStoreBlock, ChainStoreData, }, + components::ethereum::BlockDetailLevel, prelude::{ - web3, - web3::types::{Bytes, H160, H2048, H256, H64, U256, U64}, + web3::{ + self, + types::{Bytes, H160, H2048, H256, H64, U256, U64}, + }, BlockNumber, Error, EthereumBlock, EthereumBlockWithCalls, EthereumCall, LightEthereumBlock, }, @@ -217,6 +220,15 @@ impl TryInto for &Block { } } +impl Into for block::DetailLevel { + fn into(self) -> BlockDetailLevel { + match self { + block::DetailLevel::DetaillevelExtended => BlockDetailLevel::Full, + block::DetailLevel::DetaillevelBase => BlockDetailLevel::Light, + } + } +} + impl TryInto for &Block { type Error = Error; @@ -370,6 +382,7 @@ impl TryInto for &Block { }) .collect::>()?, ), + detail_level: self.detail_level().into(), }; Ok(block) diff --git a/chain/ethereum/src/env.rs b/chain/ethereum/src/env.rs index 75c313212b9..77555cd4f5b 100644 --- a/chain/ethereum/src/env.rs +++ b/chain/ethereum/src/env.rs @@ -1,4 +1,5 @@ use envconfig::Envconfig; +use graph::components::adapter::ChainId; use graph::env::EnvVarBoolean; use graph::prelude::{envconfig, lazy_static, BlockNumber}; use std::fmt; @@ -88,6 +89,11 @@ pub struct EnvVars { /// This is a comma separated list of chain ids for which the gas field will not be set /// when calling `eth_call`. pub eth_call_no_gas: Vec, + /// Chain names where detail_checking should be disabled. This option should only be used + /// for chains where the firehose extended block detail level is not available. + /// Using this env var on any other chains can result in determinism issues which are + /// slashable offenses. + pub chains_firehose_disable_detail_level_check: Vec, } // This does not print any values avoid accidentally leaking any sensitive env vars @@ -105,6 +111,26 @@ impl EnvVars { impl From for EnvVars { fn from(x: Inner) -> Self { + // Add these to the main var, this is a way of keeping the default + // values, while adding something new to the list. + let disabled_detail_level_chains_extra: Vec<_> = x + .chains_firehose_disable_detail_level_check_extra + .map(|input| { + input + .split(',') + .filter(|s| !s.is_empty()) + .map(ChainId::from) + .collect() + }) + .unwrap_or_default(); + + let disabled_detail_level_chains: Vec<_> = x + .chains_firehose_disable_detail_level_check + .split(',') + .filter(|s| !s.is_empty()) + .map(ChainId::from) + .collect(); + Self { get_logs_max_contracts: x.get_logs_max_contracts, geth_eth_call_errors: x @@ -137,6 +163,10 @@ impl From for EnvVars { .filter(|s| !s.is_empty()) .map(str::to_string) .collect(), + chains_firehose_disable_detail_level_check: disabled_detail_level_chains + .into_iter() + .chain(disabled_detail_level_chains_extra.into_iter()) + .collect(), } } } @@ -186,4 +216,11 @@ struct Inner { genesis_block_number: u64, #[envconfig(from = "GRAPH_ETH_CALL_NO_GAS", default = "421613,421614")] eth_call_no_gas: String, + #[envconfig( + from = "GRAPH_ETH_FIREHOSE_DETAIL_LEVEL_DISABLE_CHAINS", + default = "optimism,optimism-sepolia,arbiturm-one" + )] + chains_firehose_disable_detail_level_check: String, + #[envconfig(from = "GRAPH_ETH_FIREHOSE_DETAIL_LEVEL_DISABLE_CHAINS_EXTRA")] + chains_firehose_disable_detail_level_check_extra: Option, } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index c4ea6323c7d..5c9168d6e7f 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1892,15 +1892,13 @@ pub(crate) async fn get_calls( // For final blocks, or nonfinal blocks where we already checked // (`calls.is_some()`), do nothing; if we haven't checked for calls, do // that now - match block { + let block_finality = match block { BlockFinality::Final(_) - | BlockFinality::NonFinal(EthereumBlockWithCalls { - ethereum_block: _, - calls: Some(_), - }) => Ok(block), + | BlockFinality::NonFinal(EthereumBlockWithCalls { calls: Some(_), .. }) => Ok(block), BlockFinality::NonFinal(EthereumBlockWithCalls { ethereum_block, calls: None, + detail_level, }) => { let calls = if !requires_traces || ethereum_block.transaction_receipts.is_empty() { vec![] @@ -1921,9 +1919,11 @@ pub(crate) async fn get_calls( Ok(BlockFinality::NonFinal(EthereumBlockWithCalls { ethereum_block, calls: Some(calls), + detail_level, })) } - } + }; + block_finality } pub(crate) fn parse_log_triggers( @@ -2607,6 +2607,7 @@ mod tests { EthereumBlockWithCalls, }; use graph::blockchain::BlockPtr; + use graph::components::ethereum::BlockDetailLevel; use graph::prelude::ethabi::ethereum_types::U64; use graph::prelude::tokio::{self}; use graph::prelude::web3::transports::test::TestTransport; @@ -2634,6 +2635,7 @@ mod tests { input: bytes(vec![1; 36]), ..Default::default() }]), + detail_level: BlockDetailLevel::Full, }; assert_eq!( @@ -2800,6 +2802,7 @@ mod tests { input: bytes(vec![1; 36]), ..Default::default() }]), + detail_level: BlockDetailLevel::Full, }; assert_eq!( @@ -2832,6 +2835,7 @@ mod tests { input: bytes(vec![1; 36]), ..Default::default() }]), + detail_level: BlockDetailLevel::Full, }; assert_eq!( diff --git a/chain/ethereum/src/ingestor.rs b/chain/ethereum/src/ingestor.rs index d22e08c4294..90e68e10ef4 100644 --- a/chain/ethereum/src/ingestor.rs +++ b/chain/ethereum/src/ingestor.rs @@ -3,6 +3,7 @@ use crate::{EthereumAdapter, EthereumAdapterTrait as _}; use graph::blockchain::client::ChainClient; use graph::blockchain::BlockchainKind; use graph::components::adapter::ChainId; +use graph::components::ethereum::BlockDetailLevel; use graph::futures03::compat::Future01CompatExt as _; use graph::slog::o; use graph::util::backoff::ExponentialBackoff; @@ -182,10 +183,12 @@ impl PollingBlockIngestor { // We need something that implements `Block` to store the block; the // store does not care whether the block is final or not - let ethereum_block = BlockFinality::NonFinal(EthereumBlockWithCalls { + let ethereum_block_with_calls = EthereumBlockWithCalls { ethereum_block, calls: None, - }); + detail_level: BlockDetailLevel::Full, + }; + let ethereum_block = BlockFinality::NonFinal(ethereum_block_with_calls); // Store it in the database and try to advance the chain head pointer self.chain_store diff --git a/chain/ethereum/src/protobuf/sf.ethereum.r#type.v2.rs b/chain/ethereum/src/protobuf/sf.ethereum.r#type.v2.rs index 6d13e187d14..0d91a0ca380 100644 --- a/chain/ethereum/src/protobuf/sf.ethereum.r#type.v2.rs +++ b/chain/ethereum/src/protobuf/sf.ethereum.r#type.v2.rs @@ -23,9 +23,67 @@ pub struct Block { pub transaction_traces: ::prost::alloc::vec::Vec, #[prost(message, repeated, tag = "11")] pub balance_changes: ::prost::alloc::vec::Vec, + /// DetailLevel affects the data available in this block. + /// + /// ## DetailLevel_EXTENDED + /// + /// Describes the most complete block, with traces, balance changes, storage + /// changes. It is extracted during the execution of the block. + /// + /// ## DetailLevel_BASE + /// + /// Describes a block that contains only the block header, transaction receipts + /// and event logs: everything that can be extracted using the base JSON-RPC + /// interface + /// () + /// Furthermore, the eth_getTransactionReceipt call has been avoided because it + /// brings only minimal improvements at the cost of requiring an archive node + /// or a full node with complete transaction index. + #[prost(enumeration = "block::DetailLevel", tag = "12")] + pub detail_level: i32, #[prost(message, repeated, tag = "20")] pub code_changes: ::prost::alloc::vec::Vec, } +/// Nested message and enum types in `Block`. +pub mod block { + #[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + Hash, + PartialOrd, + Ord, + ::prost::Enumeration + )] + #[repr(i32)] + pub enum DetailLevel { + DetaillevelExtended = 0, + /// DETAILLEVEL_TRACE = 1; // TBD + DetaillevelBase = 2, + } + impl DetailLevel { + /// 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 { + DetailLevel::DetaillevelExtended => "DETAILLEVEL_EXTENDED", + DetailLevel::DetaillevelBase => "DETAILLEVEL_BASE", + } + } + /// Creates an enum from field names used in the ProtoBuf definition. + pub fn from_str_name(value: &str) -> ::core::option::Option { + match value { + "DETAILLEVEL_EXTENDED" => Some(Self::DetaillevelExtended), + "DETAILLEVEL_BASE" => Some(Self::DetaillevelBase), + _ => None, + } + } + } +} /// HeaderOnlyBlock is used to optimally unpack the \[Block\] structure (note the /// corresponding message number for the `header` field) while consuming less /// memory, when only the `header` is desired. diff --git a/graph/src/blockchain/block_stream.rs b/graph/src/blockchain/block_stream.rs index 25a923dd502..99c2cb4adca 100644 --- a/graph/src/blockchain/block_stream.rs +++ b/graph/src/blockchain/block_stream.rs @@ -1,3 +1,4 @@ +use crate::components::ethereum::BlockDetailLevel; use crate::substreams::Clock; use crate::substreams_rpc::response::Message as SubstreamsMessage; use crate::substreams_rpc::BlockScopedData; @@ -518,6 +519,10 @@ impl SubstreamsError { pub enum BlockStreamError { #[error("Failed to decode protobuf {0}")] ProtobufDecodingError(#[from] prost::DecodeError), + #[error( + "subgraph requires block detail level: {0}, check firehose documentation for more details" + )] + BlockLevelRequirementUnet(BlockDetailLevel), #[error("substreams error: {0}")] SubstreamsError(#[from] SubstreamsError), #[error("block stream error {0}")] diff --git a/graph/src/components/ethereum/mod.rs b/graph/src/components/ethereum/mod.rs index 45f1f5d98ad..e2b50012f4f 100644 --- a/graph/src/components/ethereum/mod.rs +++ b/graph/src/components/ethereum/mod.rs @@ -1,6 +1,6 @@ mod types; pub use self::types::{ - evaluate_transaction_status, EthereumBlock, EthereumBlockWithCalls, EthereumCall, - LightEthereumBlock, LightEthereumBlockExt, + evaluate_transaction_status, BlockDetailLevel, EthereumBlock, EthereumBlockWithCalls, + EthereumCall, LightEthereumBlock, LightEthereumBlockExt, }; diff --git a/graph/src/components/ethereum/types.rs b/graph/src/components/ethereum/types.rs index b43730590d4..8683ab3abb1 100644 --- a/graph/src/components/ethereum/types.rs +++ b/graph/src/components/ethereum/types.rs @@ -66,12 +66,25 @@ impl LightEthereumBlockExt for LightEthereumBlock { } } +#[derive(Debug, Clone, Ord, PartialOrd, PartialEq, Eq)] +pub enum BlockDetailLevel { + Light, + Full, +} + +impl std::fmt::Display for BlockDetailLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!("{:?}", self)) + } +} + #[derive(Clone, Debug)] pub struct EthereumBlockWithCalls { pub ethereum_block: EthereumBlock, /// The calls in this block; `None` means we haven't checked yet, /// `Some(vec![])` means that we checked and there were none pub calls: Option>, + pub detail_level: BlockDetailLevel, } impl EthereumBlockWithCalls { diff --git a/node/src/chain.rs b/node/src/chain.rs index 3e87ff8295b..12fc24f7b8e 100644 --- a/node/src/chain.rs +++ b/node/src/chain.rs @@ -8,6 +8,7 @@ use ethereum::chain::{ }; use ethereum::network::EthereumNetworkAdapter; use ethereum::ProviderEthRpcMetrics; +use ethereum::ENV_VARS as ETH_VARS; use graph::anyhow::bail; use graph::blockchain::client::ChainClient; use graph::blockchain::{ @@ -547,7 +548,11 @@ pub async fn networks_as_chains( call_cache, client, chain_head_update_listener.clone(), - Arc::new(EthereumStreamBuilder {}), + Arc::new(EthereumStreamBuilder { + firehose_chains_detail_level_check_disabled: ETH_VARS + .chains_firehose_disable_detail_level_check + .clone(), + }), Arc::new(EthereumBlockRefetcher {}), Arc::new(adapter_selector), Arc::new(EthereumRuntimeAdapterBuilder {}),