diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 709884496e8..a56f06f4022 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -1302,6 +1302,10 @@ impl blockchain::DataSourceTemplate for DataSourceTemplate { fn kind(&self) -> &str { &self.kind } + + fn has_address_scoped_block_handler(&self) -> bool { + self.mapping.has_block_handler_with_call_filter() + } } #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] @@ -1597,3 +1601,82 @@ fn string_to_b256(s: &str) -> B256 { pub struct TemplateSource { pub abi: String, } + +#[cfg(test)] +mod address_scoped_block_handler_tests { + use super::*; + use graph::blockchain::DataSourceTemplate as _; + + fn template_with(block_handlers: Vec) -> DataSourceTemplate { + let mut mapping = Mapping { + kind: "ethereum/events".to_string(), + api_version: semver::Version::new(0, 0, 7), + language: "wasm/assemblyscript".to_string(), + entities: vec![], + abis: vec![], + block_handlers, + call_handlers: vec![], + event_handlers: vec![], + runtime: std::sync::Arc::new(vec![]), + link: Link { + link: "test".to_string(), + }, + }; + mapping.block_handlers.shrink_to_fit(); + DataSourceTemplate { + kind: "ethereum/contract".to_string(), + network: Some("test".to_string()), + name: "T".to_string(), + manifest_idx: 0, + source: TemplateSource { + abi: "abi".to_string(), + }, + mapping, + } + } + + fn handler(filter: Option) -> MappingBlockHandler { + MappingBlockHandler { + handler: "h".to_string(), + filter, + } + } + + #[test] + fn call_filtered_block_handler_is_address_scoped() { + let template = template_with(vec![handler(Some(BlockHandlerFilter::Call))]); + assert!(template.has_address_scoped_block_handler()); + } + + #[test] + fn block_handlers_without_a_call_filter_are_not_address_scoped() { + for filter in [ + None, + Some(BlockHandlerFilter::Once), + Some(BlockHandlerFilter::Polling { + every: std::num::NonZeroU32::new(1).unwrap(), + }), + ] { + let template = template_with(vec![handler(filter.clone())]); + assert!( + !template.has_address_scoped_block_handler(), + "a {filter:?} block handler does not depend on the created address" + ); + } + } + + #[test] + fn a_template_without_block_handlers_is_not_address_scoped() { + assert!(!template_with(vec![]).has_address_scoped_block_handler()); + } + + #[test] + fn one_call_filtered_handler_is_enough_among_other_handlers() { + let template = template_with(vec![ + handler(None), + handler(Some(BlockHandlerFilter::Call)), + handler(Some(BlockHandlerFilter::Once)), + ]); + assert!(template.has_address_scoped_block_handler()); + } +} diff --git a/core/src/subgraph/runner/mod.rs b/core/src/subgraph/runner/mod.rs index 026be3dfc27..e39a4b4a7b6 100644 --- a/core/src/subgraph/runner/mod.rs +++ b/core/src/subgraph/runner/mod.rs @@ -14,8 +14,8 @@ use graph::blockchain::block_stream::{ BlockStream, BlockStreamEvent, BlockWithTriggers, FirehoseCursor, }; use graph::blockchain::{ - Block, BlockTime, Blockchain, DataSource as _, SubgraphFilter, Trigger, TriggerFilter as _, - TriggerFilterWrapper, + Block, BlockTime, Blockchain, DataSource as _, DataSourceTemplate as _, SubgraphFilter, + Trigger, TriggerFilter as _, TriggerFilterWrapper, }; use graph::components::store::{ EmptyStore, GetScope, ReadStore, SeqGenerator, StoredDynamicDataSource, @@ -147,7 +147,32 @@ where } fn is_static_filters_enabled(&self) -> bool { - self.inputs.static_filters || self.ctx.hosts_len() > ENV_VARS.static_filters_threshold + let requested = + self.inputs.static_filters || self.ctx.hosts_len() > ENV_VARS.static_filters_threshold; + if !requested { + return false; + } + + // Static filters deliberately drop the addresses of created data + // sources, so a template handler that fires only on calls to the + // address of the data source it creates has nothing to match on and + // would never run. Such subgraphs have to keep the full filter, which + // means restarting the block stream whenever a data source is created. + let incompatible = self.ctx.templates().iter().any(|template| { + template + .as_onchain() + .is_some_and(|t| t.has_address_scoped_block_handler()) + }); + if incompatible { + info!( + self.logger, + "subgraph has a template handler that only fires on calls to the created \ + data source's address; keeping the full trigger filter instead of static filters" + ); + return false; + } + + true } fn build_filter(&self) -> TriggerFilterWrapper { diff --git a/graph/src/blockchain/mod.rs b/graph/src/blockchain/mod.rs index df942d19910..4829f7c3583 100644 --- a/graph/src/blockchain/mod.rs +++ b/graph/src/blockchain/mod.rs @@ -380,6 +380,18 @@ pub trait DataSourceTemplate: Send + Sync + Debug { fn name(&self) -> &str; fn manifest_idx(&self) -> u32; fn kind(&self) -> &str; + /// Whether a data source created from this template carries a handler that + /// only fires on calls to that specific data source's address, and which + /// therefore cannot be expressed by a filter built from the static data + /// sources alone. + /// + /// Static filters drop the addresses of created data sources on purpose, so + /// a chain that can have such a handler must report it here and let the + /// runner fall back to the full filter. Defaults to `false` for chains + /// whose trigger filters have no equivalent construct. + fn has_address_scoped_block_handler(&self) -> bool { + false + } fn info(&self) -> DataSourceTemplateInfo { DataSourceTemplateInfo { api_version: self.api_version(),