From 867b7059d9f92f15c57c7615fa1ea867f52ac5da Mon Sep 17 00:00:00 2001 From: Liang Xu <24490353+lx3133584@users.noreply.github.com> Date: Fri, 6 Nov 2026 04:02:41 +0800 Subject: [PATCH] core: keep the full trigger filter when a template handler is address-scoped A blockHandler with `filter: { kind: call }` declared in a template never fired under static filters. Static filters deliberately drop the addresses of created data sources, and the template contributes only trigger_every_block through EthereumBlockFilter::from_mapping, so the Start/End triggers a data source created from that template receives never match a Call handler. Nothing was logged, and the subgraph kept indexing while its entities and PoI drifted from a node running in default mode. Report the condition on the template trait and let the runner fall back to the full filter, which rebuilds on data source creation and therefore does include the created address. Chains without an equivalent construct keep the default, so their behaviour does not change. Subgraphs that already indexed in static filters with such a template have a PoI computed from the missing handlers; they need to be rewound to before the block where static filters took effect. Signed-off-by: Liang Xu <24490353+lx3133584@users.noreply.github.com> --- chain/ethereum/src/data_source.rs | 83 +++++++++++++++++++++++++++++++ core/src/subgraph/runner/mod.rs | 31 ++++++++++-- graph/src/blockchain/mod.rs | 12 +++++ 3 files changed, 123 insertions(+), 3 deletions(-) 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(),