From 13617c533fce40bfca095a015fc1a8eca35db574 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Thu, 29 Feb 2024 17:10:02 -0800 Subject: [PATCH 01/51] all: Expose match_and_decode as a TriggerProcessor method --- chain/substreams/src/trigger.rs | 25 ++++++++-- core/src/subgraph/context/mod.rs | 6 +-- core/src/subgraph/trigger_processor.rs | 56 +++++++++++++++-------- graph/src/components/trigger_processor.rs | 23 +++++++++- 4 files changed, 84 insertions(+), 26 deletions(-) diff --git a/chain/substreams/src/trigger.rs b/chain/substreams/src/trigger.rs index a3cdf5cd5cb..da323457dcd 100644 --- a/chain/substreams/src/trigger.rs +++ b/chain/substreams/src/trigger.rs @@ -3,13 +3,15 @@ use std::sync::Arc; use anyhow::Error; use graph::{ blockchain::{ - self, block_stream::BlockWithTriggers, BlockPtr, EmptyNodeCapabilities, MappingTriggerTrait, + self, block_stream::BlockWithTriggers, BlockPtr, Blockchain, EmptyNodeCapabilities, + MappingTriggerTrait, }, components::{ + metrics::subgraph::SubgraphInstanceMetrics, store::{DeploymentLocator, SubgraphFork}, subgraph::{MappingError, ProofOfIndexingEvent, SharedProofOfIndexing}, }, - data_source, + data_source::{self, MappingTrigger}, prelude::{ anyhow, async_trait, BlockHash, BlockNumber, BlockState, CheapClone, RuntimeHostBuilder, }, @@ -197,7 +199,7 @@ where async fn process_trigger<'a>( &'a self, logger: &Logger, - _: Box + Send + 'a>, + _: Box + Send + 'a>, block: &Arc, _trigger: &data_source::TriggerData, mut state: BlockState, @@ -248,4 +250,21 @@ where Ok(state) } + + fn match_and_decode<'a>( + &'a self, + _: &Logger, + _: &Arc<::Block>, + _: &graph::data_source::TriggerData, + _: Box + Send + 'a>, + _: &Arc, + ) -> Result< + Vec<( + &'a T::Host, + graph::data_source::TriggerWithHandler>, + )>, + MappingError, + > { + Ok(vec![]) + } } diff --git a/core/src/subgraph/context/mod.rs b/core/src/subgraph/context/mod.rs index 6c87e095d1e..729c752db98 100644 --- a/core/src/subgraph/context/mod.rs +++ b/core/src/subgraph/context/mod.rs @@ -189,10 +189,10 @@ impl> IndexingContext { Ok(state) } - pub async fn process_trigger_in_hosts( - &self, + pub async fn process_trigger_in_hosts<'a>( + &'a self, logger: &Logger, - hosts: Box + Send + '_>, + hosts: Box + Send + 'a>, block: &Arc, trigger: &TriggerData, state: BlockState, diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index f0dc70712d6..c010e4944b9 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -22,7 +22,7 @@ where async fn process_trigger<'a>( &'a self, logger: &Logger, - hosts: Box + Send + 'a>, + hosts: Box + Send + 'a>, block: &Arc, trigger: &TriggerData, mut state: BlockState, @@ -34,23 +34,15 @@ where ) -> Result { let error_count = state.deterministic_errors.len(); - let mut host_mapping: Vec<(&T::Host, TriggerWithHandler>)> = vec![]; - - { - let _section = subgraph_metrics.stopwatch.start_section("match_and_decode"); - - for host in hosts { - let mapping_trigger = match host.match_and_decode(trigger, block, logger)? { - // Trigger matches and was decoded as a mapping trigger. - Some(mapping_trigger) => mapping_trigger, - - // Trigger does not match, do not process it. - None => continue, - }; - - host_mapping.push((host, mapping_trigger)); - } - } + let host_mapping: Vec<(&'a T::Host, TriggerWithHandler<_>)> = + >::match_and_decode( + self, + logger, + block, + trigger, + hosts, + subgraph_metrics, + )?; if host_mapping.is_empty() { return Ok(state); @@ -100,4 +92,32 @@ where Ok(state) } + + fn match_and_decode<'a>( + &'a self, + logger: &Logger, + block: &Arc, + trigger: &TriggerData, + hosts: Box + Send + 'a>, + subgraph_metrics: &Arc, + ) -> Result>)>, MappingError> { + let mut host_mapping: Vec<(&'a T::Host, TriggerWithHandler>)> = vec![]; + + { + let _section = subgraph_metrics.stopwatch.start_section("match_and_decode"); + + for host in hosts { + let mapping_trigger = match host.match_and_decode(trigger, block, logger)? { + // Trigger matches and was decoded as a mapping trigger. + Some(mapping_trigger) => mapping_trigger, + + // Trigger does not match, do not process it. + None => continue, + }; + + host_mapping.push((host, mapping_trigger)); + } + } + Ok(host_mapping) + } } diff --git a/graph/src/components/trigger_processor.rs b/graph/src/components/trigger_processor.rs index 03b7c84065d..94497448ca7 100644 --- a/graph/src/components/trigger_processor.rs +++ b/graph/src/components/trigger_processor.rs @@ -3,7 +3,11 @@ use std::sync::Arc; use async_trait::async_trait; use slog::Logger; -use crate::{blockchain::Blockchain, data_source::TriggerData, prelude::SubgraphInstanceMetrics}; +use crate::{ + blockchain::Blockchain, + data_source::{MappingTrigger, TriggerData}, + prelude::SubgraphInstanceMetrics, +}; use super::{ store::SubgraphFork, @@ -16,10 +20,25 @@ where C: Blockchain, T: RuntimeHostBuilder, { + fn match_and_decode<'a>( + &'a self, + logger: &Logger, + block: &Arc, + trigger: &TriggerData, + hosts: Box + Send + 'a>, + subgraph_metrics: &Arc, + ) -> Result< + Vec<( + &'a T::Host, + crate::data_source::TriggerWithHandler>, + )>, + MappingError, + >; + async fn process_trigger<'a>( &'a self, logger: &Logger, - hosts: Box + Send + 'a>, + hosts: Box + Send + 'a>, block: &Arc, trigger: &TriggerData, mut state: BlockState, From dbedcc48918a0f6ee72c457812c26113a73464b5 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Thu, 29 Feb 2024 17:52:19 -0800 Subject: [PATCH 02/51] all: Introduce a name for a host and a trigger --- chain/substreams/src/trigger.rs | 11 +++-------- core/src/subgraph/trigger_processor.rs | 20 ++++++++++++++------ graph/src/components/trigger_processor.rs | 22 +++++++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/chain/substreams/src/trigger.rs b/chain/substreams/src/trigger.rs index da323457dcd..b959e2057d7 100644 --- a/chain/substreams/src/trigger.rs +++ b/chain/substreams/src/trigger.rs @@ -10,8 +10,9 @@ use graph::{ metrics::subgraph::SubgraphInstanceMetrics, store::{DeploymentLocator, SubgraphFork}, subgraph::{MappingError, ProofOfIndexingEvent, SharedProofOfIndexing}, + trigger_processor::HostedTrigger, }, - data_source::{self, MappingTrigger}, + data_source, prelude::{ anyhow, async_trait, BlockHash, BlockNumber, BlockState, CheapClone, RuntimeHostBuilder, }, @@ -258,13 +259,7 @@ where _: &graph::data_source::TriggerData, _: Box + Send + 'a>, _: &Arc, - ) -> Result< - Vec<( - &'a T::Host, - graph::data_source::TriggerWithHandler>, - )>, - MappingError, - > { + ) -> Result>, MappingError> { Ok(vec![]) } } diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index c010e4944b9..8fa13bfe0c1 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -3,7 +3,8 @@ use graph::blockchain::{Block, Blockchain}; use graph::cheap_clone::CheapClone; use graph::components::store::SubgraphFork; use graph::components::subgraph::{MappingError, SharedProofOfIndexing}; -use graph::data_source::{MappingTrigger, TriggerData, TriggerWithHandler}; +use graph::components::trigger_processor::HostedTrigger; +use graph::data_source::TriggerData; use graph::prelude::tokio::time::Instant; use graph::prelude::{ BlockState, RuntimeHost, RuntimeHostBuilder, SubgraphInstanceMetrics, TriggerProcessor, @@ -34,7 +35,7 @@ where ) -> Result { let error_count = state.deterministic_errors.len(); - let host_mapping: Vec<(&'a T::Host, TriggerWithHandler<_>)> = + let host_mapping: Vec> = >::match_and_decode( self, logger, @@ -54,7 +55,11 @@ where .start_handler(causality_region); } - for (host, mapping_trigger) in host_mapping { + for HostedTrigger { + host, + mapping_trigger, + } in host_mapping + { let start = Instant::now(); state = host .process_mapping_trigger( @@ -100,8 +105,8 @@ where trigger: &TriggerData, hosts: Box + Send + 'a>, subgraph_metrics: &Arc, - ) -> Result>)>, MappingError> { - let mut host_mapping: Vec<(&'a T::Host, TriggerWithHandler>)> = vec![]; + ) -> Result>, MappingError> { + let mut host_mapping = vec![]; { let _section = subgraph_metrics.stopwatch.start_section("match_and_decode"); @@ -115,7 +120,10 @@ where None => continue, }; - host_mapping.push((host, mapping_trigger)); + host_mapping.push(HostedTrigger { + host, + mapping_trigger, + }); } } Ok(host_mapping) diff --git a/graph/src/components/trigger_processor.rs b/graph/src/components/trigger_processor.rs index 94497448ca7..013b646791e 100644 --- a/graph/src/components/trigger_processor.rs +++ b/graph/src/components/trigger_processor.rs @@ -5,15 +5,25 @@ use slog::Logger; use crate::{ blockchain::Blockchain, - data_source::{MappingTrigger, TriggerData}, + data_source::{MappingTrigger, TriggerData, TriggerWithHandler}, prelude::SubgraphInstanceMetrics, }; use super::{ store::SubgraphFork, - subgraph::{BlockState, MappingError, RuntimeHostBuilder, SharedProofOfIndexing}, + subgraph::{BlockState, MappingError, RuntimeHost, RuntimeHostBuilder, SharedProofOfIndexing}, }; +/// A trigger that is almost ready to run: we have a host to run it on, and +/// transformed the `TriggerData` into a `MappingTrigger`. +pub struct HostedTrigger<'a, C> +where + C: Blockchain, +{ + pub host: &'a dyn RuntimeHost, + pub mapping_trigger: TriggerWithHandler>, +} + #[async_trait] pub trait TriggerProcessor: Sync + Send where @@ -27,13 +37,7 @@ where trigger: &TriggerData, hosts: Box + Send + 'a>, subgraph_metrics: &Arc, - ) -> Result< - Vec<( - &'a T::Host, - crate::data_source::TriggerWithHandler>, - )>, - MappingError, - >; + ) -> Result>, MappingError>; async fn process_trigger<'a>( &'a self, From ea98c2b7936de6aabecfd967fa7afe8843c88b6d Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Thu, 29 Feb 2024 18:11:25 -0800 Subject: [PATCH 03/51] all: Lift calling match_and_decode out of TriggerProcessor --- chain/substreams/src/trigger.rs | 4 +--- core/src/subgraph/context/mod.rs | 11 +++++++++-- core/src/subgraph/trigger_processor.rs | 17 +++-------------- graph/src/components/trigger_processor.rs | 3 +-- 4 files changed, 14 insertions(+), 21 deletions(-) diff --git a/chain/substreams/src/trigger.rs b/chain/substreams/src/trigger.rs index b959e2057d7..faf7a250d31 100644 --- a/chain/substreams/src/trigger.rs +++ b/chain/substreams/src/trigger.rs @@ -12,7 +12,6 @@ use graph::{ subgraph::{MappingError, ProofOfIndexingEvent, SharedProofOfIndexing}, trigger_processor::HostedTrigger, }, - data_source, prelude::{ anyhow, async_trait, BlockHash, BlockNumber, BlockState, CheapClone, RuntimeHostBuilder, }, @@ -200,9 +199,8 @@ where async fn process_trigger<'a>( &'a self, logger: &Logger, - _: Box + Send + 'a>, + _: Vec>, block: &Arc, - _trigger: &data_source::TriggerData, mut state: BlockState, proof_of_indexing: &SharedProofOfIndexing, causality_region: &str, diff --git a/core/src/subgraph/context/mod.rs b/core/src/subgraph/context/mod.rs index 729c752db98..a7d63aaef22 100644 --- a/core/src/subgraph/context/mod.rs +++ b/core/src/subgraph/context/mod.rs @@ -202,12 +202,19 @@ impl> IndexingContext { subgraph_metrics: &Arc, instrument: bool, ) -> Result { + let triggers: Vec<_> = self.trigger_processor.match_and_decode( + logger, + block, + trigger, + hosts, + subgraph_metrics, + )?; + self.trigger_processor .process_trigger( logger, - hosts, + triggers, block, - trigger, state, proof_of_indexing, causality_region, diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index 8fa13bfe0c1..f89078b770d 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -23,9 +23,8 @@ where async fn process_trigger<'a>( &'a self, logger: &Logger, - hosts: Box + Send + 'a>, + triggers: Vec>, block: &Arc, - trigger: &TriggerData, mut state: BlockState, proof_of_indexing: &SharedProofOfIndexing, causality_region: &str, @@ -35,17 +34,7 @@ where ) -> Result { let error_count = state.deterministic_errors.len(); - let host_mapping: Vec> = - >::match_and_decode( - self, - logger, - block, - trigger, - hosts, - subgraph_metrics, - )?; - - if host_mapping.is_empty() { + if triggers.is_empty() { return Ok(state); } @@ -58,7 +47,7 @@ where for HostedTrigger { host, mapping_trigger, - } in host_mapping + } in triggers { let start = Instant::now(); state = host diff --git a/graph/src/components/trigger_processor.rs b/graph/src/components/trigger_processor.rs index 013b646791e..2e7b5391b14 100644 --- a/graph/src/components/trigger_processor.rs +++ b/graph/src/components/trigger_processor.rs @@ -42,9 +42,8 @@ where async fn process_trigger<'a>( &'a self, logger: &Logger, - hosts: Box + Send + 'a>, + triggers: Vec>, block: &Arc, - trigger: &TriggerData, mut state: BlockState, proof_of_indexing: &SharedProofOfIndexing, causality_region: &str, From a60f36fa9749c8a99c4a09f8a77d30af23c1c875 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 1 Mar 2024 12:26:42 -0800 Subject: [PATCH 04/51] core: Refactor how IndexingContext.process_trigger_in_hosts is called Remove a bunch of intermediate functions to call process_trigger_in_hosts directly --- core/src/subgraph/context/instance/mod.rs | 2 +- core/src/subgraph/context/mod.rs | 29 +--- core/src/subgraph/runner.rs | 172 +++++++++++----------- 3 files changed, 84 insertions(+), 119 deletions(-) diff --git a/core/src/subgraph/context/instance/mod.rs b/core/src/subgraph/context/instance/mod.rs index 49e5cfd3c86..df330881d8d 100644 --- a/core/src/subgraph/context/instance/mod.rs +++ b/core/src/subgraph/context/instance/mod.rs @@ -13,7 +13,7 @@ use graph::{ use hosts::{OffchainHosts, OnchainHosts}; use std::collections::HashMap; -pub(super) struct SubgraphInstance> { +pub(crate) struct SubgraphInstance> { subgraph_id: DeploymentHash, network: String, host_builder: T, diff --git a/core/src/subgraph/context/mod.rs b/core/src/subgraph/context/mod.rs index a7d63aaef22..1776f37e56e 100644 --- a/core/src/subgraph/context/mod.rs +++ b/core/src/subgraph/context/mod.rs @@ -69,7 +69,7 @@ where T: RuntimeHostBuilder, C: Blockchain, { - instance: SubgraphInstance, + pub(crate) instance: SubgraphInstance, pub instances: SubgraphKeepAlive, pub offchain_monitor: OffchainMonitor, pub filter: Option, @@ -102,33 +102,6 @@ impl> IndexingContext { } } - pub async fn process_trigger( - &self, - logger: &Logger, - block: &Arc, - trigger: &TriggerData, - state: BlockState, - proof_of_indexing: &SharedProofOfIndexing, - causality_region: &str, - debug_fork: &Option>, - subgraph_metrics: &Arc, - instrument: bool, - ) -> Result { - self.process_trigger_in_hosts( - logger, - self.instance.hosts_for_trigger(trigger), - block, - trigger, - state, - proof_of_indexing, - causality_region, - debug_fork, - subgraph_metrics, - instrument, - ) - .await - } - pub async fn process_block( &self, logger: &Logger, diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index 53e08154a8b..8ae77d25b70 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -299,46 +299,72 @@ where // Causality region for onchain triggers. let causality_region = PoICausalityRegion::from_network(&self.inputs.network); - let mut block_state = { - let _section = self - .metrics - .stream - .stopwatch - .start_section(PROCESS_TRIGGERS_SECTION_NAME); + let mut block_state = BlockState::new( + self.inputs.store.clone(), + std::mem::take(&mut self.state.entity_lfu_cache), + ); - // Process events one after the other, passing in entity operations - // collected previously to every new event being processed + let _section = self + .metrics + .stream + .stopwatch + .start_section(PROCESS_TRIGGERS_SECTION_NAME); + + // Process events one after the other, passing in entity operations + // collected previously to every new event being processed + let mut res = Ok(block_state); + for trigger in triggers.into_iter().map(TriggerData::Onchain) { match self - .process_triggers( - &proof_of_indexing, + .ctx + .process_trigger_in_hosts( + &self.logger, + self.ctx.instance.hosts_for_trigger(&trigger), &block, - triggers.into_iter().map(TriggerData::Onchain), + &trigger, + // We break out of the loop as soon as res becomes an Err + res.unwrap(), + &proof_of_indexing, &causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, ) .await { - // Triggers processed with no errors or with only deterministic errors. - Ok(block_state) => block_state, - - // Some form of unknown or non-deterministic error ocurred. - Err(MappingError::Unknown(e)) => return Err(BlockProcessingError::Unknown(e)), - Err(MappingError::PossibleReorg(e)) => { - info!(logger, - "Possible reorg detected, retrying"; - "error" => format!("{:#}", e), - ); - - // In case of a possible reorg, we want this function to do nothing and restart the - // block stream so it has a chance to detect the reorg. - // - // The state is unchanged at this point, except for having cleared the entity cache. - // Losing the cache is a bit annoying but not an issue for correctness. - // - // See also b21fa73b-6453-4340-99fb-1a78ec62efb1. - return Ok(Action::Restart); + Ok(state) => res = Ok(state), + Err(mut e) => { + let error_context = trigger.error_context(); + if !error_context.is_empty() { + e = e.context(error_context); + } + e = e.context("failed to process trigger".to_string()); + res = Err(e); + break; } } - }; + } + match res { + // Triggers processed with no errors or with only deterministic errors. + Ok(state) => block_state = state, + + // Some form of unknown or non-deterministic error ocurred. + Err(MappingError::Unknown(e)) => return Err(BlockProcessingError::Unknown(e)), + Err(MappingError::PossibleReorg(e)) => { + info!(logger, + "Possible reorg detected, retrying"; + "error" => format!("{:#}", e), + ); + + // In case of a possible reorg, we want this function to do nothing and restart the + // block stream so it has a chance to detect the reorg. + // + // The state is unchanged at this point, except for having cleared the entity cache. + // Losing the cache is a bit annoying but not an issue for correctness. + // + // See also b21fa73b-6453-4340-99fb-1a78ec62efb1. + return Ok(Action::Restart); + } + } // Check if there are any datasources that have expired in this block. ie: the end_block // of that data source is equal to the block number of the current block. @@ -622,43 +648,6 @@ where } } - async fn process_triggers( - &mut self, - proof_of_indexing: &SharedProofOfIndexing, - block: &Arc, - triggers: impl Iterator>, - causality_region: &str, - ) -> Result { - let mut block_state = BlockState::new( - self.inputs.store.clone(), - std::mem::take(&mut self.state.entity_lfu_cache), - ); - - for trigger in triggers { - block_state = self - .ctx - .process_trigger( - &self.logger, - block, - &trigger, - block_state, - proof_of_indexing, - causality_region, - &self.inputs.debug_fork, - &self.metrics.subgraph, - self.inputs.instrument, - ) - .await - .map_err(move |mut e| { - let error_context = trigger.error_context(); - if !error_context.is_empty() { - e = e.context(error_context); - } - e.context("failed to process trigger".to_string()) - })?; - } - Ok(block_state) - } async fn process_wasm_block( &mut self, proof_of_indexing: &SharedProofOfIndexing, @@ -1024,28 +1013,31 @@ where let proof_of_indexing = None; let causality_region = ""; - block_state = self - .ctx - .process_trigger( - &self.logger, - block, - &TriggerData::Offchain(trigger), - block_state, - &proof_of_indexing, - causality_region, - &self.inputs.debug_fork, - &self.metrics.subgraph, - self.inputs.instrument, - ) - .await - .map_err(move |err| { - let err = match err { - // Ignoring `PossibleReorg` isn't so bad since the subgraph will retry - // non-deterministic errors. - MappingError::PossibleReorg(e) | MappingError::Unknown(e) => e, - }; - err.context("failed to process trigger".to_string()) - })?; + block_state = { + let trigger = TriggerData::Offchain(trigger); + self.ctx + .process_trigger_in_hosts( + &self.logger, + self.ctx.instance.hosts_for_trigger(&trigger), + block, + &trigger, + block_state, + &proof_of_indexing, + causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, + ) + .await + } + .map_err(move |err| { + let err = match err { + // Ignoring `PossibleReorg` isn't so bad since the subgraph will retry + // non-deterministic errors. + MappingError::PossibleReorg(e) | MappingError::Unknown(e) => e, + }; + err.context("failed to process trigger".to_string()) + })?; anyhow::ensure!( !block_state.has_created_on_chain_data_sources(), From 75b65396401da07458697670a517fab9ebcd0863 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 1 Mar 2024 12:39:46 -0800 Subject: [PATCH 05/51] core: Assign the result of calling process_trigger_in_hosts to a variable --- core/src/subgraph/runner.rs | 80 +++++++++++++++++++------------------ 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index 8ae77d25b70..29adb08bf20 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -314,7 +314,7 @@ where // collected previously to every new event being processed let mut res = Ok(block_state); for trigger in triggers.into_iter().map(TriggerData::Onchain) { - match self + let process_res = self .ctx .process_trigger_in_hosts( &self.logger, @@ -329,8 +329,8 @@ where &self.metrics.subgraph, self.inputs.instrument, ) - .await - { + .await; + match process_res { Ok(state) => res = Ok(state), Err(mut e) => { let error_context = trigger.error_context(); @@ -455,7 +455,7 @@ where // Process the triggers in each host in the same order the // corresponding data sources have been created. for trigger in triggers { - block_state = self + let process_res = self .ctx .process_trigger_in_hosts( &logger, @@ -469,18 +469,18 @@ where &self.metrics.subgraph, self.inputs.instrument, ) - .await - .map_err(|e| { - // This treats a `PossibleReorg` as an ordinary error which will fail the subgraph. - // This can cause an unnecessary subgraph failure, to fix it we need to figure out a - // way to revert the effect of `create_dynamic_data_sources` so we may return a - // clean context as in b21fa73b-6453-4340-99fb-1a78ec62efb1. - match e { - MappingError::PossibleReorg(e) | MappingError::Unknown(e) => { - BlockProcessingError::Unknown(e) - } + .await; + block_state = process_res.map_err(|e| { + // This treats a `PossibleReorg` as an ordinary error which will fail the subgraph. + // This can cause an unnecessary subgraph failure, to fix it we need to figure out a + // way to revert the effect of `create_dynamic_data_sources` so we may return a + // clean context as in b21fa73b-6453-4340-99fb-1a78ec62efb1. + match e { + MappingError::PossibleReorg(e) | MappingError::Unknown(e) => { + BlockProcessingError::Unknown(e) } - })?; + } + })?; } } } @@ -1013,31 +1013,33 @@ where let proof_of_indexing = None; let causality_region = ""; - block_state = { - let trigger = TriggerData::Offchain(trigger); - self.ctx - .process_trigger_in_hosts( - &self.logger, - self.ctx.instance.hosts_for_trigger(&trigger), - block, - &trigger, - block_state, - &proof_of_indexing, - causality_region, - &self.inputs.debug_fork, - &self.metrics.subgraph, - self.inputs.instrument, - ) - .await + let trigger = TriggerData::Offchain(trigger); + let process_res = self + .ctx + .process_trigger_in_hosts( + &self.logger, + self.ctx.instance.hosts_for_trigger(&trigger), + block, + &trigger, + block_state, + &proof_of_indexing, + causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, + ) + .await; + match process_res { + Ok(state) => block_state = state, + Err(err) => { + let err = match err { + // Ignoring `PossibleReorg` isn't so bad since the subgraph will retry + // non-deterministic errors. + MappingError::PossibleReorg(e) | MappingError::Unknown(e) => e, + }; + return Err(err.context("failed to process trigger".to_string())); + } } - .map_err(move |err| { - let err = match err { - // Ignoring `PossibleReorg` isn't so bad since the subgraph will retry - // non-deterministic errors. - MappingError::PossibleReorg(e) | MappingError::Unknown(e) => e, - }; - err.context("failed to process trigger".to_string()) - })?; anyhow::ensure!( !block_state.has_created_on_chain_data_sources(), From d27734887ee16ba2bef703ac3ea3c72ad2286f0a Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 1 Mar 2024 13:00:48 -0800 Subject: [PATCH 06/51] core: Inline all calls to IndexingContext.process_trigger_in_hosts --- core/src/subgraph/context/mod.rs | 39 +---------- core/src/subgraph/runner.rs | 109 +++++++++++++++++++++---------- 2 files changed, 76 insertions(+), 72 deletions(-) diff --git a/core/src/subgraph/context/mod.rs b/core/src/subgraph/context/mod.rs index 1776f37e56e..2559033534a 100644 --- a/core/src/subgraph/context/mod.rs +++ b/core/src/subgraph/context/mod.rs @@ -15,7 +15,7 @@ use graph::{ data_source::{ causality_region::CausalityRegionSeq, offchain::{self, Base64}, - CausalityRegion, DataSource, DataSourceTemplate, TriggerData, + CausalityRegion, DataSource, DataSourceTemplate, }, ipfs_client::CidFile, prelude::{ @@ -73,7 +73,7 @@ where pub instances: SubgraphKeepAlive, pub offchain_monitor: OffchainMonitor, pub filter: Option, - trigger_processor: Box>, + pub(crate) trigger_processor: Box>, } impl> IndexingContext { @@ -162,41 +162,6 @@ impl> IndexingContext { Ok(state) } - pub async fn process_trigger_in_hosts<'a>( - &'a self, - logger: &Logger, - hosts: Box + Send + 'a>, - block: &Arc, - trigger: &TriggerData, - state: BlockState, - proof_of_indexing: &SharedProofOfIndexing, - causality_region: &str, - debug_fork: &Option>, - subgraph_metrics: &Arc, - instrument: bool, - ) -> Result { - let triggers: Vec<_> = self.trigger_processor.match_and_decode( - logger, - block, - trigger, - hosts, - subgraph_metrics, - )?; - - self.trigger_processor - .process_trigger( - logger, - triggers, - block, - state, - proof_of_indexing, - causality_region, - debug_fork, - subgraph_metrics, - instrument, - ) - .await - } /// Removes data sources hosts with a creation block greater or equal to `reverted_block`, so /// that they are no longer candidates for `process_trigger`. diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index 29adb08bf20..40ea711c96d 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -314,22 +314,34 @@ where // collected previously to every new event being processed let mut res = Ok(block_state); for trigger in triggers.into_iter().map(TriggerData::Onchain) { - let process_res = self - .ctx - .process_trigger_in_hosts( + let process_res = { + let triggers_res = self.ctx.trigger_processor.match_and_decode( &self.logger, - self.ctx.instance.hosts_for_trigger(&trigger), &block, &trigger, - // We break out of the loop as soon as res becomes an Err - res.unwrap(), - &proof_of_indexing, - &causality_region, - &self.inputs.debug_fork, + self.ctx.instance.hosts_for_trigger(&trigger), &self.metrics.subgraph, - self.inputs.instrument, - ) - .await; + ); + match triggers_res { + Ok(triggers) => { + self.ctx + .trigger_processor + .process_trigger( + &self.logger, + triggers, + &block, + res.unwrap(), + &proof_of_indexing, + &causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, + ) + .await + } + Err(e) => Err(e), + } + }; match process_res { Ok(state) => res = Ok(state), Err(mut e) => { @@ -455,21 +467,35 @@ where // Process the triggers in each host in the same order the // corresponding data sources have been created. for trigger in triggers { - let process_res = self - .ctx - .process_trigger_in_hosts( + let trigger = TriggerData::Onchain(trigger); + let process_res = { + let triggers_res = self.ctx.trigger_processor.match_and_decode( &logger, - Box::new(runtime_hosts.iter().map(|host| host.as_ref())), &block, - &TriggerData::Onchain(trigger), - block_state, - &proof_of_indexing, - &causality_region, - &self.inputs.debug_fork, + &trigger, + Box::new(runtime_hosts.iter().map(Arc::as_ref)), &self.metrics.subgraph, - self.inputs.instrument, - ) - .await; + ); + match triggers_res { + Ok(triggers) => { + self.ctx + .trigger_processor + .process_trigger( + &logger, + triggers, + &block, + block_state, + &proof_of_indexing, + &causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, + ) + .await + } + Err(e) => Err(e), + } + }; block_state = process_res.map_err(|e| { // This treats a `PossibleReorg` as an ordinary error which will fail the subgraph. // This can cause an unnecessary subgraph failure, to fix it we need to figure out a @@ -1014,21 +1040,34 @@ where let causality_region = ""; let trigger = TriggerData::Offchain(trigger); - let process_res = self - .ctx - .process_trigger_in_hosts( + let process_res = { + let triggers_res = self.ctx.trigger_processor.match_and_decode( &self.logger, - self.ctx.instance.hosts_for_trigger(&trigger), block, &trigger, - block_state, - &proof_of_indexing, - causality_region, - &self.inputs.debug_fork, + self.ctx.instance.hosts_for_trigger(&trigger), &self.metrics.subgraph, - self.inputs.instrument, - ) - .await; + ); + match triggers_res { + Ok(triggers) => { + self.ctx + .trigger_processor + .process_trigger( + &self.logger, + triggers, + block, + block_state, + &proof_of_indexing, + causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, + ) + .await + } + Err(e) => Err(e), + } + }; match process_res { Ok(state) => block_state = state, Err(err) => { From 7d247dafd49ac859b25b4d5cf092d8a73d1cf56a Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 1 Mar 2024 14:14:20 -0800 Subject: [PATCH 07/51] core: Restructure the loop in SubgraphRunner.process_block The loop over 'normal' events is broken into distinct steps for match_and_decode and for process_block --- core/src/subgraph/runner.rs | 75 +++++++++++++---------- graph/src/components/trigger_processor.rs | 19 ++++++ 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index 40ea711c96d..3a7d822bfde 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -10,6 +10,7 @@ use graph::blockchain::block_stream::{ use graph::blockchain::{Block, BlockTime, Blockchain, DataSource as _, TriggerFilter as _}; use graph::components::store::{EmptyStore, GetScope, ReadStore, StoredDynamicDataSource}; use graph::components::subgraph::InstanceDSTemplate; +use graph::components::trigger_processor::ErrorContext; use graph::components::{ store::ModificationsAndCache, subgraph::{MappingError, PoICausalityRegion, ProofOfIndexing, SharedProofOfIndexing}, @@ -310,49 +311,55 @@ where .stopwatch .start_section(PROCESS_TRIGGERS_SECTION_NAME); + fn add_context(mut e: MappingError, trigger: &dyn ErrorContext) -> MappingError { + if let Some(error_context) = trigger.error_context() { + if !error_context.is_empty() { + e = e.context(error_context) + } + } + e = e.context("failed to process trigger".to_string()); + e + } + // Process events one after the other, passing in entity operations // collected previously to every new event being processed let mut res = Ok(block_state); for trigger in triggers.into_iter().map(TriggerData::Onchain) { - let process_res = { - let triggers_res = self.ctx.trigger_processor.match_and_decode( + let triggers_res = self + .ctx + .trigger_processor + .match_and_decode( &self.logger, &block, &trigger, self.ctx.instance.hosts_for_trigger(&trigger), &self.metrics.subgraph, - ); - match triggers_res { - Ok(triggers) => { - self.ctx - .trigger_processor - .process_trigger( - &self.logger, - triggers, - &block, - res.unwrap(), - &proof_of_indexing, - &causality_region, - &self.inputs.debug_fork, - &self.metrics.subgraph, - self.inputs.instrument, - ) - .await - } - Err(e) => Err(e), - } - }; - match process_res { - Ok(state) => res = Ok(state), - Err(mut e) => { - let error_context = trigger.error_context(); - if !error_context.is_empty() { - e = e.context(error_context); - } - e = e.context("failed to process trigger".to_string()); - res = Err(e); - break; - } + ) + .map_err(|e| add_context(e, &trigger)); + if let Err(e) = triggers_res { + res = Err(e); + break; + } + + let hosted_triggers = triggers_res.unwrap(); + res = self + .ctx + .trigger_processor + .process_trigger( + &self.logger, + hosted_triggers, + &block, + res.unwrap(), + &proof_of_indexing, + &causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, + ) + .await + .map_err(|e| add_context(e, &trigger)); + if res.is_err() { + break; } } match res { diff --git a/graph/src/components/trigger_processor.rs b/graph/src/components/trigger_processor.rs index 2e7b5391b14..1ba0111e7b0 100644 --- a/graph/src/components/trigger_processor.rs +++ b/graph/src/components/trigger_processor.rs @@ -24,6 +24,25 @@ where pub mapping_trigger: TriggerWithHandler>, } +pub trait ErrorContext { + fn error_context(&self) -> Option; +} + +impl ErrorContext for TriggerData { + fn error_context(&self) -> Option { + Some(self.error_context()) + } +} + +impl<'a, C> ErrorContext for HostedTrigger<'a, C> +where + C: Blockchain, +{ + fn error_context(&self) -> Option { + self.mapping_trigger.trigger.error_context() + } +} + #[async_trait] pub trait TriggerProcessor: Sync + Send where From cbf552098c90e7a6185d04d77e4c9a05e10cf9d6 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 1 Mar 2024 14:39:55 -0800 Subject: [PATCH 08/51] core: Split match_and_decode and processing into separate loops --- core/src/subgraph/runner.rs | 93 ++++++++++++----------- graph/src/components/subgraph/host.rs | 9 +++ graph/src/components/trigger_processor.rs | 19 ----- 3 files changed, 57 insertions(+), 64 deletions(-) diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index 3a7d822bfde..e02dcf8cd5b 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -10,7 +10,6 @@ use graph::blockchain::block_stream::{ use graph::blockchain::{Block, BlockTime, Blockchain, DataSource as _, TriggerFilter as _}; use graph::components::store::{EmptyStore, GetScope, ReadStore, StoredDynamicDataSource}; use graph::components::subgraph::InstanceDSTemplate; -use graph::components::trigger_processor::ErrorContext; use graph::components::{ store::ModificationsAndCache, subgraph::{MappingError, PoICausalityRegion, ProofOfIndexing, SharedProofOfIndexing}, @@ -311,57 +310,61 @@ where .stopwatch .start_section(PROCESS_TRIGGERS_SECTION_NAME); - fn add_context(mut e: MappingError, trigger: &dyn ErrorContext) -> MappingError { - if let Some(error_context) = trigger.error_context() { - if !error_context.is_empty() { - e = e.context(error_context) - } - } - e = e.context("failed to process trigger".to_string()); - e - } + // Match and decode all triggers in the block + let match_res: Result, _> = triggers + .into_iter() + .map(TriggerData::Onchain) + .map(|trigger| { + self.ctx + .trigger_processor + .match_and_decode( + &self.logger, + &block, + &trigger, + self.ctx.instance.hosts_for_trigger(&trigger), + &self.metrics.subgraph, + ) + .map_err(|e| e.add_trigger_context(&trigger)) + .map(|hosted_triggers| (trigger, hosted_triggers)) + }) + .collect::, _>>(); // Process events one after the other, passing in entity operations // collected previously to every new event being processed let mut res = Ok(block_state); - for trigger in triggers.into_iter().map(TriggerData::Onchain) { - let triggers_res = self - .ctx - .trigger_processor - .match_and_decode( - &self.logger, - &block, - &trigger, - self.ctx.instance.hosts_for_trigger(&trigger), - &self.metrics.subgraph, - ) - .map_err(|e| add_context(e, &trigger)); - if let Err(e) = triggers_res { + match match_res { + Ok(ts) => { + for (trigger, hosted_triggers) in ts { + let process_res = self + .ctx + .trigger_processor + .process_trigger( + &self.logger, + hosted_triggers, + &block, + res.unwrap(), + &proof_of_indexing, + &causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, + ) + .await + .map_err(|e| e.add_trigger_context(&trigger)); + match process_res { + Ok(state) => res = Ok(state), + Err(e) => { + res = Err(e); + break; + } + } + } + } + Err(e) => { res = Err(e); - break; } + }; - let hosted_triggers = triggers_res.unwrap(); - res = self - .ctx - .trigger_processor - .process_trigger( - &self.logger, - hosted_triggers, - &block, - res.unwrap(), - &proof_of_indexing, - &causality_region, - &self.inputs.debug_fork, - &self.metrics.subgraph, - self.inputs.instrument, - ) - .await - .map_err(|e| add_context(e, &trigger)); - if res.is_err() { - break; - } - } match res { // Triggers processed with no errors or with only deterministic errors. Ok(state) => block_state = state, diff --git a/graph/src/components/subgraph/host.rs b/graph/src/components/subgraph/host.rs index f133e0ac086..4390ce87c68 100644 --- a/graph/src/components/subgraph/host.rs +++ b/graph/src/components/subgraph/host.rs @@ -48,6 +48,15 @@ impl MappingError { Unknown(e) => Unknown(e.context(s)), } } + + pub fn add_trigger_context(mut self, trigger: &TriggerData) -> MappingError { + let error_context = trigger.error_context(); + if !error_context.is_empty() { + self = self.context(error_context) + } + self = self.context("failed to process trigger".to_string()); + self + } } /// Common trait for runtime host implementations. diff --git a/graph/src/components/trigger_processor.rs b/graph/src/components/trigger_processor.rs index 1ba0111e7b0..2e7b5391b14 100644 --- a/graph/src/components/trigger_processor.rs +++ b/graph/src/components/trigger_processor.rs @@ -24,25 +24,6 @@ where pub mapping_trigger: TriggerWithHandler>, } -pub trait ErrorContext { - fn error_context(&self) -> Option; -} - -impl ErrorContext for TriggerData { - fn error_context(&self) -> Option { - Some(self.error_context()) - } -} - -impl<'a, C> ErrorContext for HostedTrigger<'a, C> -where - C: Blockchain, -{ - fn error_context(&self) -> Option { - self.mapping_trigger.trigger.error_context() - } -} - #[async_trait] pub trait TriggerProcessor: Sync + Send where From a025ad076e6d08e2a05a917ee136a76d92e56f9d Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Sat, 2 Mar 2024 15:53:01 -0800 Subject: [PATCH 09/51] test: Use substreams::TriggerProcessor for substreams --- tests/src/fixture/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/src/fixture/mod.rs b/tests/src/fixture/mod.rs index 6dbdaf97688..b83c9387c3a 100644 --- a/tests/src/fixture/mod.rs +++ b/tests/src/fixture/mod.rs @@ -224,7 +224,9 @@ impl TestContext { RuntimeHostBuilder, > { let (logger, deployment, raw) = self.get_runner_context().await; - let tp: Box> = Box::new(SubgraphTriggerProcessor {}); + let tp: Box> = Box::new( + graph_chain_substreams::TriggerProcessor::new(deployment.clone()), + ); self.instance_manager .build_subgraph_runner( From 5859c21025b191612831a03dface1cce2cdfa074 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Sat, 2 Mar 2024 16:40:33 -0800 Subject: [PATCH 10/51] chain, core, graph: Put match_and_decode into a different trait --- chain/substreams/src/trigger.rs | 15 +----------- core/src/subgraph/context/mod.rs | 4 ++++ core/src/subgraph/instance_manager.rs | 3 +++ core/src/subgraph/runner.rs | 6 ++--- core/src/subgraph/trigger_processor.rs | 10 +++++++- graph/src/components/trigger_processor.rs | 28 +++++++++++++++-------- 6 files changed, 39 insertions(+), 27 deletions(-) diff --git a/chain/substreams/src/trigger.rs b/chain/substreams/src/trigger.rs index faf7a250d31..2360e8a711f 100644 --- a/chain/substreams/src/trigger.rs +++ b/chain/substreams/src/trigger.rs @@ -3,11 +3,9 @@ use std::sync::Arc; use anyhow::Error; use graph::{ blockchain::{ - self, block_stream::BlockWithTriggers, BlockPtr, Blockchain, EmptyNodeCapabilities, - MappingTriggerTrait, + self, block_stream::BlockWithTriggers, BlockPtr, EmptyNodeCapabilities, MappingTriggerTrait, }, components::{ - metrics::subgraph::SubgraphInstanceMetrics, store::{DeploymentLocator, SubgraphFork}, subgraph::{MappingError, ProofOfIndexingEvent, SharedProofOfIndexing}, trigger_processor::HostedTrigger, @@ -249,15 +247,4 @@ where Ok(state) } - - fn match_and_decode<'a>( - &'a self, - _: &Logger, - _: &Arc<::Block>, - _: &graph::data_source::TriggerData, - _: Box + Send + 'a>, - _: &Arc, - ) -> Result>, MappingError> { - Ok(vec![]) - } } diff --git a/core/src/subgraph/context/mod.rs b/core/src/subgraph/context/mod.rs index 2559033534a..a7abe77432c 100644 --- a/core/src/subgraph/context/mod.rs +++ b/core/src/subgraph/context/mod.rs @@ -10,6 +10,7 @@ use graph::{ components::{ store::{DeploymentId, SubgraphFork}, subgraph::{HostMetrics, MappingError, RuntimeHost as _, SharedProofOfIndexing}, + trigger_processor::Decoder, }, data::subgraph::SubgraphManifest, data_source::{ @@ -74,6 +75,7 @@ where pub offchain_monitor: OffchainMonitor, pub filter: Option, pub(crate) trigger_processor: Box>, + pub(crate) decoder: Box>, } impl> IndexingContext { @@ -85,6 +87,7 @@ impl> IndexingContext { instances: SubgraphKeepAlive, offchain_monitor: OffchainMonitor, trigger_processor: Box>, + decoder: Box>, ) -> Self { let instance = SubgraphInstance::new( manifest, @@ -99,6 +102,7 @@ impl> IndexingContext { offchain_monitor, filter: None, trigger_processor, + decoder, } } diff --git a/core/src/subgraph/instance_manager.rs b/core/src/subgraph/instance_manager.rs index d0176238013..e709cb92e6a 100644 --- a/core/src/subgraph/instance_manager.rs +++ b/core/src/subgraph/instance_manager.rs @@ -426,6 +426,8 @@ impl SubgraphInstanceManager { instrument, }; + let decoder = Box::new(crate::subgraph::Decoder {}); + // Initialize the indexing context, including both static and dynamic data sources. // The order of inclusion is the order of processing when a same trigger matches // multiple data sources. @@ -438,6 +440,7 @@ impl SubgraphInstanceManager { self.instances.cheap_clone(), offchain_monitor, tp, + decoder, ); for data_source in data_sources { ctx.add_dynamic_data_source(&logger, data_source)?; diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index e02dcf8cd5b..082916ffe9a 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -316,7 +316,7 @@ where .map(TriggerData::Onchain) .map(|trigger| { self.ctx - .trigger_processor + .decoder .match_and_decode( &self.logger, &block, @@ -479,7 +479,7 @@ where for trigger in triggers { let trigger = TriggerData::Onchain(trigger); let process_res = { - let triggers_res = self.ctx.trigger_processor.match_and_decode( + let triggers_res = self.ctx.decoder.match_and_decode( &logger, &block, &trigger, @@ -1051,7 +1051,7 @@ where let trigger = TriggerData::Offchain(trigger); let process_res = { - let triggers_res = self.ctx.trigger_processor.match_and_decode( + let triggers_res = self.ctx.decoder.match_and_decode( &self.logger, block, &trigger, diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index f89078b770d..74762e29731 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -3,7 +3,7 @@ use graph::blockchain::{Block, Blockchain}; use graph::cheap_clone::CheapClone; use graph::components::store::SubgraphFork; use graph::components::subgraph::{MappingError, SharedProofOfIndexing}; -use graph::components::trigger_processor::HostedTrigger; +use graph::components::trigger_processor::{Decoder as DecoderTrait, HostedTrigger}; use graph::data_source::TriggerData; use graph::prelude::tokio::time::Instant; use graph::prelude::{ @@ -86,7 +86,15 @@ where Ok(state) } +} + +pub struct Decoder {} +impl DecoderTrait for Decoder +where + C: Blockchain, + T: RuntimeHostBuilder, +{ fn match_and_decode<'a>( &'a self, logger: &Logger, diff --git a/graph/src/components/trigger_processor.rs b/graph/src/components/trigger_processor.rs index 2e7b5391b14..08f8d302fd3 100644 --- a/graph/src/components/trigger_processor.rs +++ b/graph/src/components/trigger_processor.rs @@ -30,15 +30,6 @@ where C: Blockchain, T: RuntimeHostBuilder, { - fn match_and_decode<'a>( - &'a self, - logger: &Logger, - block: &Arc, - trigger: &TriggerData, - hosts: Box + Send + 'a>, - subgraph_metrics: &Arc, - ) -> Result>, MappingError>; - async fn process_trigger<'a>( &'a self, logger: &Logger, @@ -52,3 +43,22 @@ where instrument: bool, ) -> Result; } + +/// A trait for taking triggers as `TriggerData` (usually from the block +/// stream) and turning them into `HostedTrigger`s that are ready to run. +/// +/// The output triggers will be run in the order in which they are returned. +pub trait Decoder: Sync + Send +where + C: Blockchain, + T: RuntimeHostBuilder, +{ + fn match_and_decode<'a>( + &'a self, + logger: &Logger, + block: &Arc, + trigger: &TriggerData, + hosts: Box + Send + 'a>, + subgraph_metrics: &Arc, + ) -> Result>, MappingError>; +} From bb639c0848f8f8a9e4a19e721b98fba12d061c74 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Mon, 4 Mar 2024 10:13:38 -0800 Subject: [PATCH 11/51] core: Split match_and_decode and processing for new data sources --- core/src/subgraph/runner.rs | 99 ++++++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index 082916ffe9a..a4c18023ba3 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -476,48 +476,69 @@ where // Process the triggers in each host in the same order the // corresponding data sources have been created. - for trigger in triggers { - let trigger = TriggerData::Onchain(trigger); - let process_res = { - let triggers_res = self.ctx.decoder.match_and_decode( - &logger, - &block, - &trigger, - Box::new(runtime_hosts.iter().map(Arc::as_ref)), - &self.metrics.subgraph, - ); - match triggers_res { - Ok(triggers) => { - self.ctx - .trigger_processor - .process_trigger( - &logger, - triggers, - &block, - block_state, - &proof_of_indexing, - &causality_region, - &self.inputs.debug_fork, - &self.metrics.subgraph, - self.inputs.instrument, - ) - .await - } - Err(e) => Err(e), - } - }; - block_state = process_res.map_err(|e| { - // This treats a `PossibleReorg` as an ordinary error which will fail the subgraph. - // This can cause an unnecessary subgraph failure, to fix it we need to figure out a - // way to revert the effect of `create_dynamic_data_sources` so we may return a - // clean context as in b21fa73b-6453-4340-99fb-1a78ec62efb1. - match e { - MappingError::PossibleReorg(e) | MappingError::Unknown(e) => { - BlockProcessingError::Unknown(e) + let match_res: Result, _> = triggers + .into_iter() + .map(TriggerData::Onchain) + .map(|trigger| { + self.ctx + .decoder + .match_and_decode( + &self.logger, + &block, + &trigger, + Box::new(runtime_hosts.iter().map(Arc::as_ref)), + &self.metrics.subgraph, + ) + .map_err(|e| e.add_trigger_context(&trigger)) + .map(|hosted_triggers| (trigger, hosted_triggers)) + }) + .collect::, _>>(); + + let mut res = Ok(block_state); + match match_res { + Ok(ts) => { + for (trigger, hosted_triggers) in ts { + let process_res = self + .ctx + .trigger_processor + .process_trigger( + &self.logger, + hosted_triggers, + &block, + res.unwrap(), + &proof_of_indexing, + &causality_region, + &self.inputs.debug_fork, + &self.metrics.subgraph, + self.inputs.instrument, + ) + .await + .map_err(|e| e.add_trigger_context(&trigger)); + match process_res { + Ok(state) => res = Ok(state), + Err(e) => { + res = Err(e); + break; + } } } - })?; + } + Err(e) => { + res = Err(e); + } } + + block_state = res.map_err(|e| { + // This treats a `PossibleReorg` as an ordinary error which will fail the subgraph. + // This can cause an unnecessary subgraph failure, to fix it we need to figure out a + // way to revert the effect of `create_dynamic_data_sources` so we may return a + // clean context as in b21fa73b-6453-4340-99fb-1a78ec62efb1. + match e { + MappingError::PossibleReorg(e) | MappingError::Unknown(e) => { + BlockProcessingError::Unknown(e) + } + } + })?; } } From beff28b5cf4c42a0f672ea4c5d60f7cdc20e440a Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Mon, 4 Mar 2024 11:55:26 -0800 Subject: [PATCH 12/51] core, graph: Simplify handling of match_and_decode result --- core/src/subgraph/runner.rs | 62 ++++++++++------------- core/src/subgraph/trigger_processor.rs | 40 ++++++++++++--- graph/src/components/trigger_processor.rs | 14 ++++- 3 files changed, 72 insertions(+), 44 deletions(-) diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index a4c18023ba3..f18351ee777 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -315,17 +315,14 @@ where .into_iter() .map(TriggerData::Onchain) .map(|trigger| { - self.ctx - .decoder - .match_and_decode( - &self.logger, - &block, - &trigger, - self.ctx.instance.hosts_for_trigger(&trigger), - &self.metrics.subgraph, - ) - .map_err(|e| e.add_trigger_context(&trigger)) - .map(|hosted_triggers| (trigger, hosted_triggers)) + let hosts = self.ctx.instance.hosts_for_trigger(&trigger); + self.ctx.decoder.match_and_decode( + &self.logger, + &block, + trigger, + hosts, + &self.metrics.subgraph, + ) }) .collect::, _>>(); @@ -333,14 +330,14 @@ where // collected previously to every new event being processed let mut res = Ok(block_state); match match_res { - Ok(ts) => { - for (trigger, hosted_triggers) in ts { + Ok(runnables) => { + for runnable in runnables { let process_res = self .ctx .trigger_processor .process_trigger( &self.logger, - hosted_triggers, + runnable.hosted_triggers, &block, res.unwrap(), &proof_of_indexing, @@ -350,7 +347,7 @@ where self.inputs.instrument, ) .await - .map_err(|e| e.add_trigger_context(&trigger)); + .map_err(|e| e.add_trigger_context(&runnable.trigger)); match process_res { Ok(state) => res = Ok(state), Err(e) => { @@ -480,30 +477,26 @@ where .into_iter() .map(TriggerData::Onchain) .map(|trigger| { - self.ctx - .decoder - .match_and_decode( - &self.logger, - &block, - &trigger, - Box::new(runtime_hosts.iter().map(Arc::as_ref)), - &self.metrics.subgraph, - ) - .map_err(|e| e.add_trigger_context(&trigger)) - .map(|hosted_triggers| (trigger, hosted_triggers)) + self.ctx.decoder.match_and_decode( + &self.logger, + &block, + trigger, + Box::new(runtime_hosts.iter().map(Arc::as_ref)), + &self.metrics.subgraph, + ) }) .collect::, _>>(); let mut res = Ok(block_state); match match_res { - Ok(ts) => { - for (trigger, hosted_triggers) in ts { + Ok(runnables) => { + for runnable in runnables { let process_res = self .ctx .trigger_processor .process_trigger( &self.logger, - hosted_triggers, + runnable.hosted_triggers, &block, res.unwrap(), &proof_of_indexing, @@ -513,7 +506,7 @@ where self.inputs.instrument, ) .await - .map_err(|e| e.add_trigger_context(&trigger)); + .map_err(|e| e.add_trigger_context(&runnable.trigger)); match process_res { Ok(state) => res = Ok(state), Err(e) => { @@ -1072,20 +1065,21 @@ where let trigger = TriggerData::Offchain(trigger); let process_res = { + let hosts = self.ctx.instance.hosts_for_trigger(&trigger); let triggers_res = self.ctx.decoder.match_and_decode( &self.logger, block, - &trigger, - self.ctx.instance.hosts_for_trigger(&trigger), + trigger, + hosts, &self.metrics.subgraph, ); match triggers_res { - Ok(triggers) => { + Ok(runnable) => { self.ctx .trigger_processor .process_trigger( &self.logger, - triggers, + runnable.hosted_triggers, block, block_state, &proof_of_indexing, diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index 74762e29731..8ef5c503e96 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -3,7 +3,9 @@ use graph::blockchain::{Block, Blockchain}; use graph::cheap_clone::CheapClone; use graph::components::store::SubgraphFork; use graph::components::subgraph::{MappingError, SharedProofOfIndexing}; -use graph::components::trigger_processor::{Decoder as DecoderTrait, HostedTrigger}; +use graph::components::trigger_processor::{ + Decoder as DecoderTrait, HostedTrigger, RunnableTriggers, +}; use graph::data_source::TriggerData; use graph::prelude::tokio::time::Instant; use graph::prelude::{ @@ -90,19 +92,19 @@ where pub struct Decoder {} -impl DecoderTrait for Decoder -where - C: Blockchain, - T: RuntimeHostBuilder, -{ - fn match_and_decode<'a>( +impl Decoder { + fn match_and_decode_inner<'a, C, T>( &'a self, logger: &Logger, block: &Arc, trigger: &TriggerData, hosts: Box + Send + 'a>, subgraph_metrics: &Arc, - ) -> Result>, MappingError> { + ) -> Result>, MappingError> + where + C: Blockchain, + T: RuntimeHostBuilder, + { let mut host_mapping = vec![]; { @@ -126,3 +128,25 @@ where Ok(host_mapping) } } + +impl DecoderTrait for Decoder +where + C: Blockchain, + T: RuntimeHostBuilder, +{ + fn match_and_decode<'a>( + &'a self, + logger: &Logger, + block: &Arc, + trigger: TriggerData, + hosts: Box + Send + 'a>, + subgraph_metrics: &Arc, + ) -> Result, MappingError> { + self.match_and_decode_inner::(logger, block, &trigger, hosts, subgraph_metrics) + .map_err(|e| e.add_trigger_context(&trigger)) + .map(|hosted_triggers| RunnableTriggers { + trigger, + hosted_triggers, + }) + } +} diff --git a/graph/src/components/trigger_processor.rs b/graph/src/components/trigger_processor.rs index 08f8d302fd3..d5a1048ede3 100644 --- a/graph/src/components/trigger_processor.rs +++ b/graph/src/components/trigger_processor.rs @@ -24,6 +24,16 @@ where pub mapping_trigger: TriggerWithHandler>, } +/// The `TriggerData` and the `HostedTriggers` that were derived from it. We +/// need to hang on to the `TriggerData` solely for error reporting. +pub struct RunnableTriggers<'a, C> +where + C: Blockchain, +{ + pub trigger: TriggerData, + pub hosted_triggers: Vec>, +} + #[async_trait] pub trait TriggerProcessor: Sync + Send where @@ -57,8 +67,8 @@ where &'a self, logger: &Logger, block: &Arc, - trigger: &TriggerData, + trigger: TriggerData, hosts: Box + Send + 'a>, subgraph_metrics: &Arc, - ) -> Result>, MappingError>; + ) -> Result, MappingError>; } From ce15f1f58ae92e4aa6e3a7fa59ab8b2c1f9f05c0 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Mon, 4 Mar 2024 13:38:47 -0800 Subject: [PATCH 13/51] core, graph: Decoder doesn't need to be a trait Also introduce match_and_decode_many --- core/src/subgraph/context/mod.rs | 7 ++-- core/src/subgraph/runner.rs | 45 +++++++++------------- core/src/subgraph/trigger_processor.rs | 46 +++++++++++++++++------ graph/src/components/trigger_processor.rs | 22 +++++++++++ 4 files changed, 78 insertions(+), 42 deletions(-) diff --git a/core/src/subgraph/context/mod.rs b/core/src/subgraph/context/mod.rs index a7abe77432c..8837361c4dc 100644 --- a/core/src/subgraph/context/mod.rs +++ b/core/src/subgraph/context/mod.rs @@ -10,7 +10,6 @@ use graph::{ components::{ store::{DeploymentId, SubgraphFork}, subgraph::{HostMetrics, MappingError, RuntimeHost as _, SharedProofOfIndexing}, - trigger_processor::Decoder, }, data::subgraph::SubgraphManifest, data_source::{ @@ -32,6 +31,8 @@ use std::{collections::HashMap, time::Instant}; use self::instance::SubgraphInstance; +use super::Decoder; + #[derive(Clone, Debug)] pub struct SubgraphKeepAlive { alive_map: Arc>>, @@ -75,7 +76,7 @@ where pub offchain_monitor: OffchainMonitor, pub filter: Option, pub(crate) trigger_processor: Box>, - pub(crate) decoder: Box>, + pub(crate) decoder: Box, } impl> IndexingContext { @@ -87,7 +88,7 @@ impl> IndexingContext { instances: SubgraphKeepAlive, offchain_monitor: OffchainMonitor, trigger_processor: Box>, - decoder: Box>, + decoder: Box, ) -> Self { let instance = SubgraphInstance::new( manifest, diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index f18351ee777..d46262bda93 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -311,20 +311,14 @@ where .start_section(PROCESS_TRIGGERS_SECTION_NAME); // Match and decode all triggers in the block - let match_res: Result, _> = triggers - .into_iter() - .map(TriggerData::Onchain) - .map(|trigger| { - let hosts = self.ctx.instance.hosts_for_trigger(&trigger); - self.ctx.decoder.match_and_decode( - &self.logger, - &block, - trigger, - hosts, - &self.metrics.subgraph, - ) - }) - .collect::, _>>(); + let hosts_filter = |trigger: &TriggerData| self.ctx.instance.hosts_for_trigger(trigger); + let match_res = self.ctx.decoder.match_and_decode_many::( + &logger, + &block, + triggers.into_iter().map(TriggerData::Onchain), + hosts_filter, + &self.metrics.subgraph, + ); // Process events one after the other, passing in entity operations // collected previously to every new event being processed @@ -473,19 +467,14 @@ where // Process the triggers in each host in the same order the // corresponding data sources have been created. - let match_res: Result, _> = triggers - .into_iter() - .map(TriggerData::Onchain) - .map(|trigger| { - self.ctx.decoder.match_and_decode( - &self.logger, - &block, - trigger, - Box::new(runtime_hosts.iter().map(Arc::as_ref)), - &self.metrics.subgraph, - ) - }) - .collect::, _>>(); + let match_res: Result, _> = + self.ctx.decoder.match_and_decode_many::( + &logger, + &block, + triggers.into_iter().map(TriggerData::Onchain), + |_| Box::new(runtime_hosts.iter().map(Arc::as_ref)), + &self.metrics.subgraph, + ); let mut res = Ok(block_state); match match_res { @@ -1066,7 +1055,7 @@ where let trigger = TriggerData::Offchain(trigger); let process_res = { let hosts = self.ctx.instance.hosts_for_trigger(&trigger); - let triggers_res = self.ctx.decoder.match_and_decode( + let triggers_res = self.ctx.decoder.match_and_decode::( &self.logger, block, trigger, diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index 8ef5c503e96..d611995cbde 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -3,9 +3,7 @@ use graph::blockchain::{Block, Blockchain}; use graph::cheap_clone::CheapClone; use graph::components::store::SubgraphFork; use graph::components::subgraph::{MappingError, SharedProofOfIndexing}; -use graph::components::trigger_processor::{ - Decoder as DecoderTrait, HostedTrigger, RunnableTriggers, -}; +use graph::components::trigger_processor::{HostedTrigger, RunnableTriggers}; use graph::data_source::TriggerData; use graph::prelude::tokio::time::Instant; use graph::prelude::{ @@ -90,6 +88,10 @@ where } } +/// A helper for taking triggers as `TriggerData` (usually from the block +/// stream) and turning them into `HostedTrigger`s that are ready to run. +/// +/// The output triggers will be run in the order in which they are returned. pub struct Decoder {} impl Decoder { @@ -127,21 +129,19 @@ impl Decoder { } Ok(host_mapping) } -} -impl DecoderTrait for Decoder -where - C: Blockchain, - T: RuntimeHostBuilder, -{ - fn match_and_decode<'a>( + pub(crate) fn match_and_decode<'a, C, T>( &'a self, logger: &Logger, block: &Arc, trigger: TriggerData, hosts: Box + Send + 'a>, subgraph_metrics: &Arc, - ) -> Result, MappingError> { + ) -> Result, MappingError> + where + C: Blockchain, + T: RuntimeHostBuilder, + { self.match_and_decode_inner::(logger, block, &trigger, hosts, subgraph_metrics) .map_err(|e| e.add_trigger_context(&trigger)) .map(|hosted_triggers| RunnableTriggers { @@ -149,4 +149,28 @@ where hosted_triggers, }) } + + pub(crate) fn match_and_decode_many<'a, C, T, F>( + &'a self, + logger: &Logger, + block: &Arc, + triggers: impl Iterator>, + hosts_filter: F, + subgraph_metrics: &Arc, + ) -> Result>, MappingError> + where + C: Blockchain, + T: RuntimeHostBuilder, + F: Fn(&TriggerData) -> Box + Send + 'a>, + { + let mut runnables = vec![]; + for trigger in triggers { + let hosts = hosts_filter(&trigger); + match self.match_and_decode::(logger, block, trigger, hosts, subgraph_metrics) { + Ok(runnable_triggers) => runnables.push(runnable_triggers), + Err(e) => return Err(e), + } + } + Ok(runnables) + } } diff --git a/graph/src/components/trigger_processor.rs b/graph/src/components/trigger_processor.rs index d5a1048ede3..f21fe5b7894 100644 --- a/graph/src/components/trigger_processor.rs +++ b/graph/src/components/trigger_processor.rs @@ -71,4 +71,26 @@ where hosts: Box + Send + 'a>, subgraph_metrics: &Arc, ) -> Result, MappingError>; + + fn match_and_decode_many<'a, F>( + &'a self, + logger: &Logger, + block: &Arc, + triggers: Box>>, + hosts_filter: F, + subgraph_metrics: &Arc, + ) -> Result>, MappingError> + where + F: Fn(&TriggerData) -> Box + Send + 'a>, + { + let mut runnables = vec![]; + for trigger in triggers { + let hosts = hosts_filter(&trigger); + match self.match_and_decode(logger, block, trigger, hosts, subgraph_metrics) { + Ok(runnable_triggers) => runnables.push(runnable_triggers), + Err(e) => return Err(e), + } + } + Ok(runnables) + } } From 27d62beda9d20d31bd344634a06e7a230eb6deab Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Thu, 29 Feb 2024 12:40:30 -0800 Subject: [PATCH 14/51] all: Pass the spec version to DataSource.validate --- chain/arweave/src/data_source.rs | 2 +- chain/cosmos/src/data_source.rs | 8 ++++---- chain/ethereum/src/data_source.rs | 2 +- chain/near/src/chain.rs | 11 ++++++----- chain/near/src/data_source.rs | 2 +- chain/starknet/src/chain.rs | 8 ++++---- chain/starknet/src/data_source.rs | 2 +- chain/substreams/src/data_source.rs | 19 +++++++++++++------ graph/src/blockchain/mock.rs | 2 +- graph/src/blockchain/mod.rs | 2 +- graph/src/data/subgraph/mod.rs | 2 +- graph/src/data_source/mod.rs | 4 ++-- .../tests/chain/ethereum/manifest.rs | 10 +++++----- 13 files changed, 41 insertions(+), 33 deletions(-) diff --git a/chain/arweave/src/data_source.rs b/chain/arweave/src/data_source.rs index d106829279f..f94d6260785 100644 --- a/chain/arweave/src/data_source.rs +++ b/chain/arweave/src/data_source.rs @@ -157,7 +157,7 @@ impl blockchain::DataSource for DataSource { todo!() } - fn validate(&self) -> Vec { + fn validate(&self, _: &semver::Version) -> Vec { let mut errors = Vec::new(); if self.kind != ARWEAVE_KIND { diff --git a/chain/cosmos/src/data_source.rs b/chain/cosmos/src/data_source.rs index 9d8b2f26672..d7f4f951274 100644 --- a/chain/cosmos/src/data_source.rs +++ b/chain/cosmos/src/data_source.rs @@ -193,7 +193,7 @@ impl blockchain::DataSource for DataSource { Err(anyhow!(DYNAMIC_DATA_SOURCE_ERROR)) } - fn validate(&self) -> Vec { + fn validate(&self, _: &semver::Version) -> Vec { let mut errors = Vec::new(); if self.kind != COSMOS_KIND { @@ -555,7 +555,7 @@ fn duplicate_url_type(message: &str) -> Error { mod tests { use super::*; - use graph::blockchain::DataSource as _; + use graph::{blockchain::DataSource as _, data::subgraph::LATEST_VERSION}; #[test] fn test_event_handlers_origin_validation() { @@ -597,7 +597,7 @@ mod tests { ]; for (data_source, errors) in &cases { - let validation_errors = data_source.validate(); + let validation_errors = data_source.validate(&LATEST_VERSION); assert_eq!(errors.len(), validation_errors.len()); @@ -645,7 +645,7 @@ mod tests { ]; for (data_source, errors) in &cases { - let validation_errors = data_source.validate(); + let validation_errors = data_source.validate(&LATEST_VERSION); assert_eq!(errors.len(), validation_errors.len()); diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 20aaf5e9651..a16ccc341b6 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -268,7 +268,7 @@ impl blockchain::DataSource for DataSource { }) } - fn validate(&self) -> Vec { + fn validate(&self, _: &semver::Version) -> Vec { let mut errors = vec![]; if !ETHEREUM_KINDS.contains(&self.kind.as_str()) { diff --git a/chain/near/src/chain.rs b/chain/near/src/chain.rs index 135350a1deb..58e569515dd 100644 --- a/chain/near/src/chain.rs +++ b/chain/near/src/chain.rs @@ -565,6 +565,7 @@ mod test { use graph::{ blockchain::{block_stream::BlockWithTriggers, DataSource as _, TriggersAdapter as _}, + data::subgraph::LATEST_VERSION, prelude::{tokio, Link}, semver::Version, slog::{self, o, Logger}, @@ -587,7 +588,7 @@ mod test { #[test] fn validate_empty() { let ds = new_data_source(None, None); - let errs = ds.validate(); + let errs = ds.validate(LATEST_VERSION); assert_eq!(errs.len(), 1, "{:?}", ds); assert_eq!(errs[0].to_string(), "subgraph source address is required"); } @@ -595,7 +596,7 @@ mod test { #[test] fn validate_empty_account_none_partial() { let ds = new_data_source(None, Some(PartialAccounts::default())); - let errs = ds.validate(); + let errs = ds.validate(LATEST_VERSION); assert_eq!(errs.len(), 1, "{:?}", ds); assert_eq!(errs[0].to_string(), "subgraph source address is required"); } @@ -609,7 +610,7 @@ mod test { suffixes: vec!["x.near".to_string()], }), ); - let errs = ds.validate(); + let errs = ds.validate(LATEST_VERSION); assert_eq!(errs.len(), 0, "{:?}", ds); } @@ -623,7 +624,7 @@ mod test { }), ); let errs: Vec = ds - .validate() + .validate(LATEST_VERSION) .into_iter() .map(|err| err.to_string()) .collect(); @@ -644,7 +645,7 @@ mod test { #[test] fn validate_empty_partials() { let ds = new_data_source(Some("x.near".to_string()), None); - let errs = ds.validate(); + let errs = ds.validate(LATEST_VERSION); assert_eq!(errs.len(), 0, "{:?}", ds); } diff --git a/chain/near/src/data_source.rs b/chain/near/src/data_source.rs index 7b001098993..ea54c31d157 100644 --- a/chain/near/src/data_source.rs +++ b/chain/near/src/data_source.rs @@ -226,7 +226,7 @@ impl blockchain::DataSource for DataSource { todo!() } - fn validate(&self) -> Vec { + fn validate(&self, _: &semver::Version) -> Vec { let mut errors = Vec::new(); if self.kind != NEAR_KIND { diff --git a/chain/starknet/src/chain.rs b/chain/starknet/src/chain.rs index 060a502d80d..1d2a940eeec 100644 --- a/chain/starknet/src/chain.rs +++ b/chain/starknet/src/chain.rs @@ -430,7 +430,7 @@ impl TriggersAdapterTrait for TriggersAdapter { mod tests { use std::sync::Arc; - use graph::blockchain::DataSource as _; + use graph::{blockchain::DataSource as _, data::subgraph::LATEST_VERSION}; use crate::{ data_source::{ @@ -443,7 +443,7 @@ mod tests { fn validate_no_handler() { let ds = new_data_source(None); - let errs = ds.validate(); + let errs = ds.validate(LATEST_VERSION); assert_eq!(errs.len(), 1, "{:?}", ds); assert_eq!( errs[0].to_string(), @@ -458,7 +458,7 @@ mod tests { handler: "asdf".into(), }); - let errs = ds.validate(); + let errs = ds.validate(LATEST_VERSION); assert_eq!(errs.len(), 1, "{:?}", ds); assert_eq!( errs[0].to_string(), @@ -474,7 +474,7 @@ mod tests { event_selector: [2u8; 32].into(), }); - let errs = ds.validate(); + let errs = ds.validate(LATEST_VERSION); assert_eq!(errs.len(), 1, "{:?}", ds); assert_eq!(errs[0].to_string(), "subgraph source address is required"); } diff --git a/chain/starknet/src/data_source.rs b/chain/starknet/src/data_source.rs index ef8ccd3ad9b..8f168dc47c5 100644 --- a/chain/starknet/src/data_source.rs +++ b/chain/starknet/src/data_source.rs @@ -208,7 +208,7 @@ impl blockchain::DataSource for DataSource { todo!() } - fn validate(&self) -> Vec { + fn validate(&self, _: &semver::Version) -> Vec { let mut errors = Vec::new(); if self.kind != STARKNET_KIND { diff --git a/chain/substreams/src/data_source.rs b/chain/substreams/src/data_source.rs index ba7666fc9d3..4d3230cae6b 100644 --- a/chain/substreams/src/data_source.rs +++ b/chain/substreams/src/data_source.rs @@ -106,7 +106,7 @@ impl blockchain::DataSource for DataSource { unimplemented!("{}", DYNAMIC_DATA_SOURCE_ERROR) } - fn validate(&self) -> Vec { + fn validate(&self, _: &semver::Version) -> Vec { let mut errs = vec![]; if &self.kind != SUBSTREAMS_KIND { @@ -327,11 +327,14 @@ mod test { use graph::{ blockchain::{DataSource as _, UnresolvedDataSource as _}, components::link_resolver::LinkResolver, + data::subgraph::LATEST_VERSION, prelude::{async_trait, serde_yaml, JsonValueStream, Link}, slog::{o, Discard, Logger}, - substreams::module::{Kind, KindMap, KindStore}, substreams::{ - module::input::{Input, Params}, + module::{ + input::{Input, Params}, + Kind, KindMap, KindStore, + }, Module, Modules, Package, }, }; @@ -459,15 +462,19 @@ mod test { #[test] fn data_source_validation() { let mut ds = gen_data_source(); - assert_eq!(true, ds.validate().is_empty()); + assert_eq!(true, ds.validate(LATEST_VERSION).is_empty()); ds.network = None; - assert_eq!(true, ds.validate().is_empty()); + assert_eq!(true, ds.validate(LATEST_VERSION).is_empty()); ds.kind = "asdasd".into(); ds.name = "".into(); ds.mapping.kind = "asdasd".into(); - let errs: Vec = ds.validate().into_iter().map(|e| e.to_string()).collect(); + let errs: Vec = ds + .validate(LATEST_VERSION) + .into_iter() + .map(|e| e.to_string()) + .collect(); assert_eq!( errs, vec![ diff --git a/graph/src/blockchain/mock.rs b/graph/src/blockchain/mock.rs index 1319dd5f054..9ff7475e5a6 100644 --- a/graph/src/blockchain/mock.rs +++ b/graph/src/blockchain/mock.rs @@ -140,7 +140,7 @@ impl DataSource for MockDataSource { todo!() } - fn validate(&self) -> Vec { + fn validate(&self, _: &semver::Version) -> Vec { todo!() } } diff --git a/graph/src/blockchain/mod.rs b/graph/src/blockchain/mod.rs index c4ec226ad2f..665081d148b 100644 --- a/graph/src/blockchain/mod.rs +++ b/graph/src/blockchain/mod.rs @@ -300,7 +300,7 @@ pub trait DataSource: 'static + Sized + Send + Sync + Clone { fn as_stored_dynamic_data_source(&self) -> StoredDynamicDataSource; /// Used as part of manifest validation. If there are no errors, return an empty vector. - fn validate(&self) -> Vec; + fn validate(&self, spec_version: &semver::Version) -> Vec; fn has_expired(&self, block: BlockNumber) -> bool { self.end_block() diff --git a/graph/src/data/subgraph/mod.rs b/graph/src/data/subgraph/mod.rs index c69cef1fe26..7c377a8e6e2 100644 --- a/graph/src/data/subgraph/mod.rs +++ b/graph/src/data/subgraph/mod.rs @@ -691,7 +691,7 @@ impl UnvalidatedSubgraphManifest { } for ds in &self.0.data_sources { - errors.extend(ds.validate().into_iter().map(|e| { + errors.extend(ds.validate(&self.0.spec_version).into_iter().map(|e| { SubgraphManifestValidationError::DataSourceValidation(ds.name().to_owned(), e) })); } diff --git a/graph/src/data_source/mod.rs b/graph/src/data_source/mod.rs index 2c982146c16..6441a6f72ee 100644 --- a/graph/src/data_source/mod.rs +++ b/graph/src/data_source/mod.rs @@ -236,9 +236,9 @@ impl DataSource { } } - pub fn validate(&self) -> Vec { + pub fn validate(&self, spec_version: &semver::Version) -> Vec { match self { - Self::Onchain(ds) => ds.validate(), + Self::Onchain(ds) => ds.validate(spec_version), Self::Offchain(_) => vec![], } } diff --git a/store/test-store/tests/chain/ethereum/manifest.rs b/store/test-store/tests/chain/ethereum/manifest.rs index 2aca9d5f759..0d9452f1bd3 100644 --- a/store/test-store/tests/chain/ethereum/manifest.rs +++ b/store/test-store/tests/chain/ethereum/manifest.rs @@ -10,8 +10,8 @@ use graph::data::store::scalar::Bytes; use graph::data::store::Value; use graph::data::subgraph::schema::SubgraphError; use graph::data::subgraph::{ - Prune, SPEC_VERSION_0_0_4, SPEC_VERSION_0_0_7, SPEC_VERSION_0_0_8, SPEC_VERSION_0_0_9, - SPEC_VERSION_1_0_0, + Prune, LATEST_VERSION, SPEC_VERSION_0_0_4, SPEC_VERSION_0_0_7, SPEC_VERSION_0_0_8, + SPEC_VERSION_0_0_9, SPEC_VERSION_1_0_0, }; use graph::data_source::offchain::OffchainDataSourceKind; use graph::data_source::DataSourceTemplate; @@ -592,7 +592,7 @@ specVersion: 0.0.8 .collect::>(); let data_source = onchain_data_sources.get(0).unwrap(); - let validation_errors = data_source.validate(); + let validation_errors = data_source.validate(&LATEST_VERSION); let filter = data_source.mapping.block_handlers[0].filter.clone(); assert_eq!(0, validation_errors.len()); @@ -688,7 +688,7 @@ specVersion: 0.0.8 .collect::>(); let data_source = onchain_data_sources.get(0).unwrap(); - let validation_errors = data_source.validate(); + let validation_errors = data_source.validate(LATEST_VERSION); let filters = data_source .mapping .block_handlers @@ -753,7 +753,7 @@ specVersion: 0.0.8 .collect::>(); let data_source = onchain_data_sources.get(0).unwrap(); - let validation_errors = data_source.validate(); + let validation_errors = data_source.validate(LATEST_VERSION); let filters = data_source .mapping .block_handlers From b01c1aa5c43a692b30b4eae1113f28d7d5a312cb Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Tue, 5 Mar 2024 14:19:31 -0800 Subject: [PATCH 15/51] chain/ethereum: Factor function look up into MappingABI --- chain/ethereum/src/data_source.rs | 56 +++++++++++++++++-- chain/ethereum/src/runtime/runtime_adapter.rs | 54 ++++-------------- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index a16ccc341b6..95bc9950a7b 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -1096,12 +1096,6 @@ pub struct UnresolvedMappingABI { pub file: Link, } -#[derive(Clone, Debug, PartialEq)] -pub struct MappingABI { - pub name: String, - pub contract: Contract, -} - impl UnresolvedMappingABI { pub async fn resolve( self, @@ -1122,6 +1116,56 @@ impl UnresolvedMappingABI { } } +#[derive(Clone, Debug, PartialEq)] +pub struct MappingABI { + pub name: String, + pub contract: Contract, +} + +impl MappingABI { + pub fn function( + &self, + contract_name: &str, + name: &str, + signature: Option<&str>, + ) -> Result<&Function, Error> { + let contract = &self.contract; + let function = match signature { + // Behavior for apiVersion < 0.0.4: look up function by name; for overloaded + // functions this always picks the same overloaded variant, which is incorrect + // and may lead to encoding/decoding errors + None => contract.function(name).with_context(|| { + format!( + "Unknown function \"{}::{}\" called from WASM runtime", + contract_name, name + ) + })?, + + // Behavior for apiVersion >= 0.0.04: look up function by signature of + // the form `functionName(uint256,string) returns (bytes32,string)`; this + // correctly picks the correct variant of an overloaded function + Some(ref signature) => contract + .functions_by_name(name) + .with_context(|| { + format!( + "Unknown function \"{}::{}\" called from WASM runtime", + contract_name, name + ) + })? + .iter() + .find(|f| signature == &f.signature()) + .with_context(|| { + format!( + "Unknown function \"{}::{}\" with signature `{}` \ + called from WASM runtime", + contract_name, name, signature, + ) + })?, + }; + Ok(function) + } +} + #[derive(Clone, Debug, Hash, Eq, PartialEq, Deserialize)] pub struct MappingBlockHandler { pub handler: String, diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index 868079402e7..f9ffb93f585 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -203,7 +203,7 @@ fn eth_call( let start_time = Instant::now(); // Obtain the path to the contract ABI - let contract = abis + let abi = abis .iter() .find(|abi| abi.name == unresolved_call.contract_name) .with_context(|| { @@ -213,49 +213,15 @@ fn eth_call( unresolved_call.contract_name ) }) - .map_err(HostExportError::Deterministic)? - .contract - .clone(); - - let function = match unresolved_call.function_signature { - // Behavior for apiVersion < 0.0.4: look up function by name; for overloaded - // functions this always picks the same overloaded variant, which is incorrect - // and may lead to encoding/decoding errors - None => contract - .function(unresolved_call.function_name.as_str()) - .with_context(|| { - format!( - "Unknown function \"{}::{}\" called from WASM runtime", - unresolved_call.contract_name, unresolved_call.function_name - ) - }) - .map_err(HostExportError::Deterministic)?, - - // Behavior for apiVersion >= 0.0.04: look up function by signature of - // the form `functionName(uint256,string) returns (bytes32,string)`; this - // correctly picks the correct variant of an overloaded function - Some(ref function_signature) => contract - .functions_by_name(unresolved_call.function_name.as_str()) - .with_context(|| { - format!( - "Unknown function \"{}::{}\" called from WASM runtime", - unresolved_call.contract_name, unresolved_call.function_name - ) - }) - .map_err(HostExportError::Deterministic)? - .iter() - .find(|f| function_signature == &f.signature()) - .with_context(|| { - format!( - "Unknown function \"{}::{}\" with signature `{}` \ - called from WASM runtime", - unresolved_call.contract_name, - unresolved_call.function_name, - function_signature, - ) - }) - .map_err(HostExportError::Deterministic)?, - }; + .map_err(HostExportError::Deterministic)?; + + let function = abi + .function( + &unresolved_call.contract_name, + &unresolved_call.function_name, + unresolved_call.function_signature.as_deref(), + ) + .map_err(HostExportError::Deterministic)?; let call = EthereumContractCall { address: unresolved_call.contract_address, From dd9f6ece7552b3d7f508e2bd66a2fea069ca526a Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Thu, 29 Feb 2024 12:46:28 -0800 Subject: [PATCH 16/51] all: Allow declaring eth calls on ethereum event handlers --- chain/ethereum/src/data_source.rs | 211 +++++++++++++++++- graph/src/data/subgraph/api_version.rs | 5 +- .../tests/chain/ethereum/manifest.rs | 71 +++++- 3 files changed, 284 insertions(+), 3 deletions(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 95bc9950a7b..4062c46378e 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -3,13 +3,17 @@ use anyhow::{ensure, Context}; use graph::blockchain::TriggerWithHandler; use graph::components::store::StoredDynamicDataSource; use graph::components::subgraph::InstanceDSTemplateInfo; +use graph::data::value::Word; use graph::data_source::CausalityRegion; use graph::prelude::ethabi::ethereum_types::H160; use graph::prelude::ethabi::StateMutability; use graph::prelude::futures03::future::try_join; use graph::prelude::futures03::stream::FuturesOrdered; +use graph::prelude::regex::Regex; use graph::prelude::{Link, SubgraphManifestValidationError}; use graph::slog::{o, trace}; +use lazy_static::lazy_static; +use serde::de; use std::collections::HashSet; use std::num::NonZeroU32; use std::str::FromStr; @@ -30,6 +34,7 @@ use graph::{ use graph::data::subgraph::{ calls_host_fn, DataSourceContext, Source, MIN_SPEC_VERSION, SPEC_VERSION_0_0_8, + SPEC_VERSION_1_2_0, }; use crate::chain::Chain; @@ -268,7 +273,7 @@ impl blockchain::DataSource for DataSource { }) } - fn validate(&self, _: &semver::Version) -> Vec { + fn validate(&self, spec_version: &semver::Version) -> Vec { let mut errors = vec![]; if !ETHEREUM_KINDS.contains(&self.kind.as_str()) { @@ -347,6 +352,33 @@ impl blockchain::DataSource for DataSource { } } + if spec_version < &SPEC_VERSION_1_2_0 { + for handler in &self.mapping.event_handlers { + if !handler.calls.decls.is_empty() { + errors.push(anyhow!( + "handler {}: declaring eth calls on handlers is only supported for specVersion >= 1.2.0", handler.event + )); + break; + } + } + } + + for handler in &self.mapping.event_handlers { + for call in handler.calls.decls.as_ref() { + match self.mapping.find_abi(&call.expr.abi) { + // TODO: Handle overloaded functions by passing a signature + Ok(abi) => match abi.function(&call.expr.abi, &call.expr.func, None) { + Ok(_) => {} + Err(e) => { + errors.push(e); + } + }, + Err(e) => { + errors.push(e); + } + } + } + } errors } @@ -1210,6 +1242,8 @@ pub struct MappingEventHandler { pub handler: String, #[serde(default)] pub receipt: bool, + #[serde(default)] + pub calls: CallDecls, } impl MappingEventHandler { @@ -1237,3 +1271,178 @@ fn string_to_h256(s: &str) -> H256 { pub struct TemplateSource { pub abi: String, } + +/// Internal representation of declared calls. In the manifest that's +/// written as part of an event handler as +/// ```yaml +/// calls: +/// - myCall1: Contract[address].function(arg1, arg2, ...) +/// - .. +/// ``` +/// +/// The `address` and `arg` fields can be either `event.address` or +/// `event.params.`. Each entry under `calls` gets turned into a +/// `CallDcl` +#[derive(Clone, Debug, Default, Hash, Eq, PartialEq)] +pub struct CallDecls { + pub decls: Arc>, + readonly: (), +} + +impl CheapClone for CallDecls {} + +/// A single call declaration, like `myCall1: +/// Contract[address].function(arg1, arg2, ...)` +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct CallDecl { + /// A user-defined label + pub label: String, + /// The call expression + pub expr: CallExpr, + readonly: (), +} + +impl<'de> de::Deserialize<'de> for CallDecls { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let decls: std::collections::HashMap = + de::Deserialize::deserialize(deserializer)?; + let decls = decls + .into_iter() + .map(|(name, expr)| { + expr.parse::().map(|expr| CallDecl { + label: name, + expr, + readonly: (), + }) + }) + .collect::>() + .map(|decls| Arc::new(decls)) + .map_err(de::Error::custom)?; + Ok(CallDecls { + decls, + readonly: (), + }) + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub struct CallExpr { + pub abi: Word, + pub address: CallArg, + pub func: Word, + pub args: Vec, + readonly: (), +} + +/// Parse expressions of the form `Contract[address].function(arg1, arg2, +/// ...)` where the `address` and the args are either `event.address` or +/// `event.params.`. +/// +/// The parser is pretty awful as it generates error messages that aren't +/// very helpful. We should replace all this with a real parser, most likely +/// `combine` which is what `graphql_parser` uses +impl FromStr for CallExpr { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + lazy_static! { + static ref RE: Regex = Regex::new( + r"(?x) + (?P[a-zA-Z0-9_]+)\[ + (?P
[^]]+)\] + \. + (?P[a-zA-Z0-9_]+)\( + (?P[^)]*) + \)" + ) + .unwrap(); + } + let x = RE + .captures(s) + .ok_or_else(|| anyhow!("invalid call expression `{s}`"))?; + let abi = Word::from(x.name("abi").unwrap().as_str()); + let address = x.name("address").unwrap().as_str().parse()?; + let func = Word::from(x.name("func").unwrap().as_str()); + let args: Vec = x + .name("args") + .unwrap() + .as_str() + .split(',') + .filter(|s| !s.is_empty()) + .map(|s| s.trim().parse::()) + .collect::>()?; + Ok(CallExpr { + abi, + address, + func, + args, + readonly: (), + }) + } +} + +#[derive(Clone, Debug, Hash, Eq, PartialEq)] +pub enum CallArg { + Address, + Param(Word), +} + +impl FromStr for CallArg { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + fn invalid(s: &str) -> Result { + Err(anyhow!("invalid call argument `{}`", s)) + } + + let mut parts = s.split("."); + match parts.next() { + Some("event") => { /* ok */ } + Some(_) => return Err(anyhow!("call arguments must start with `event`")), + None => return Err(anyhow!("empty call argument")), + } + match parts.next() { + Some("address") => Ok(CallArg::Address), + Some("params") => match parts.next() { + Some(s) => Ok(CallArg::Param(Word::from(s))), + None => invalid(s), + }, + Some(s) => invalid(s), + None => invalid(s), + } + } +} + +#[test] +fn test_call_expr() { + let expr: CallExpr = "ERC20[event.address].balanceOf(event.params.token)" + .parse() + .unwrap(); + assert_eq!(expr.abi, "ERC20"); + assert_eq!(expr.address, CallArg::Address); + assert_eq!(expr.func, "balanceOf"); + assert_eq!(expr.args, vec![CallArg::Param("token".into())]); + + let expr: CallExpr = "Pool[event.params.pool].fees(event.params.token0, event.params.token1)" + .parse() + .unwrap(); + assert_eq!(expr.abi, "Pool"); + assert_eq!(expr.address, CallArg::Param("pool".into())); + assert_eq!(expr.func, "fees"); + assert_eq!( + expr.args, + vec![ + CallArg::Param("token0".into()), + CallArg::Param("token1".into()) + ] + ); + + let expr: CallExpr = "Pool[event.address].growth()".parse().unwrap(); + assert_eq!(expr.abi, "Pool"); + assert_eq!(expr.address, CallArg::Address); + assert_eq!(expr.func, "growth"); + assert_eq!(expr.args, vec![]); +} diff --git a/graph/src/data/subgraph/api_version.rs b/graph/src/data/subgraph/api_version.rs index 691fac3ed48..95271b83db7 100644 --- a/graph/src/data/subgraph/api_version.rs +++ b/graph/src/data/subgraph/api_version.rs @@ -51,8 +51,11 @@ pub const SPEC_VERSION_1_0_0: Version = Version::new(1, 0, 0); // Enables `id: Int8` pub const SPEC_VERSION_1_1_0: Version = Version::new(1, 1, 0); +// Enables eth call declarations +pub const SPEC_VERSION_1_2_0: Version = Version::new(1, 2, 0); + // The latest spec version available -pub const LATEST_VERSION: &Version = &SPEC_VERSION_1_1_0; +pub const LATEST_VERSION: &Version = &SPEC_VERSION_1_2_0; pub const MIN_SPEC_VERSION: Version = Version::new(0, 0, 2); diff --git a/store/test-store/tests/chain/ethereum/manifest.rs b/store/test-store/tests/chain/ethereum/manifest.rs index 0d9452f1bd3..4a8a2cca70c 100644 --- a/store/test-store/tests/chain/ethereum/manifest.rs +++ b/store/test-store/tests/chain/ethereum/manifest.rs @@ -11,7 +11,7 @@ use graph::data::store::Value; use graph::data::subgraph::schema::SubgraphError; use graph::data::subgraph::{ Prune, LATEST_VERSION, SPEC_VERSION_0_0_4, SPEC_VERSION_0_0_7, SPEC_VERSION_0_0_8, - SPEC_VERSION_0_0_9, SPEC_VERSION_1_0_0, + SPEC_VERSION_0_0_9, SPEC_VERSION_1_0_0, SPEC_VERSION_1_2_0, }; use graph::data_source::offchain::OffchainDataSourceKind; use graph::data_source::DataSourceTemplate; @@ -1302,3 +1302,72 @@ schema: assert!(manifest.features.contains(&SubgraphFeature::NonFatalErrors)) }); } + +#[test] +fn parses_eth_call_decls() { + const YAML: &str = " +specVersion: 1.2.0 +schema: + file: + /: /ipfs/Qmschema +features: + - ipfsOnEthereumContracts +dataSources: + - kind: ethereum/contract + name: Factory + network: mainnet + source: + abi: Factory + startBlock: 9562480 + mapping: + kind: ethereum/events + apiVersion: 0.0.4 + language: wasm/assemblyscript + entities: + - TestEntity + file: + /: /ipfs/Qmmapping + abis: + - name: Factory + file: + /: /ipfs/Qmabi + eventHandlers: + - event: Created(address) + handler: handleGet + calls: + fake1: Factory[event.address].get(event.params.address) + fake2: Factory[event.params.address].get(event.params.address) +"; + + test_store::run_test_sequentially(|store| async move { + let store = store.subgraph_store(); + let unvalidated: UnvalidatedSubgraphManifest = { + let mut resolver = TextResolver::default(); + let id = DeploymentHash::new("Qmmanifest").unwrap(); + resolver.add(id.as_str(), &YAML); + resolver.add("/ipfs/Qmabi", &ABI); + resolver.add("/ipfs/Qmschema", &GQL_SCHEMA); + resolver.add("/ipfs/Qmmapping", &MAPPING_WITH_IPFS_FUNC_WASM); + + let resolver: Arc = Arc::new(resolver); + + let raw = serde_yaml::from_str(YAML).unwrap(); + UnvalidatedSubgraphManifest::resolve( + id, + raw, + &resolver, + &LOGGER, + SPEC_VERSION_1_2_0.clone(), + ) + .await + .expect("Parsing simple manifest works") + }; + + let manifest = unvalidated.validate(store.clone(), true).await.unwrap(); + let ds = &manifest.data_sources[0].as_onchain().unwrap(); + // For more detailed tests of parsing CallDecls see the data_soure + // module in chain/ethereum + let decls = &ds.mapping.event_handlers[0].calls.decls; + assert_eq!(2, decls.len()); + }); +} From cbfbacbd4d2363236ba5333eb6f78970e144cce2 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Mon, 4 Mar 2024 14:29:12 -0800 Subject: [PATCH 17/51] all: Introduce a Blockchain::DecoderHook --- chain/arweave/src/chain.rs | 8 +++- chain/cosmos/src/chain.rs | 8 +++- chain/ethereum/src/chain.rs | 10 ++++- chain/near/src/chain.rs | 9 ++++- chain/starknet/src/chain.rs | 8 +++- chain/substreams/src/chain.rs | 9 ++++- core/src/subgraph/context/mod.rs | 4 +- core/src/subgraph/instance_manager.rs | 5 ++- core/src/subgraph/runner.rs | 29 ++++++++------ core/src/subgraph/trigger_processor.rs | 53 ++++++++++++++++---------- graph/src/blockchain/mock.rs | 8 +++- graph/src/blockchain/mod.rs | 34 ++++++++++++++++- 12 files changed, 140 insertions(+), 45 deletions(-) diff --git a/chain/arweave/src/chain.rs b/chain/arweave/src/chain.rs index 65019d0724b..8b9b25d8a6a 100644 --- a/chain/arweave/src/chain.rs +++ b/chain/arweave/src/chain.rs @@ -3,7 +3,7 @@ use graph::blockchain::client::ChainClient; use graph::blockchain::firehose_block_ingestor::FirehoseBlockIngestor; use graph::blockchain::{ BasicBlockchainBuilder, Block, BlockIngestor, BlockchainBuilder, BlockchainKind, - EmptyNodeCapabilities, NoopRuntimeAdapter, + EmptyNodeCapabilities, NoopDecoderHook, NoopRuntimeAdapter, }; use graph::cheap_clone::CheapClone; use graph::components::store::DeploymentCursorTracker; @@ -88,6 +88,8 @@ impl Blockchain for Chain { type NodeCapabilities = EmptyNodeCapabilities; + type DecoderHook = NoopDecoderHook; + fn triggers_adapter( &self, _loc: &DeploymentLocator, @@ -179,6 +181,10 @@ impl Blockchain for Chain { ); Ok(Box::new(ingestor)) } + + fn decoder_hook(&self) -> Self::DecoderHook { + NoopDecoderHook + } } pub struct TriggersAdapter {} diff --git a/chain/cosmos/src/chain.rs b/chain/cosmos/src/chain.rs index 6a493a144d4..9b7414fc9ce 100644 --- a/chain/cosmos/src/chain.rs +++ b/chain/cosmos/src/chain.rs @@ -1,5 +1,5 @@ use graph::blockchain::firehose_block_ingestor::FirehoseBlockIngestor; -use graph::blockchain::BlockIngestor; +use graph::blockchain::{BlockIngestor, NoopDecoderHook}; use graph::env::EnvVars; use graph::prelude::MetricsRegistry; use graph::substreams::Clock; @@ -82,6 +82,8 @@ impl Blockchain for Chain { type NodeCapabilities = EmptyNodeCapabilities; + type DecoderHook = NoopDecoderHook; + fn is_refetch_block_required(&self) -> bool { false } @@ -173,6 +175,10 @@ impl Blockchain for Chain { ); Ok(Box::new(ingestor)) } + + fn decoder_hook(&self) -> Self::DecoderHook { + NoopDecoderHook + } } pub struct TriggersAdapter {} diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 0754e7e2146..2c9b0f41b32 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -2,7 +2,9 @@ use anyhow::{anyhow, bail, Result}; use anyhow::{Context, Error}; use graph::blockchain::client::ChainClient; use graph::blockchain::firehose_block_ingestor::{FirehoseBlockIngestor, Transforms}; -use graph::blockchain::{BlockIngestor, BlockTime, BlockchainKind, TriggersAdapterSelector}; +use graph::blockchain::{ + BlockIngestor, BlockTime, BlockchainKind, NoopDecoderHook, TriggersAdapterSelector, +}; use graph::components::store::DeploymentCursorTracker; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::firehose::{FirehoseEndpoint, ForkStep}; @@ -354,6 +356,8 @@ impl Blockchain for Chain { type NodeCapabilities = crate::capabilities::NodeCapabilities; + type DecoderHook = NoopDecoderHook; + fn triggers_adapter( &self, loc: &DeploymentLocator, @@ -504,6 +508,10 @@ impl Blockchain for Chain { Ok(ingestor) } + + fn decoder_hook(&self) -> Self::DecoderHook { + NoopDecoderHook + } } /// This is used in `EthereumAdapter::triggers_in_block`, called when re-processing a block for diff --git a/chain/near/src/chain.rs b/chain/near/src/chain.rs index 58e569515dd..b94ee48c551 100644 --- a/chain/near/src/chain.rs +++ b/chain/near/src/chain.rs @@ -3,7 +3,8 @@ use graph::blockchain::client::ChainClient; use graph::blockchain::firehose_block_ingestor::FirehoseBlockIngestor; use graph::blockchain::substreams_block_stream::SubstreamsBlockStream; use graph::blockchain::{ - BasicBlockchainBuilder, BlockIngestor, BlockchainBuilder, BlockchainKind, NoopRuntimeAdapter, + BasicBlockchainBuilder, BlockIngestor, BlockchainBuilder, BlockchainKind, NoopDecoderHook, + NoopRuntimeAdapter, }; use graph::cheap_clone::CheapClone; use graph::components::store::DeploymentCursorTracker; @@ -209,6 +210,8 @@ impl Blockchain for Chain { type NodeCapabilities = EmptyNodeCapabilities; + type DecoderHook = NoopDecoderHook; + fn triggers_adapter( &self, _loc: &DeploymentLocator, @@ -301,6 +304,10 @@ impl Blockchain for Chain { ); Ok(Box::new(ingestor)) } + + fn decoder_hook(&self) -> Self::DecoderHook { + NoopDecoderHook + } } pub struct TriggersAdapter {} diff --git a/chain/starknet/src/chain.rs b/chain/starknet/src/chain.rs index 1d2a940eeec..d9831636ee4 100644 --- a/chain/starknet/src/chain.rs +++ b/chain/starknet/src/chain.rs @@ -10,7 +10,7 @@ use graph::{ firehose_block_ingestor::FirehoseBlockIngestor, firehose_block_stream::FirehoseBlockStream, BasicBlockchainBuilder, Block, BlockIngestor, BlockPtr, Blockchain, BlockchainBuilder, - BlockchainKind, EmptyNodeCapabilities, IngestorError, NoopRuntimeAdapter, + BlockchainKind, EmptyNodeCapabilities, IngestorError, NoopDecoderHook, NoopRuntimeAdapter, RuntimeAdapter as RuntimeAdapterTrait, }, cheap_clone::CheapClone, @@ -94,6 +94,8 @@ impl Blockchain for Chain { type NodeCapabilities = EmptyNodeCapabilities; + type DecoderHook = NoopDecoderHook; + fn triggers_adapter( &self, _log: &DeploymentLocator, @@ -171,6 +173,10 @@ impl Blockchain for Chain { ); Ok(Box::new(ingestor)) } + + fn decoder_hook(&self) -> Self::DecoderHook { + NoopDecoderHook + } } #[async_trait] diff --git a/chain/substreams/src/chain.rs b/chain/substreams/src/chain.rs index 1dad1764707..bb3c2229a98 100644 --- a/chain/substreams/src/chain.rs +++ b/chain/substreams/src/chain.rs @@ -3,7 +3,8 @@ use crate::{data_source::*, EntityChanges, TriggerData, TriggerFilter, TriggersA use anyhow::Error; use graph::blockchain::client::ChainClient; use graph::blockchain::{ - BasicBlockchainBuilder, BlockIngestor, BlockTime, EmptyNodeCapabilities, NoopRuntimeAdapter, + BasicBlockchainBuilder, BlockIngestor, BlockTime, EmptyNodeCapabilities, NoopDecoderHook, + NoopRuntimeAdapter, }; use graph::components::store::DeploymentCursorTracker; use graph::env::EnvVars; @@ -120,6 +121,8 @@ impl Blockchain for Chain { type NodeCapabilities = EmptyNodeCapabilities; + type DecoderHook = NoopDecoderHook; + fn triggers_adapter( &self, _log: &DeploymentLocator, @@ -195,6 +198,10 @@ impl Blockchain for Chain { self.metrics_registry.cheap_clone(), ))) } + + fn decoder_hook(&self) -> Self::DecoderHook { + NoopDecoderHook + } } impl blockchain::BlockchainBuilder for BasicBlockchainBuilder { diff --git a/core/src/subgraph/context/mod.rs b/core/src/subgraph/context/mod.rs index 8837361c4dc..42c6ef2b192 100644 --- a/core/src/subgraph/context/mod.rs +++ b/core/src/subgraph/context/mod.rs @@ -76,7 +76,7 @@ where pub offchain_monitor: OffchainMonitor, pub filter: Option, pub(crate) trigger_processor: Box>, - pub(crate) decoder: Box, + pub(crate) decoder: Box>, } impl> IndexingContext { @@ -88,7 +88,7 @@ impl> IndexingContext { instances: SubgraphKeepAlive, offchain_monitor: OffchainMonitor, trigger_processor: Box>, - decoder: Box, + decoder: Box>, ) -> Self { let instance = SubgraphInstance::new( manifest, diff --git a/core/src/subgraph/instance_manager.rs b/core/src/subgraph/instance_manager.rs index e709cb92e6a..83021bddd4b 100644 --- a/core/src/subgraph/instance_manager.rs +++ b/core/src/subgraph/instance_manager.rs @@ -2,6 +2,7 @@ use crate::polling_monitor::{ArweaveService, IpfsService}; use crate::subgraph::context::{IndexingContext, SubgraphKeepAlive}; use crate::subgraph::inputs::IndexingInputs; use crate::subgraph::loader::load_dynamic_data_sources; +use crate::subgraph::Decoder; use std::collections::BTreeSet; use crate::subgraph::runner::SubgraphRunner; @@ -408,6 +409,8 @@ impl SubgraphInstanceManager { let instrument = self.subgraph_store.instrument(&deployment)?; + let decoder = Box::new(Decoder::new(chain.as_ref())); + let inputs = IndexingInputs { deployment: deployment.clone(), features, @@ -426,8 +429,6 @@ impl SubgraphInstanceManager { instrument, }; - let decoder = Box::new(crate::subgraph::Decoder {}); - // Initialize the indexing context, including both static and dynamic data sources. // The order of inclusion is the order of processing when a same trigger matches // multiple data sources. diff --git a/core/src/subgraph/runner.rs b/core/src/subgraph/runner.rs index d46262bda93..489dd473861 100644 --- a/core/src/subgraph/runner.rs +++ b/core/src/subgraph/runner.rs @@ -312,13 +312,17 @@ where // Match and decode all triggers in the block let hosts_filter = |trigger: &TriggerData| self.ctx.instance.hosts_for_trigger(trigger); - let match_res = self.ctx.decoder.match_and_decode_many::( - &logger, - &block, - triggers.into_iter().map(TriggerData::Onchain), - hosts_filter, - &self.metrics.subgraph, - ); + let match_res = self + .ctx + .decoder + .match_and_decode_many( + &logger, + &block, + triggers.into_iter().map(TriggerData::Onchain), + hosts_filter, + &self.metrics.subgraph, + ) + .await; // Process events one after the other, passing in entity operations // collected previously to every new event being processed @@ -467,14 +471,17 @@ where // Process the triggers in each host in the same order the // corresponding data sources have been created. - let match_res: Result, _> = - self.ctx.decoder.match_and_decode_many::( + let match_res: Result, _> = self + .ctx + .decoder + .match_and_decode_many( &logger, &block, triggers.into_iter().map(TriggerData::Onchain), |_| Box::new(runtime_hosts.iter().map(Arc::as_ref)), &self.metrics.subgraph, - ); + ) + .await; let mut res = Ok(block_state); match match_res { @@ -1055,7 +1062,7 @@ where let trigger = TriggerData::Offchain(trigger); let process_res = { let hosts = self.ctx.instance.hosts_for_trigger(&trigger); - let triggers_res = self.ctx.decoder.match_and_decode::( + let triggers_res = self.ctx.decoder.match_and_decode( &self.logger, block, trigger, diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index d611995cbde..1025d4b2880 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use graph::blockchain::{Block, Blockchain}; +use graph::blockchain::{Block, Blockchain, DecoderHook as _}; use graph::cheap_clone::CheapClone; use graph::components::store::SubgraphFork; use graph::components::subgraph::{MappingError, SharedProofOfIndexing}; @@ -10,6 +10,7 @@ use graph::prelude::{ BlockState, RuntimeHost, RuntimeHostBuilder, SubgraphInstanceMetrics, TriggerProcessor, }; use graph::slog::Logger; +use std::marker::PhantomData; use std::sync::Arc; pub struct SubgraphTriggerProcessor {} @@ -92,21 +93,37 @@ where /// stream) and turning them into `HostedTrigger`s that are ready to run. /// /// The output triggers will be run in the order in which they are returned. -pub struct Decoder {} +pub struct Decoder +where + C: Blockchain, + T: RuntimeHostBuilder, +{ + hook: C::DecoderHook, + _builder: PhantomData, +} -impl Decoder { - fn match_and_decode_inner<'a, C, T>( +impl Decoder +where + C: Blockchain, + T: RuntimeHostBuilder, +{ + pub fn new(chain: &C) -> Self { + Decoder { + hook: chain.decoder_hook(), + _builder: PhantomData, + } + } +} + +impl> Decoder { + fn match_and_decode_inner<'a>( &'a self, logger: &Logger, block: &Arc, trigger: &TriggerData, hosts: Box + Send + 'a>, subgraph_metrics: &Arc, - ) -> Result>, MappingError> - where - C: Blockchain, - T: RuntimeHostBuilder, - { + ) -> Result>, MappingError> { let mut host_mapping = vec![]; { @@ -130,19 +147,15 @@ impl Decoder { Ok(host_mapping) } - pub(crate) fn match_and_decode<'a, C, T>( + pub(crate) fn match_and_decode<'a>( &'a self, logger: &Logger, block: &Arc, trigger: TriggerData, hosts: Box + Send + 'a>, subgraph_metrics: &Arc, - ) -> Result, MappingError> - where - C: Blockchain, - T: RuntimeHostBuilder, - { - self.match_and_decode_inner::(logger, block, &trigger, hosts, subgraph_metrics) + ) -> Result, MappingError> { + self.match_and_decode_inner(logger, block, &trigger, hosts, subgraph_metrics) .map_err(|e| e.add_trigger_context(&trigger)) .map(|hosted_triggers| RunnableTriggers { trigger, @@ -150,7 +163,7 @@ impl Decoder { }) } - pub(crate) fn match_and_decode_many<'a, C, T, F>( + pub(crate) async fn match_and_decode_many<'a, F>( &'a self, logger: &Logger, block: &Arc, @@ -159,18 +172,16 @@ impl Decoder { subgraph_metrics: &Arc, ) -> Result>, MappingError> where - C: Blockchain, - T: RuntimeHostBuilder, F: Fn(&TriggerData) -> Box + Send + 'a>, { let mut runnables = vec![]; for trigger in triggers { let hosts = hosts_filter(&trigger); - match self.match_and_decode::(logger, block, trigger, hosts, subgraph_metrics) { + match self.match_and_decode(logger, block, trigger, hosts, subgraph_metrics) { Ok(runnable_triggers) => runnables.push(runnable_triggers), Err(e) => return Err(e), } } - Ok(runnables) + self.hook.after_decode(runnables).await } } diff --git a/graph/src/blockchain/mock.rs b/graph/src/blockchain/mock.rs index 9ff7475e5a6..abfb67658b7 100644 --- a/graph/src/blockchain/mock.rs +++ b/graph/src/blockchain/mock.rs @@ -16,7 +16,7 @@ use super::{ block_stream::{self, BlockStream, FirehoseCursor}, client::ChainClient, BlockIngestor, BlockTime, EmptyNodeCapabilities, HostFn, IngestorError, MappingTriggerTrait, - TriggerWithHandler, + NoopDecoderHook, TriggerWithHandler, }; use super::{ @@ -325,6 +325,8 @@ impl Blockchain for MockBlockchain { type NodeCapabilities = EmptyNodeCapabilities; + type DecoderHook = NoopDecoderHook; + fn triggers_adapter( &self, _loc: &crate::components::store::DeploymentLocator, @@ -380,4 +382,8 @@ impl Blockchain for MockBlockchain { fn block_ingestor(&self) -> anyhow::Result> { todo!() } + + fn decoder_hook(&self) -> Self::DecoderHook { + todo!() + } } diff --git a/graph/src/blockchain/mod.rs b/graph/src/blockchain/mod.rs index 665081d148b..15215df1735 100644 --- a/graph/src/blockchain/mod.rs +++ b/graph/src/blockchain/mod.rs @@ -19,8 +19,8 @@ use crate::{ cheap_clone::CheapClone, components::{ store::{DeploymentCursorTracker, DeploymentLocator, StoredDynamicDataSource}, - subgraph::HostMetrics, - subgraph::InstanceDSTemplateInfo, + subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError}, + trigger_processor::RunnableTriggers, }, data::subgraph::{UnifiedMappingApiVersion, MIN_SPEC_VERSION}, data_source::{self, DataSourceTemplateInfo}, @@ -168,6 +168,11 @@ pub trait Blockchain: Debug + Sized + Send + Sync + Unpin + 'static { type NodeCapabilities: NodeCapabilities + std::fmt::Display; + /// A callback that is called after the triggers have been decoded and + /// gets an opportunity to post-process triggers before they are run on + /// hosts + type DecoderHook: DecoderHook + Sync + Send; + fn triggers_adapter( &self, log: &DeploymentLocator, @@ -205,6 +210,8 @@ pub trait Blockchain: Debug + Sized + Send + Sync + Unpin + 'static { fn chain_client(&self) -> Arc>; fn block_ingestor(&self) -> anyhow::Result>; + + fn decoder_hook(&self) -> Self::DecoderHook; } #[derive(Error, Debug)] @@ -369,6 +376,29 @@ pub trait MappingTriggerTrait { fn error_context(&self) -> String; } +/// A callback that is called after the triggers have been decoded. +#[async_trait] +pub trait DecoderHook { + async fn after_decode<'a>( + &self, + triggers: Vec>, + ) -> Result>, MappingError>; +} + +/// A decoder hook that does nothing and just returns the triggers that were +/// passed in +pub struct NoopDecoderHook; + +#[async_trait] +impl DecoderHook for NoopDecoderHook { + async fn after_decode<'a>( + &self, + triggers: Vec>, + ) -> Result>, MappingError> { + Ok(triggers) + } +} + pub struct HostFnCtx<'a> { pub logger: Logger, pub block_ptr: BlockPtr, From a293fd8c6817fe5aa2f3c55138d28ca7c094607d Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Mon, 4 Mar 2024 17:23:41 -0800 Subject: [PATCH 18/51] chain/ethereum: Add a buffered call cache The new cache keeps call results in memory for the duration of a block --- chain/ethereum/src/buffered_call_cache.rs | 92 +++++++++++++++++++++++ chain/ethereum/src/lib.rs | 3 + node/src/main.rs | 10 ++- node/src/manager/commands/run.rs | 6 +- 4 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 chain/ethereum/src/buffered_call_cache.rs diff --git a/chain/ethereum/src/buffered_call_cache.rs b/chain/ethereum/src/buffered_call_cache.rs new file mode 100644 index 00000000000..b98d6d65453 --- /dev/null +++ b/chain/ethereum/src/buffered_call_cache.rs @@ -0,0 +1,92 @@ +use std::{ + collections::HashMap, + sync::{Arc, Mutex}, +}; + +use graph::{ + components::store::EthereumCallCache, + prelude::{ethabi, BlockPtr, CachedEthereumCall}, +}; + +/// A wrapper around an Ethereum call cache that buffers call results in +/// memory for the duration of a block. If `get_call` or `set_call` are +/// called with a different block pointer than the one used in the previous +/// call, the buffer is cleared. +pub struct BufferedCallCache { + call_cache: Arc, + buffer: Arc), Vec>>>, + block: Arc>>, +} + +impl BufferedCallCache { + pub fn new(call_cache: Arc) -> Self { + Self { + call_cache, + buffer: Arc::new(Mutex::new(HashMap::new())), + block: Arc::new(Mutex::new(None)), + } + } + + fn check_block(&self, block: &BlockPtr) { + let mut self_block = self.block.lock().unwrap(); + if self_block.as_ref() != Some(block) { + *self_block = Some(block.clone()); + self.buffer.lock().unwrap().clear(); + } + } +} + +impl EthereumCallCache for BufferedCallCache { + fn get_call( + &self, + contract_address: ethabi::Address, + encoded_call: &[u8], + block: BlockPtr, + ) -> Result>, graph::prelude::Error> { + self.check_block(&block); + + { + let buffer = self.buffer.lock().unwrap(); + if let Some(result) = buffer.get(&(contract_address, encoded_call.to_vec())) { + return Ok(Some(result.clone())); + } + } + + let result = self + .call_cache + .get_call(contract_address, encoded_call, block)?; + + if let Some(result) = &result { + let mut buffer = self.buffer.lock().unwrap(); + buffer.insert((contract_address, encoded_call.to_vec()), result.clone()); + } + Ok(result) + } + + fn get_calls_in_block( + &self, + block: BlockPtr, + ) -> Result, graph::prelude::Error> { + self.call_cache.get_calls_in_block(block) + } + + fn set_call( + &self, + contract_address: ethabi::Address, + encoded_call: &[u8], + block: BlockPtr, + return_value: &[u8], + ) -> Result<(), graph::prelude::Error> { + self.check_block(&block); + + self.call_cache + .set_call(contract_address, encoded_call, block, return_value)?; + + let mut buffer = self.buffer.lock().unwrap(); + buffer.insert( + (contract_address, encoded_call.to_vec()), + return_value.to_vec(), + ); + Ok(()) + } +} diff --git a/chain/ethereum/src/lib.rs b/chain/ethereum/src/lib.rs index cc9c24e79a4..0eb122246a5 100644 --- a/chain/ethereum/src/lib.rs +++ b/chain/ethereum/src/lib.rs @@ -1,4 +1,5 @@ mod adapter; +mod buffered_call_cache; mod capabilities; pub mod codec; mod data_source; @@ -14,6 +15,8 @@ pub use self::runtime::RuntimeAdapter; pub use self::transport::Transport; pub use env::ENV_VARS; +pub use buffered_call_cache::BufferedCallCache; + // ETHDEP: These concrete types should probably not be exposed. pub use data_source::{ BlockHandlerFilter, DataSource, DataSourceTemplate, Mapping, MappingABI, TemplateSource, diff --git a/node/src/main.rs b/node/src/main.rs index 8fbac6f3a84..c1cf0f9a320 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -867,9 +867,15 @@ fn ethereum_networks_as_chains( chain_store.clone(), ); + // Use a call cache that buffers in memory. It is important that + // the `RuntimeAdapter` and the `Chain` use the same call cache + // as that is used to pass the results of declared calls into + // mappings. + let call_cache = Arc::new(ethereum::BufferedCallCache::new(chain_store.cheap_clone())); + let runtime_adapter = Arc::new(RuntimeAdapter { eth_adapters: Arc::new(eth_adapters.clone()), - call_cache: chain_store.cheap_clone(), + call_cache: call_cache.cheap_clone(), chain_identifier: Arc::new(chain_store.chain_identifier.clone()), }); @@ -880,7 +886,7 @@ fn ethereum_networks_as_chains( node_id.clone(), registry.clone(), chain_store.cheap_clone(), - chain_store, + call_cache, client, chain_head_update_listener.clone(), Arc::new(EthereumStreamBuilder {}), diff --git a/node/src/manager/commands/run.rs b/node/src/manager/commands/run.rs index 960b01d62b3..636abed8ec8 100644 --- a/node/src/manager/commands/run.rs +++ b/node/src/manager/commands/run.rs @@ -147,6 +147,8 @@ pub async fn run( let client = Arc::new(ChainClient::new(firehose_endpoints, eth_adapters)); + let call_cache = Arc::new(ethereum::BufferedCallCache::new(chain_store.cheap_clone())); + let chain_config = config.chains.chains.get(&network_name).unwrap(); let chain = ethereum::Chain::new( logger_factory.clone(), @@ -154,7 +156,7 @@ pub async fn run( node_id.clone(), metrics_registry.clone(), chain_store.cheap_clone(), - chain_store.cheap_clone(), + call_cache.cheap_clone(), client.clone(), chain_head_update_listener, Arc::new(EthereumStreamBuilder {}), @@ -166,7 +168,7 @@ pub async fn run( chain_store.cheap_clone(), )), Arc::new(EthereumRuntimeAdapter { - call_cache: chain_store.cheap_clone(), + call_cache, eth_adapters: Arc::new(eth_adapters2), chain_identifier: Arc::new(chain_store.chain_identifier.clone()), }), From a502528e233251bbf8b6b7d1bd9ff469662f76d4 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Tue, 5 Mar 2024 15:23:53 -0800 Subject: [PATCH 19/51] graph, runtime: Expose the HostMetrics of a RuntimeHost --- graph/src/components/subgraph/host.rs | 3 +++ runtime/wasm/src/host.rs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/graph/src/components/subgraph/host.rs b/graph/src/components/subgraph/host.rs index 4390ce87c68..e7b9c4b8a74 100644 --- a/graph/src/components/subgraph/host.rs +++ b/graph/src/components/subgraph/host.rs @@ -105,6 +105,9 @@ pub trait RuntimeHost: Send + Sync + 'static { /// Convenience function to avoid leaking internal representation of /// mutable number. Calling this on OnChain Datasources is a noop. fn set_done_at(&self, block: Option); + + /// Return a metrics object for this host. + fn host_metrics(&self) -> Arc; } pub struct HostMetrics { diff --git a/runtime/wasm/src/host.rs b/runtime/wasm/src/host.rs index 661e3a65cec..5029025b645 100644 --- a/runtime/wasm/src/host.rs +++ b/runtime/wasm/src/host.rs @@ -373,6 +373,10 @@ impl RuntimeHostTrait for RuntimeHost { DataSource::Offchain(ds) => ds.set_done_at(block), } } + + fn host_metrics(&self) -> Arc { + self.metrics.cheap_clone() + } } impl PartialEq for RuntimeHost { From cf94cf9c3e534d6ce11e3bea5823a821dc97d6d7 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Mon, 4 Mar 2024 16:14:50 -0800 Subject: [PATCH 20/51] all: Add a decoder hook for ethereum The hook makes all declared eth calls before triggers are run, but does them in sequence --- chain/ethereum/src/chain.rs | 14 +- chain/ethereum/src/data_source.rs | 255 ++++++++++++++++++++++++- chain/ethereum/src/trigger.rs | 4 + core/src/subgraph/trigger_processor.rs | 4 +- graph/src/blockchain/mod.rs | 4 + graph/src/data_source/mod.rs | 7 + node/src/main.rs | 1 + node/src/manager/commands/run.rs | 3 +- tests/src/fixture/ethereum.rs | 4 + 9 files changed, 284 insertions(+), 12 deletions(-) diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index 2c9b0f41b32..a977b26330e 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -2,9 +2,7 @@ use anyhow::{anyhow, bail, Result}; use anyhow::{Context, Error}; use graph::blockchain::client::ChainClient; use graph::blockchain::firehose_block_ingestor::{FirehoseBlockIngestor, Transforms}; -use graph::blockchain::{ - BlockIngestor, BlockTime, BlockchainKind, NoopDecoderHook, TriggersAdapterSelector, -}; +use graph::blockchain::{BlockIngestor, BlockTime, BlockchainKind, TriggersAdapterSelector}; use graph::components::store::DeploymentCursorTracker; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::firehose::{FirehoseEndpoint, ForkStep}; @@ -269,6 +267,7 @@ pub struct Chain { block_refetcher: Arc>, adapter_selector: Arc>, runtime_adapter: Arc>, + eth_adapters: Arc, } impl std::fmt::Debug for Chain { @@ -292,6 +291,7 @@ impl Chain { block_refetcher: Arc>, adapter_selector: Arc>, runtime_adapter: Arc>, + eth_adapters: Arc, reorg_threshold: BlockNumber, polling_ingestor_interval: Duration, is_ingestible: bool, @@ -309,6 +309,7 @@ impl Chain { block_refetcher, adapter_selector, runtime_adapter, + eth_adapters, reorg_threshold, is_ingestible, polling_ingestor_interval, @@ -356,7 +357,7 @@ impl Blockchain for Chain { type NodeCapabilities = crate::capabilities::NodeCapabilities; - type DecoderHook = NoopDecoderHook; + type DecoderHook = crate::data_source::DecoderHook; fn triggers_adapter( &self, @@ -510,7 +511,10 @@ impl Blockchain for Chain { } fn decoder_hook(&self) -> Self::DecoderHook { - NoopDecoderHook + crate::data_source::DecoderHook::new( + self.eth_adapters.cheap_clone(), + self.call_cache.cheap_clone(), + ) } } diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 4062c46378e..eda0e6944d0 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -1,23 +1,26 @@ use anyhow::{anyhow, Error}; use anyhow::{ensure, Context}; -use graph::blockchain::TriggerWithHandler; -use graph::components::store::StoredDynamicDataSource; -use graph::components::subgraph::InstanceDSTemplateInfo; +use graph::blockchain::{BlockPtr, TriggerWithHandler}; +use graph::components::store::{EthereumCallCache, StoredDynamicDataSource}; +use graph::components::subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError}; +use graph::components::trigger_processor::RunnableTriggers; use graph::data::value::Word; use graph::data_source::CausalityRegion; use graph::prelude::ethabi::ethereum_types::H160; -use graph::prelude::ethabi::StateMutability; +use graph::prelude::ethabi::{StateMutability, Token}; use graph::prelude::futures03::future::try_join; use graph::prelude::futures03::stream::FuturesOrdered; use graph::prelude::regex::Regex; +use graph::prelude::Future01CompatExt; use graph::prelude::{Link, SubgraphManifestValidationError}; -use graph::slog::{o, trace}; +use graph::slog::{info, o, trace}; use lazy_static::lazy_static; use serde::de; use std::collections::HashSet; use std::num::NonZeroU32; use std::str::FromStr; use std::sync::Arc; +use std::time::Instant; use tiny_keccak::{keccak256, Keccak}; use graph::{ @@ -37,8 +40,11 @@ use graph::data::subgraph::{ SPEC_VERSION_1_2_0, }; +use crate::adapter::EthereumAdapter as _; use crate::chain::Chain; +use crate::network::EthereumNetworkAdapters; use crate::trigger::{EthereumBlockTriggerType, EthereumTrigger, MappingTrigger}; +use crate::{EthereumContractCall, EthereumContractCallError, NodeCapabilities}; // The recommended kind is `ethereum`, `ethereum/contract` is accepted for backwards compatibility. const ETHEREUM_KINDS: &[&str] = &["ethereum/contract", "ethereum"]; @@ -773,6 +779,7 @@ impl DataSource { "transaction" => format!("{}", &transaction.hash), }); let handler = event_handler.handler.clone(); + let calls = DeclaredCall::new(&self.mapping, &event_handler, &log, ¶ms)?; Ok(Some(TriggerWithHandler::::new_with_logging_extras( MappingTrigger::Log { block: block.cheap_clone(), @@ -780,6 +787,7 @@ impl DataSource { log, params, receipt: receipt.map(|r| r.cheap_clone()), + calls, }, handler, block.block_ptr(), @@ -902,6 +910,205 @@ impl DataSource { } } +#[derive(Clone, Debug)] +pub struct DeclaredCall { + contract_name: String, + address: Address, + function: Function, + args: Vec, +} + +impl DeclaredCall { + fn new( + mapping: &Mapping, + handler: &MappingEventHandler, + log: &Log, + params: &[LogParam], + ) -> Result, anyhow::Error> { + let mut calls = Vec::new(); + for decl in handler.calls.decls.iter() { + let contract_name = decl.expr.abi.to_string(); + let function_name = decl.expr.func.as_str(); + // Obtain the path to the contract ABI + let abi = mapping.find_abi(&contract_name)?; + // TODO: Handle overloaded functions + let function = { + // Behavior for apiVersion < 0.0.4: look up function by name; for overloaded + // functions this always picks the same overloaded variant, which is incorrect + // and may lead to encoding/decoding errors + abi.contract.function(function_name).with_context(|| { + format!( + "Unknown function \"{}::{}\" called from WASM runtime", + contract_name, function_name + ) + })? + }; + + let address = decl.address(log, params)?; + let args = decl.args(log, params)?; + + let call = DeclaredCall { + contract_name, + address, + function: function.clone(), + args, + }; + calls.push(call); + } + + Ok(calls) + } + + fn as_eth_call(self, block_ptr: BlockPtr) -> (EthereumContractCall, String) { + ( + EthereumContractCall { + address: self.address, + block_ptr, + function: self.function, + args: self.args, + gas: None, + }, + self.contract_name, + ) + } +} + +pub struct DecoderHook { + eth_adapters: Arc, + call_cache: Arc, +} + +impl DecoderHook { + pub fn new( + eth_adapters: Arc, + call_cache: Arc, + ) -> Self { + Self { + eth_adapters, + call_cache, + } + } +} + +impl DecoderHook { + async fn eth_call( + &self, + logger: &Logger, + block_ptr: &BlockPtr, + metrics: Arc, + call: DeclaredCall, + ) -> Result { + let start = Instant::now(); + let function_name = call.function.name.clone(); + let address = call.address; + let (eth_call, contract_name) = call.as_eth_call(block_ptr.clone()); + let eth_adapter = self.eth_adapters.call_or_cheapest(Some(&NodeCapabilities { + archive: true, + traces: false, + }))?; + + let result = eth_adapter + .contract_call(logger, eth_call, self.call_cache.cheap_clone()) + .compat() + .await; + + let elapsed = start.elapsed(); + + metrics.observe_eth_call_execution_time( + elapsed.as_secs_f64(), + &contract_name, + &function_name, + ); + + // This error analysis is very much modeled on the one in + // `crate::runtime_adapter::eth_call`; it would be better to + // make them the same but there are subtle differences (return + // type, error type) + match result { + Ok(_) => Ok(0), + Err(EthereumContractCallError::Revert(reason)) => { + info!(logger, "Declared contract call reverted"; + "reason" => reason, + "contract" => format!("0x{:x}", address), + "call" => format!("{}.{}", contract_name, function_name)); + Ok(1) + } + + // Any error reported by the Ethereum node could be due to the block no longer being on + // the main chain. This is very unespecific but we don't want to risk failing a + // subgraph due to a transient error such as a reorg. + Err(EthereumContractCallError::Web3Error(e)) => Err(MappingError::PossibleReorg(anyhow::anyhow!( + "Ethereum node returned an error when calling function \"{}\" of contract \"{}\": {}", + function_name, + contract_name, + e + ))), + + // Also retry on timeouts. + Err(EthereumContractCallError::Timeout) => Err(MappingError::PossibleReorg(anyhow::anyhow!( + "Ethereum node did not respond when calling function \"{}\" of contract \"{}\"", + function_name, + contract_name, + ))), + + Err(e) => Err(MappingError::Unknown(anyhow::anyhow!( + "Failed to call function \"{}\" of contract \"{}\": {}", + function_name, + contract_name, + e + ))), + + } + } +} + +#[async_trait] +impl blockchain::DecoderHook for DecoderHook { + async fn after_decode<'a>( + &self, + logger: &Logger, + block_ptr: &BlockPtr, + runnables: Vec>, + ) -> Result>, MappingError> { + let start = Instant::now(); + let calls: Vec<_> = runnables + .iter() + .map(|r| &r.hosted_triggers) + .flatten() + .filter_map(|trigger| { + trigger + .mapping_trigger + .trigger + .as_onchain() + .map(|t| (trigger.host.host_metrics(), t)) + }) + .filter_map(|(metrics, trigger)| match trigger { + MappingTrigger::Log { calls, .. } => Some( + calls + .clone() + .into_iter() + .map(move |call| (metrics.cheap_clone(), call)), + ), + MappingTrigger::Block { .. } | MappingTrigger::Call { .. } => None, + }) + .flatten() + .collect(); + + let calls_count = calls.len(); + let mut fail_count: usize = 0; + for (metrics, call) in calls { + fail_count += self.eth_call(logger, block_ptr, metrics, call).await?; + } + + // TODO: Remove this logging before merging + if calls_count > 0 { + info!(logger, "After decode hook"; "runnables" => runnables.len(), "calls_count" => calls_count, "fail_count" => fail_count, "calls_ms" => start.elapsed().as_millis()); + } + + Ok(runnables) + } +} + #[derive(Clone, Debug, Eq, PartialEq, Deserialize)] pub struct UnresolvedDataSource { pub kind: String, @@ -1301,6 +1508,44 @@ pub struct CallDecl { pub expr: CallExpr, readonly: (), } +impl CallDecl { + fn address(&self, log: &Log, params: &[LogParam]) -> Result { + let address = match &self.expr.address { + CallArg::Address => log.address, + CallArg::Param(name) => { + let value = params + .iter() + .find(|param| ¶m.name == name.as_str()) + .ok_or_else(|| anyhow!("unknown param {name}"))? + .value + .clone(); + value + .into_address() + .ok_or_else(|| anyhow!("param {name} is not an address"))? + } + }; + Ok(address) + } + + fn args(&self, log: &Log, params: &[LogParam]) -> Result, Error> { + self.expr + .args + .iter() + .map(|arg| match arg { + CallArg::Address => Ok(Token::Address(log.address)), + CallArg::Param(name) => { + let value = params + .iter() + .find(|param| ¶m.name == name.as_str()) + .ok_or_else(|| anyhow!("unknown param {name}"))? + .value + .clone(); + Ok(value) + } + }) + .collect() + } +} impl<'de> de::Deserialize<'de> for CallDecls { fn deserialize(deserializer: D) -> Result diff --git a/chain/ethereum/src/trigger.rs b/chain/ethereum/src/trigger.rs index a57f4c8410f..128ed8d3e98 100644 --- a/chain/ethereum/src/trigger.rs +++ b/chain/ethereum/src/trigger.rs @@ -28,6 +28,7 @@ use graph_runtime_wasm::module::ToAscPtr; use std::ops::Deref; use std::{cmp::Ordering, sync::Arc}; +use crate::data_source::DeclaredCall; use crate::runtime::abi::AscEthereumBlock; use crate::runtime::abi::AscEthereumBlock_0_0_6; use crate::runtime::abi::AscEthereumCall; @@ -48,6 +49,7 @@ pub enum MappingTrigger { log: Arc, params: Vec, receipt: Option>, + calls: Vec, }, Call { block: Arc, @@ -102,6 +104,7 @@ impl std::fmt::Debug for MappingTrigger { log, params, receipt: _, + calls: _, } => MappingTriggerWithoutBlock::Log { _transaction: transaction.cheap_clone(), _log: log.cheap_clone(), @@ -139,6 +142,7 @@ impl ToAscPtr for MappingTrigger { log, params, receipt, + calls: _, } => { let api_version = heap.api_version(); let ethereum_event_data = EthereumEventData { diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index 1025d4b2880..ac072e1faed 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -182,6 +182,8 @@ impl> Decoder { Err(e) => return Err(e), } } - self.hook.after_decode(runnables).await + self.hook + .after_decode(logger, &block.ptr(), runnables) + .await } } diff --git a/graph/src/blockchain/mod.rs b/graph/src/blockchain/mod.rs index 15215df1735..298ef0741fc 100644 --- a/graph/src/blockchain/mod.rs +++ b/graph/src/blockchain/mod.rs @@ -381,6 +381,8 @@ pub trait MappingTriggerTrait { pub trait DecoderHook { async fn after_decode<'a>( &self, + logger: &Logger, + block_ptr: &BlockPtr, triggers: Vec>, ) -> Result>, MappingError>; } @@ -393,6 +395,8 @@ pub struct NoopDecoderHook; impl DecoderHook for NoopDecoderHook { async fn after_decode<'a>( &self, + _: &Logger, + _: &BlockPtr, triggers: Vec>, ) -> Result>, MappingError> { Ok(triggers) diff --git a/graph/src/data_source/mod.rs b/graph/src/data_source/mod.rs index 6441a6f72ee..27e49927d02 100644 --- a/graph/src/data_source/mod.rs +++ b/graph/src/data_source/mod.rs @@ -492,6 +492,13 @@ impl MappingTrigger { Self::Offchain(_) => None, // TODO: Add error context for offchain triggers } } + + pub fn as_onchain(&self) -> Option<&C::MappingTrigger> { + match self { + Self::Onchain(trigger) => Some(trigger), + Self::Offchain(_) => None, + } + } } macro_rules! clone_data_source { diff --git a/node/src/main.rs b/node/src/main.rs index c1cf0f9a320..d37a8190930 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -893,6 +893,7 @@ fn ethereum_networks_as_chains( Arc::new(EthereumBlockRefetcher {}), Arc::new(adapter_selector), runtime_adapter, + Arc::new(eth_adapters.clone()), ENV_VARS.reorg_threshold, chain_config.polling_interval, is_ingestible, diff --git a/node/src/manager/commands/run.rs b/node/src/manager/commands/run.rs index 636abed8ec8..bce87d4449d 100644 --- a/node/src/manager/commands/run.rs +++ b/node/src/manager/commands/run.rs @@ -125,7 +125,7 @@ pub async fn run( }; let eth_adapters2 = eth_adapters.clone(); - + let eth_adapters3 = eth_adapters.clone(); let (_, ethereum_idents) = connect_ethereum_networks(&logger, eth_networks).await?; // let (near_networks, near_idents) = connect_firehose_networks::( // &logger, @@ -172,6 +172,7 @@ pub async fn run( eth_adapters: Arc::new(eth_adapters2), chain_identifier: Arc::new(chain_store.chain_identifier.clone()), }), + Arc::new(eth_adapters3), graph::env::ENV_VARS.reorg_threshold, chain_config.polling_interval, // We assume the tested chain is always ingestible for now diff --git a/tests/src/fixture/ethereum.rs b/tests/src/fixture/ethereum.rs index b59d10c9e31..8e0b4537164 100644 --- a/tests/src/fixture/ethereum.rs +++ b/tests/src/fixture/ethereum.rs @@ -13,6 +13,7 @@ use graph::prelude::ethabi::ethereum_types::H256; use graph::prelude::web3::types::{Address, Log, Transaction, H160}; use graph::prelude::{ethabi, tiny_keccak, LightEthereumBlock, ENV_VARS}; use graph::{blockchain::block_stream::BlockWithTriggers, prelude::ethabi::ethereum_types::U64}; +use graph_chain_ethereum::network::EthereumNetworkAdapters; use graph_chain_ethereum::trigger::LogRef; use graph_chain_ethereum::Chain; use graph_chain_ethereum::{ @@ -44,6 +45,8 @@ pub async fn chain( let static_block_stream = Arc::new(StaticStreamBuilder { chain: blocks }); let block_stream_builder = Arc::new(MutexBlockStreamBuilder(Mutex::new(static_block_stream))); + let eth_adapters = Arc::new(EthereumNetworkAdapters::default()); + let chain = Chain::new( logger_factory, stores.network_name.clone(), @@ -57,6 +60,7 @@ pub async fn chain( Arc::new(StaticBlockRefetcher { x: PhantomData }), triggers_adapter, Arc::new(NoopRuntimeAdapter { x: PhantomData }), + eth_adapters, ENV_VARS.reorg_threshold, ENV_VARS.ingestor_polling_interval, // We assume the tested chain is always ingestible for now From f25610a407d3e6daffeb5c4a11e71446a96a5d14 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Tue, 5 Mar 2024 15:40:23 -0800 Subject: [PATCH 21/51] chain, graph: Allow turning off declared calls --- chain/ethereum/src/data_source.rs | 4 ++++ graph/src/env/mappings.rs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index eda0e6944d0..e6da0c913e3 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -6,6 +6,7 @@ use graph::components::subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingEr use graph::components::trigger_processor::RunnableTriggers; use graph::data::value::Word; use graph::data_source::CausalityRegion; +use graph::env::ENV_VARS; use graph::prelude::ethabi::ethereum_types::H160; use graph::prelude::ethabi::{StateMutability, Token}; use graph::prelude::futures03::future::try_join; @@ -1070,6 +1071,9 @@ impl blockchain::DecoderHook for DecoderHook { block_ptr: &BlockPtr, runnables: Vec>, ) -> Result>, MappingError> { + if ENV_VARS.mappings.disable_declared_calls { + return Ok(runnables); + } let start = Instant::now(); let calls: Vec<_> = runnables .iter() diff --git a/graph/src/env/mappings.rs b/graph/src/env/mappings.rs index c2fb81c0799..41499056b5b 100644 --- a/graph/src/env/mappings.rs +++ b/graph/src/env/mappings.rs @@ -57,6 +57,11 @@ pub struct EnvVarsMapping { /// Set by the flag `GRAPH_ALLOW_NON_DETERMINISTIC_IPFS`. Off by /// default. pub allow_non_deterministic_ipfs: bool, + + /// Set by the flag `GRAPH_DISABLE_DECLARED_CALLS`. Disables performing + /// eth calls before running triggers; instead eth calls happen when + /// mappings call `ethereum.call`. Off by default. + pub disable_declared_calls: bool, } // This does not print any values avoid accidentally leaking any sensitive env vars @@ -83,6 +88,7 @@ impl From for EnvVarsMapping { max_ipfs_file_bytes: x.max_ipfs_file_bytes.0, ipfs_request_limit: x.ipfs_request_limit, allow_non_deterministic_ipfs: x.allow_non_deterministic_ipfs.0, + disable_declared_calls: x.disable_declared_calls.0, } } } @@ -115,4 +121,6 @@ pub struct InnerMappingHandlers { ipfs_request_limit: u16, #[envconfig(from = "GRAPH_ALLOW_NON_DETERMINISTIC_IPFS", default = "false")] allow_non_deterministic_ipfs: EnvVarBoolean, + #[envconfig(from = "GRAPH_DISABLE_DECLARED_CALLS", default = "false")] + disable_declared_calls: EnvVarBoolean, } From daa25f5e9636cf2b9738ed1c84750762fc0e3151 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Tue, 5 Mar 2024 15:42:19 -0800 Subject: [PATCH 22/51] chain: Make eth calls in parallel --- chain/ethereum/src/data_source.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index e6da0c913e3..31cad336071 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -1099,10 +1099,15 @@ impl blockchain::DecoderHook for DecoderHook { .collect(); let calls_count = calls.len(); - let mut fail_count: usize = 0; - for (metrics, call) in calls { - fail_count += self.eth_call(logger, block_ptr, metrics, call).await?; - } + + let fail_count: usize = graph::prelude::futures03::future::try_join_all( + calls + .into_iter() + .map(|(metrics, call)| self.eth_call(logger, block_ptr, metrics, call)), + ) + .await? + .into_iter() + .sum(); // TODO: Remove this logging before merging if calls_count > 0 { From 1003ab82d9852c3d614e9259bf9366cf2e72da4f Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Tue, 5 Mar 2024 18:45:48 -0800 Subject: [PATCH 23/51] chain/ethereum: Asyncify EthereumAdapter.contract_call --- chain/ethereum/src/adapter.rs | 4 +- chain/ethereum/src/data_source.rs | 2 - chain/ethereum/src/ethereum_adapter.rs | 122 ++++++++---------- chain/ethereum/src/runtime/runtime_adapter.rs | 2 +- 4 files changed, 58 insertions(+), 72 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index 37efbf8553d..5cb70d952df 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -965,12 +965,12 @@ pub trait EthereumAdapter: Send + Sync + 'static { ) -> Box, Error = Error> + Send>; /// Call the function of a smart contract. - fn contract_call( + async fn contract_call( &self, logger: &Logger, call: EthereumContractCall, cache: Arc, - ) -> Box, Error = EthereumContractCallError> + Send>; + ) -> Result, EthereumContractCallError>; fn get_balance( &self, diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 31cad336071..272b813c912 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -12,7 +12,6 @@ use graph::prelude::ethabi::{StateMutability, Token}; use graph::prelude::futures03::future::try_join; use graph::prelude::futures03::stream::FuturesOrdered; use graph::prelude::regex::Regex; -use graph::prelude::Future01CompatExt; use graph::prelude::{Link, SubgraphManifestValidationError}; use graph::slog::{info, o, trace}; use lazy_static::lazy_static; @@ -1010,7 +1009,6 @@ impl DecoderHook { let result = eth_adapter .contract_call(logger, eth_call, self.call_cache.cheap_clone()) - .compat() .await; let elapsed = start.elapsed(); diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 26b4215734f..0d3f3373db2 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1,4 +1,3 @@ -use futures::future; use futures::prelude::*; use futures03::{future::BoxFuture, stream::FuturesUnordered}; use graph::blockchain::client::ChainClient; @@ -453,14 +452,14 @@ impl EthereumAdapter { .compat() } - fn call( + async fn call( &self, logger: Logger, contract_address: Address, call_data: Bytes, block_ptr: BlockPtr, gas: Option, - ) -> impl Future + Send { + ) -> Result { let web3 = self.web3.clone(); let logger = Logger::new(&logger, o!("provider" => self.provider.clone())); @@ -609,7 +608,7 @@ impl EthereumAdapter { }) .map_err(|e| e.into_inner().unwrap_or(EthereumContractCallError::Timeout)) .boxed() - .compat() + .await } /// Request blocks by hash through JSON-RPC. @@ -1304,12 +1303,12 @@ impl EthereumAdapterTrait for EthereumAdapter { Box::new(self.balance(logger, address, block_ptr)) } - fn contract_call( + async fn contract_call( &self, logger: &Logger, call: EthereumContractCall, cache: Arc, - ) -> Box, Error = EthereumContractCallError> + Send> { + ) -> Result, EthereumContractCallError> { // Emit custom error for type mismatches. for (token, kind) in call .args @@ -1317,17 +1316,17 @@ impl EthereumAdapterTrait for EthereumAdapter { .zip(call.function.inputs.iter().map(|p| &p.kind)) { if !token.type_check(kind) { - return Box::new(future::err(EthereumContractCallError::TypeError( + return Err(EthereumContractCallError::TypeError( token.clone(), kind.clone(), - ))); + )); } } // Encode the call parameters according to the ABI let call_data = match call.function.encode_input(&call.args) { Ok(data) => data, - Err(e) => return Box::new(future::err(EthereumContractCallError::EncodingError(e))), + Err(e) => return Err(EthereumContractCallError::EncodingError(e)), }; debug!(logger, "eth_call"; @@ -1339,67 +1338,56 @@ impl EthereumAdapterTrait for EthereumAdapter { ); // Check if we have it cached, if not do the call and cache. - Box::new( - match cache - .get_call(call.address, &call_data, call.block_ptr.clone()) - .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) - .ok() - .flatten() - { - Some(result) => { - Box::new(future::ok(result)) as Box + Send> - } - None => { - let cache = cache.clone(); - let call = call.clone(); - let logger = logger.clone(); - Box::new( - self.call( - logger.clone(), - call.address, - Bytes(call_data.clone()), - call.block_ptr.clone(), - call.gas, - ) - .map(move |result| { - // Don't block handler execution on writing to the cache. - let for_cache = result.0.clone(); - if !result.0.is_empty() { - let _ = graph::spawn_blocking_allow_panic(move || { - cache - .set_call( - call.address, - &call_data, - call.block_ptr, - &for_cache, - ) - .map_err(|e| { - error!(logger, "call cache set error"; - "error" => e.to_string()) - }) - }); - } - result.0 - }), + match cache + .get_call(call.address, &call_data, call.block_ptr.clone()) + .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) + .ok() + .flatten() + { + Some(result) => Ok(result), + None => { + let cache = cache.clone(); + let call = call.clone(); + let logger = logger.clone(); + let result = self + .call( + logger.clone(), + call.address, + Bytes(call_data.clone()), + call.block_ptr.clone(), + call.gas, ) + .await?; + // Don't block handler execution on writing to the cache. + let for_cache = result.0.clone(); + if !result.0.is_empty() { + let _ = graph::spawn_blocking_allow_panic(move || { + cache + .set_call(call.address, &call_data, call.block_ptr, &for_cache) + .map_err(|e| { + error!(logger, "call cache set error"; + "error" => e.to_string()) + }) + }); } + Ok(result.0) } - // Decode the return values according to the ABI - .and_then(move |output| { - if output.is_empty() { - // We got a `0x` response. For old Geth, this can mean a revert. It can also be - // that the contract actually returned an empty response. A view call is meant - // to return something, so we treat empty responses the same as reverts. - Err(EthereumContractCallError::Revert("empty response".into())) - } else { - // Decode failures are reverts. The reasoning is that if Solidity fails to - // decode an argument, that's a revert, so the same goes for the output. - call.function.decode_output(&output).map_err(|e| { - EthereumContractCallError::Revert(format!("failed to decode output: {}", e)) - }) - } - }), - ) + } + // Decode the return values according to the ABI + .and_then(move |output| { + if output.is_empty() { + // We got a `0x` response. For old Geth, this can mean a revert. It can also be + // that the contract actually returned an empty response. A view call is meant + // to return something, so we treat empty responses the same as reverts. + Err(EthereumContractCallError::Revert("empty response".into())) + } else { + // Decode failures are reverts. The reasoning is that if Solidity fails to + // decode an argument, that's a revert, so the same goes for the output. + call.function.decode_output(&output).map_err(|e| { + EthereumContractCallError::Revert(format!("failed to decode output: {}", e)) + }) + } + }) } /// Load Ethereum blocks in bulk, returning results as they come back as a Stream. diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index f9ffb93f585..cdbe56f0c19 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -235,7 +235,7 @@ fn eth_call( let logger1 = logger.clone(); let call_cache = call_cache.clone(); let result = match graph::block_on( - eth_adapter.contract_call(&logger1, call, call_cache).compat() + eth_adapter.contract_call(&logger1, call, call_cache) ) { Ok(tokens) => Ok(Some(tokens)), Err(EthereumContractCallError::Revert(reason)) => { From 3e40cbd6d45bf92da2d348000b5e92bc8276360f Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Tue, 5 Mar 2024 18:50:52 -0800 Subject: [PATCH 24/51] chain/ethereum: Pass EthereumCall by reference --- chain/ethereum/src/adapter.rs | 2 +- chain/ethereum/src/data_source.rs | 2 +- chain/ethereum/src/ethereum_adapter.rs | 2 +- chain/ethereum/src/runtime/runtime_adapter.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index 5cb70d952df..7c958f8f117 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -968,7 +968,7 @@ pub trait EthereumAdapter: Send + Sync + 'static { async fn contract_call( &self, logger: &Logger, - call: EthereumContractCall, + call: &EthereumContractCall, cache: Arc, ) -> Result, EthereumContractCallError>; diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 272b813c912..8f54d2fc46d 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -1008,7 +1008,7 @@ impl DecoderHook { }))?; let result = eth_adapter - .contract_call(logger, eth_call, self.call_cache.cheap_clone()) + .contract_call(logger, ð_call, self.call_cache.cheap_clone()) .await; let elapsed = start.elapsed(); diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 0d3f3373db2..94d234c0b41 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1306,7 +1306,7 @@ impl EthereumAdapterTrait for EthereumAdapter { async fn contract_call( &self, logger: &Logger, - call: EthereumContractCall, + call: &EthereumContractCall, cache: Arc, ) -> Result, EthereumContractCallError> { // Emit custom error for type mismatches. diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index cdbe56f0c19..dc2531a6dc0 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -235,7 +235,7 @@ fn eth_call( let logger1 = logger.clone(); let call_cache = call_cache.clone(); let result = match graph::block_on( - eth_adapter.contract_call(&logger1, call, call_cache) + eth_adapter.contract_call(&logger1, &call, call_cache) ) { Ok(tokens) => Ok(Some(tokens)), Err(EthereumContractCallError::Revert(reason)) => { From 2a5422291ed196f26ecba26741dc1465f4cd1871 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Tue, 5 Mar 2024 18:58:39 -0800 Subject: [PATCH 25/51] chain/ethereum: Report time for declared eth calls in their own section --- chain/ethereum/src/data_source.rs | 5 +++++ core/src/subgraph/trigger_processor.rs | 6 +++--- graph/src/blockchain/mod.rs | 3 +++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 8f54d2fc46d..4906d9c1afd 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Error}; use anyhow::{ensure, Context}; use graph::blockchain::{BlockPtr, TriggerWithHandler}; +use graph::components::metrics::subgraph::SubgraphInstanceMetrics; use graph::components::store::{EthereumCallCache, StoredDynamicDataSource}; use graph::components::subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError}; use graph::components::trigger_processor::RunnableTriggers; @@ -1068,10 +1069,14 @@ impl blockchain::DecoderHook for DecoderHook { logger: &Logger, block_ptr: &BlockPtr, runnables: Vec>, + metrics: &Arc, ) -> Result>, MappingError> { if ENV_VARS.mappings.disable_declared_calls { return Ok(runnables); } + + let _section = metrics.stopwatch.start_section("declared_ethereum_call"); + let start = Instant::now(); let calls: Vec<_> = runnables .iter() diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index ac072e1faed..6b19505c57c 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -169,7 +169,7 @@ impl> Decoder { block: &Arc, triggers: impl Iterator>, hosts_filter: F, - subgraph_metrics: &Arc, + metrics: &Arc, ) -> Result>, MappingError> where F: Fn(&TriggerData) -> Box + Send + 'a>, @@ -177,13 +177,13 @@ impl> Decoder { let mut runnables = vec![]; for trigger in triggers { let hosts = hosts_filter(&trigger); - match self.match_and_decode(logger, block, trigger, hosts, subgraph_metrics) { + match self.match_and_decode(logger, block, trigger, hosts, metrics) { Ok(runnable_triggers) => runnables.push(runnable_triggers), Err(e) => return Err(e), } } self.hook - .after_decode(logger, &block.ptr(), runnables) + .after_decode(logger, &block.ptr(), runnables, metrics) .await } } diff --git a/graph/src/blockchain/mod.rs b/graph/src/blockchain/mod.rs index 298ef0741fc..92df139a90f 100644 --- a/graph/src/blockchain/mod.rs +++ b/graph/src/blockchain/mod.rs @@ -18,6 +18,7 @@ mod types; use crate::{ cheap_clone::CheapClone, components::{ + metrics::subgraph::SubgraphInstanceMetrics, store::{DeploymentCursorTracker, DeploymentLocator, StoredDynamicDataSource}, subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError}, trigger_processor::RunnableTriggers, @@ -384,6 +385,7 @@ pub trait DecoderHook { logger: &Logger, block_ptr: &BlockPtr, triggers: Vec>, + metrics: &Arc, ) -> Result>, MappingError>; } @@ -398,6 +400,7 @@ impl DecoderHook for NoopDecoderHook { _: &Logger, _: &BlockPtr, triggers: Vec>, + _: &Arc, ) -> Result>, MappingError> { Ok(triggers) } From b6131486ebdc7365fc03d012d32f419d64680708 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 6 Mar 2024 10:30:22 -0800 Subject: [PATCH 26/51] all: Clarify whether an eth call was never made or had no result This makes it possible to cache calls we made that had no result, though we do not send these to the EthereumCallCache yet --- chain/ethereum/src/buffered_call_cache.rs | 21 ++++++-------- chain/ethereum/src/ethereum_adapter.rs | 9 +++--- graph/src/components/store/traits.rs | 28 ++++++++++++++++--- store/postgres/src/chain_store.rs | 16 ++++++++--- store/test-store/tests/postgres/chain_head.rs | 24 ++++++++++++++-- 5 files changed, 72 insertions(+), 26 deletions(-) diff --git a/chain/ethereum/src/buffered_call_cache.rs b/chain/ethereum/src/buffered_call_cache.rs index b98d6d65453..5996425b852 100644 --- a/chain/ethereum/src/buffered_call_cache.rs +++ b/chain/ethereum/src/buffered_call_cache.rs @@ -4,7 +4,7 @@ use std::{ }; use graph::{ - components::store::EthereumCallCache, + components::store::{CallResult, EthereumCallCache}, prelude::{ethabi, BlockPtr, CachedEthereumCall}, }; @@ -14,7 +14,7 @@ use graph::{ /// call, the buffer is cleared. pub struct BufferedCallCache { call_cache: Arc, - buffer: Arc), Vec>>>, + buffer: Arc), CallResult>>>, block: Arc>>, } @@ -42,7 +42,7 @@ impl EthereumCallCache for BufferedCallCache { contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - ) -> Result>, graph::prelude::Error> { + ) -> Result, graph::prelude::Error> { self.check_block(&block); { @@ -56,9 +56,9 @@ impl EthereumCallCache for BufferedCallCache { .call_cache .get_call(contract_address, encoded_call, block)?; - if let Some(result) = &result { - let mut buffer = self.buffer.lock().unwrap(); - buffer.insert((contract_address, encoded_call.to_vec()), result.clone()); + let mut buffer = self.buffer.lock().unwrap(); + if let Some(value) = &result { + buffer.insert((contract_address, encoded_call.to_vec()), value.clone()); } Ok(result) } @@ -75,18 +75,15 @@ impl EthereumCallCache for BufferedCallCache { contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - return_value: &[u8], + return_value: CallResult, ) -> Result<(), graph::prelude::Error> { self.check_block(&block); self.call_cache - .set_call(contract_address, encoded_call, block, return_value)?; + .set_call(contract_address, encoded_call, block, return_value.clone())?; let mut buffer = self.buffer.lock().unwrap(); - buffer.insert( - (contract_address, encoded_call.to_vec()), - return_value.to_vec(), - ); + buffer.insert((contract_address, encoded_call.to_vec()), return_value); Ok(()) } } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 94d234c0b41..41d95fcf65b 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -3,6 +3,7 @@ use futures03::{future::BoxFuture, stream::FuturesUnordered}; use graph::blockchain::client::ChainClient; use graph::blockchain::BlockHash; use graph::blockchain::ChainIdentifier; +use graph::components::store::CallResult; use graph::components::transaction_receipt::LightTransactionReceipt; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::data::subgraph::API_VERSION_0_0_7; @@ -1344,8 +1345,8 @@ impl EthereumAdapterTrait for EthereumAdapter { .ok() .flatten() { - Some(result) => Ok(result), - None => { + Some(CallResult::Value(result)) => Ok(result), + Some(CallResult::Null) | None => { let cache = cache.clone(); let call = call.clone(); let logger = logger.clone(); @@ -1359,11 +1360,11 @@ impl EthereumAdapterTrait for EthereumAdapter { ) .await?; // Don't block handler execution on writing to the cache. - let for_cache = result.0.clone(); + let for_cache = CallResult::Value(result.0.clone()); if !result.0.is_empty() { let _ = graph::spawn_blocking_allow_panic(move || { cache - .set_call(call.address, &call_data, call.block_ptr, &for_cache) + .set_call(call.address, &call_data, call.block_ptr, for_cache) .map_err(|e| { error!(logger, "call cache set error"; "error" => e.to_string()) diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index 6834524916a..d1c9555cffd 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -525,15 +525,35 @@ pub trait ChainStore: Send + Sync + 'static { async fn clear_call_cache(&self, from: BlockNumber, to: BlockNumber) -> Result<(), Error>; } +/// The result of an ethereum call. `Null` indicates that we made the call +/// but didn't get a value back (including when we get the error 'call +/// reverted') +#[derive(Debug, Clone, PartialEq)] +pub enum CallResult { + Null, + Value(Vec), +} + +impl CallResult { + pub fn unwrap(self) -> Vec { + use CallResult::*; + match self { + Value(val) => val, + Null => panic!("called `CachedCallResult::unwrap()` on a `Null` value"), + } + } +} + pub trait EthereumCallCache: Send + Sync + 'static { - /// Returns the return value of the provided Ethereum call, if present in - /// the cache. + /// Returns the return value of the provided Ethereum call, if present + /// in the cache. A return of `None` indicates that we know nothing + /// about the call. fn get_call( &self, contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - ) -> Result>, Error>; + ) -> Result, Error>; /// Returns all cached calls for a given `block`. This method does *not* /// update the last access time of the returned cached calls. @@ -545,7 +565,7 @@ pub trait EthereumCallCache: Send + Sync + 'static { contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - return_value: &[u8], + return_value: CallResult, ) -> Result<(), Error>; } diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index f00c8fb9a18..6f111cafd6e 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -3,6 +3,7 @@ use diesel::prelude::*; use diesel::r2d2::{ConnectionManager, PooledConnection}; use diesel::sql_types::Text; use diesel::{insert_into, update}; +use graph::components::store::CallResult; use graph::env::ENV_VARS; use graph::parking_lot::RwLock; use graph::prelude::MetricsRegistry; @@ -2392,7 +2393,7 @@ impl EthereumCallCache for ChainStore { contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - ) -> Result>, Error> { + ) -> Result, Error> { let id = contract_call_id(&contract_address, encoded_call, &block); let conn = &mut *self.get_conn()?; if let Some(call_output) = conn.transaction::<_, Error, _>(|conn| { @@ -2408,7 +2409,7 @@ impl EthereumCallCache for ChainStore { Ok(None) } })? { - Ok(Some(call_output)) + Ok(Some(CallResult::Value(call_output))) } else { Ok(None) } @@ -2424,8 +2425,15 @@ impl EthereumCallCache for ChainStore { contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - return_value: &[u8], + return_value: CallResult, ) -> Result<(), Error> { + let CallResult::Value(return_value) = return_value else { + // We do not want to cache unsuccessful calls as some RPC nodes + // have weird behavior near the chain head. The details are lost + // to time, but we had issues with some RPC clients in the past + // where calls first failed and later succeeded + return Ok(()); + }; let id = contract_call_id(&contract_address, encoded_call, &block); let conn = &mut *self.get_conn()?; conn.transaction(|conn| { @@ -2434,7 +2442,7 @@ impl EthereumCallCache for ChainStore { id.as_ref(), contract_address.as_ref(), block.number, - return_value, + &return_value, ) }) } diff --git a/store/test-store/tests/postgres/chain_head.rs b/store/test-store/tests/postgres/chain_head.rs index ef9e00b3f59..8005b203f6e 100644 --- a/store/test-store/tests/postgres/chain_head.rs +++ b/store/test-store/tests/postgres/chain_head.rs @@ -2,6 +2,7 @@ //! the chain head pointer gets updated in various situations use graph::blockchain::{BlockHash, BlockPtr}; +use graph::components::store::CallResult; use graph::env::ENV_VARS; use graph::prelude::futures03::executor; use std::future::Future; @@ -373,12 +374,16 @@ fn eth_call_cache() { let chain = vec![&*GENESIS_BLOCK, &*BLOCK_ONE, &*BLOCK_TWO]; run_test(chain, |store, _| { + fn ccr(value: &[u8]) -> CallResult { + CallResult::Value(value.to_vec()) + } + let address = H160([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); let call: [u8; 6] = [1, 2, 3, 4, 5, 6]; let return_value: [u8; 3] = [7, 8, 9]; store - .set_call(address, &call, BLOCK_ONE.block_ptr(), &return_value) + .set_call(address, &call, BLOCK_ONE.block_ptr(), ccr(&return_value)) .unwrap(); let ret = store @@ -389,6 +394,7 @@ fn eth_call_cache() { let ret = store .get_call(address, &call, BLOCK_ONE.block_ptr()) .unwrap() + .unwrap() .unwrap(); assert_eq!(&return_value, ret.as_slice()); @@ -399,14 +405,28 @@ fn eth_call_cache() { let new_return_value: [u8; 3] = [10, 11, 12]; store - .set_call(address, &call, BLOCK_TWO.block_ptr(), &new_return_value) + .set_call( + address, + &call, + BLOCK_TWO.block_ptr(), + ccr(&new_return_value), + ) .unwrap(); let ret = store .get_call(address, &call, BLOCK_TWO.block_ptr()) .unwrap() + .unwrap() .unwrap(); assert_eq!(&new_return_value, ret.as_slice()); + store + .set_call(address, &call, BLOCK_THREE.block_ptr(), CallResult::Null) + .unwrap(); + let ret = store + .get_call(address, &call, BLOCK_THREE.block_ptr()) + .unwrap(); + assert_eq!(None, ret); + Ok(()) }) } From 8bfc61e80a678c384addb82ed95f085a73e6216e Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 6 Mar 2024 10:41:57 -0800 Subject: [PATCH 27/51] chain, graph, store: Use Bytes instad of Vec for eth_call results --- chain/ethereum/src/ethereum_adapter.rs | 11 ++++++----- graph/src/components/store/traits.rs | 4 ++-- store/postgres/src/chain_store.rs | 6 ++++-- store/test-store/tests/postgres/chain_head.rs | 3 ++- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 41d95fcf65b..094800c4e42 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -5,6 +5,7 @@ use graph::blockchain::BlockHash; use graph::blockchain::ChainIdentifier; use graph::components::store::CallResult; use graph::components::transaction_receipt::LightTransactionReceipt; +use graph::data::store::scalar; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::data::subgraph::API_VERSION_0_0_7; use graph::prelude::ethabi::ParamType; @@ -460,7 +461,7 @@ impl EthereumAdapter { call_data: Bytes, block_ptr: BlockPtr, gas: Option, - ) -> Result { + ) -> Result { let web3 = self.web3.clone(); let logger = Logger::new(&logger, o!("provider" => self.provider.clone())); @@ -557,7 +558,7 @@ impl EthereumAdapter { match result { // A successful response. - Ok(bytes) => Ok(bytes), + Ok(bytes) => Ok(scalar::Bytes::from(bytes)), // Check for Geth revert. Err(web3::Error::Rpc(rpc_error)) @@ -1360,8 +1361,8 @@ impl EthereumAdapterTrait for EthereumAdapter { ) .await?; // Don't block handler execution on writing to the cache. - let for_cache = CallResult::Value(result.0.clone()); - if !result.0.is_empty() { + let for_cache = CallResult::Value(result.clone()); + if !result.is_empty() { let _ = graph::spawn_blocking_allow_panic(move || { cache .set_call(call.address, &call_data, call.block_ptr, for_cache) @@ -1371,7 +1372,7 @@ impl EthereumAdapterTrait for EthereumAdapter { }) }); } - Ok(result.0) + Ok(result) } } // Decode the return values according to the ABI diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index d1c9555cffd..e27f2f480de 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -531,11 +531,11 @@ pub trait ChainStore: Send + Sync + 'static { #[derive(Debug, Clone, PartialEq)] pub enum CallResult { Null, - Value(Vec), + Value(Bytes), } impl CallResult { - pub fn unwrap(self) -> Vec { + pub fn unwrap(self) -> Bytes { use CallResult::*; match self { Value(val) => val, diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index 6f111cafd6e..93b22dc2522 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -88,6 +88,7 @@ mod data { }; use graph::blockchain::{Block, BlockHash}; use graph::constraint_violation; + use graph::data::store::scalar::Bytes; use graph::prelude::ethabi::ethereum_types::H160; use graph::prelude::transaction_receipt::LightTransactionReceipt; use graph::prelude::web3::types::H256; @@ -1118,7 +1119,7 @@ mod data { &self, conn: &mut PgConnection, id: &[u8], - ) -> Result, bool)>, Error> { + ) -> Result, Error> { match self { Storage::Shared => { use public::eth_call_cache as cache; @@ -1155,10 +1156,11 @@ mod data { CallMetaTable::ACCESSED_AT )), )) - .first(conn) + .first::<(Vec, bool)>(conn) .optional() .map_err(Error::from), } + .map(|row| row.map(|(return_value, expired)| (Bytes::from(return_value), expired))) } pub(super) fn get_calls_in_block( diff --git a/store/test-store/tests/postgres/chain_head.rs b/store/test-store/tests/postgres/chain_head.rs index 8005b203f6e..517ce96800c 100644 --- a/store/test-store/tests/postgres/chain_head.rs +++ b/store/test-store/tests/postgres/chain_head.rs @@ -3,6 +3,7 @@ use graph::blockchain::{BlockHash, BlockPtr}; use graph::components::store::CallResult; +use graph::data::store::scalar::Bytes; use graph::env::ENV_VARS; use graph::prelude::futures03::executor; use std::future::Future; @@ -375,7 +376,7 @@ fn eth_call_cache() { run_test(chain, |store, _| { fn ccr(value: &[u8]) -> CallResult { - CallResult::Value(value.to_vec()) + CallResult::Value(Bytes::from(value)) } let address = H160([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); From 7f5a671595f38f9ea6ebb208e6d89279cb88568b Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 6 Mar 2024 11:23:45 -0800 Subject: [PATCH 28/51] chain/ethereum: Cache failed call results We want to cache failed call results in the in-memory eth call cache for the duration of a block so that failed declared calls do not need to be repeated when the mapping calls `ethereum.call`. We still do not cache failed results in the store as the store ignores CallResult::Null This also uses CallResult::Null instead of ContractCallError::Revert internally. All we ever did with that error was log it and return `None` from RuntimeAdapter::eth_call. With these changes, we just log a bit earlier and return the same result as before --- chain/ethereum/src/adapter.rs | 7 +- chain/ethereum/src/data_source.rs | 10 +-- chain/ethereum/src/ethereum_adapter.rs | 75 +++++++++++-------- chain/ethereum/src/runtime/runtime_adapter.rs | 8 +- 4 files changed, 49 insertions(+), 51 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index 7c958f8f117..721e32de36d 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -63,8 +63,6 @@ pub enum EthereumContractCallError { EncodingError(ethabi::Error), #[error("call error: {0}")] Web3Error(web3::Error), - #[error("call reverted: {0}")] - Revert(String), #[error("ethereum node took too long to perform call")] Timeout, } @@ -964,13 +962,14 @@ pub trait EthereumAdapter: Send + Sync + 'static { block_number: BlockNumber, ) -> Box, Error = Error> + Send>; - /// Call the function of a smart contract. + /// Call the function of a smart contract. A return of `None` indicates + /// that the call reverted. async fn contract_call( &self, logger: &Logger, call: &EthereumContractCall, cache: Arc, - ) -> Result, EthereumContractCallError>; + ) -> Result>, EthereumContractCallError>; fn get_balance( &self, diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 4906d9c1afd..d6c49e7fc9b 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -1001,7 +1001,6 @@ impl DecoderHook { ) -> Result { let start = Instant::now(); let function_name = call.function.name.clone(); - let address = call.address; let (eth_call, contract_name) = call.as_eth_call(block_ptr.clone()); let eth_adapter = self.eth_adapters.call_or_cheapest(Some(&NodeCapabilities { archive: true, @@ -1025,14 +1024,7 @@ impl DecoderHook { // make them the same but there are subtle differences (return // type, error type) match result { - Ok(_) => Ok(0), - Err(EthereumContractCallError::Revert(reason)) => { - info!(logger, "Declared contract call reverted"; - "reason" => reason, - "contract" => format!("0x{:x}", address), - "call" => format!("{}.{}", contract_name, function_name)); - Ok(1) - } + Ok(r) => Ok(if r.is_some() { 0 } else { 1 }), // Any error reported by the Ethereum node could be due to the block no longer being on // the main chain. This is very unespecific but we don't want to risk failing a diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 094800c4e42..557dea955d5 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -461,7 +461,15 @@ impl EthereumAdapter { call_data: Bytes, block_ptr: BlockPtr, gas: Option, - ) -> Result { + ) -> Result { + fn reverted( + logger: &Logger, + reason: &str, + ) -> Result { + info!(logger, "Contract call reverted"; "reason" => reason); + Ok(CallResult::Null) + } + let web3 = self.web3.clone(); let logger = Logger::new(&logger, o!("provider" => self.provider.clone())); @@ -474,15 +482,12 @@ impl EthereumAdapter { }; let retry_log_message = format!("eth_call RPC call for block {}", block_ptr); retry(retry_log_message, &logger) - .when(|result| match result { - Ok(_) | Err(EthereumContractCallError::Revert(_)) => false, - Err(_) => true, - }) .limit(ENV_VARS.request_retries) .timeout_secs(ENV_VARS.json_rpc_timeout.as_secs()) .run(move || { let call_data = call_data.clone(); let web3 = web3.cheap_clone(); + let logger = logger.cheap_clone(); async move { let req = CallRequest { to: Some(contract_address), @@ -558,14 +563,14 @@ impl EthereumAdapter { match result { // A successful response. - Ok(bytes) => Ok(scalar::Bytes::from(bytes)), + Ok(bytes) => Ok(CallResult::Value(scalar::Bytes::from(bytes))), // Check for Geth revert. Err(web3::Error::Rpc(rpc_error)) if geth_execution_errors .any(|e| rpc_error.message.to_lowercase().contains(e)) => { - Err(EthereumContractCallError::Revert(rpc_error.message)) + reverted(&logger, &rpc_error.message) } // Check for Parity revert. @@ -593,7 +598,7 @@ impl EthereumAdapter { }) .unwrap_or("no reason".to_owned()) }; - Err(EthereumContractCallError::Revert(reason)) + reverted(&logger, &reason) } // The VM execution error was not identified as a revert. @@ -1310,7 +1315,7 @@ impl EthereumAdapterTrait for EthereumAdapter { logger: &Logger, call: &EthereumContractCall, cache: Arc, - ) -> Result, EthereumContractCallError> { + ) -> Result>, EthereumContractCallError> { // Emit custom error for type mismatches. for (token, kind) in call .args @@ -1346,8 +1351,8 @@ impl EthereumAdapterTrait for EthereumAdapter { .ok() .flatten() { - Some(CallResult::Value(result)) => Ok(result), - Some(CallResult::Null) | None => { + Some(result) => Ok(result), + None => { let cache = cache.clone(); let call = call.clone(); let logger = logger.clone(); @@ -1361,33 +1366,39 @@ impl EthereumAdapterTrait for EthereumAdapter { ) .await?; // Don't block handler execution on writing to the cache. - let for_cache = CallResult::Value(result.clone()); - if !result.is_empty() { - let _ = graph::spawn_blocking_allow_panic(move || { - cache - .set_call(call.address, &call_data, call.block_ptr, for_cache) - .map_err(|e| { - error!(logger, "call cache set error"; + let for_cache = result.clone(); + let _ = graph::spawn_blocking_allow_panic(move || { + cache + .set_call(call.address, &call_data, call.block_ptr, for_cache) + .map_err(|e| { + error!(logger, "call cache set error"; "error" => e.to_string()) - }) - }); - } + }) + }); Ok(result) } } // Decode the return values according to the ABI .and_then(move |output| { - if output.is_empty() { - // We got a `0x` response. For old Geth, this can mean a revert. It can also be - // that the contract actually returned an empty response. A view call is meant - // to return something, so we treat empty responses the same as reverts. - Err(EthereumContractCallError::Revert("empty response".into())) - } else { - // Decode failures are reverts. The reasoning is that if Solidity fails to - // decode an argument, that's a revert, so the same goes for the output. - call.function.decode_output(&output).map_err(|e| { - EthereumContractCallError::Revert(format!("failed to decode output: {}", e)) - }) + use CallResult::*; + match output { + Value(output) => match call.function.decode_output(&output) { + Ok(tokens) => Ok(Some(tokens)), + Err(e) => { + // Decode failures are reverts. The reasoning is that if Solidity fails to + // decode an argument, that's a revert, so the same goes for the output. + let reason = format!("failed to decode output: {}", e); + info!(logger, "Contract call reverted"; "reason" => reason); + Ok(None) + } + }, + Null => { + // We got a `0x` response. For old Geth, this can mean a revert. It can also be + // that the contract actually returned an empty response. A view call is meant + // to return something, so we treat empty responses the same as reverts. + info!(logger, "Contract call reverted"; "reason" => "empty response"); + Ok(None) + } } }) } diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index dc2531a6dc0..df3378e8f0b 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -25,7 +25,7 @@ use graph::{ }, runtime::{asc_get, asc_new, AscPtr, HostExportError}, semver::Version, - slog::{info, trace, Logger}, + slog::{trace, Logger}, }; use graph_runtime_wasm::asc_abi::class::{AscBigInt, AscEnumArray, EthereumValueKind}; @@ -237,11 +237,7 @@ fn eth_call( let result = match graph::block_on( eth_adapter.contract_call(&logger1, &call, call_cache) ) { - Ok(tokens) => Ok(Some(tokens)), - Err(EthereumContractCallError::Revert(reason)) => { - info!(logger, "Contract call reverted"; "reason" => reason); - Ok(None) - } + Ok(res) => Ok(res), // Any error reported by the Ethereum node could be due to the block no longer being on // the main chain. This is very unespecific but we don't want to risk failing a From 2581185dcbbe29984ebf67f65cb60db82efe2ced Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 6 Mar 2024 14:29:38 -0800 Subject: [PATCH 29/51] chain/ethereum, graph, store: Avoid double-counting declared calls With declared calls, we seemingly make an eth call at least twice: first in the `DecoderHook` when we either get the return value from the store or from RPC, and then a second time when the mapping uses `ethereum.call` to access the return value. In both cases, we observed the call in the host metrics, inflating the call count. We now annotate the return value with a `CallSource` that tells us where we found the return value and do not mark that call in the metrics if we just looked up a declared call that we made from the `DecoderHook`. --- chain/ethereum/src/adapter.rs | 6 ++- chain/ethereum/src/buffered_call_cache.rs | 8 +-- chain/ethereum/src/data_source.rs | 35 ++++++++----- chain/ethereum/src/ethereum_adapter.rs | 50 +++++++++---------- chain/ethereum/src/runtime/runtime_adapter.rs | 22 +++++--- graph/src/components/store/traits.rs | 19 ++++++- store/postgres/src/chain_store.rs | 13 ++--- store/test-store/tests/postgres/chain_head.rs | 2 + 8 files changed, 94 insertions(+), 61 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index 721e32de36d..c934b82ff74 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -2,6 +2,7 @@ use anyhow::Error; use ethabi::{Error as ABIError, Function, ParamType, Token}; use futures::Future; use graph::blockchain::ChainIdentifier; +use graph::components::store::CallSource; use graph::firehose::CallToFilter; use graph::firehose::CombinedFilter; use graph::firehose::LogFilter; @@ -963,13 +964,14 @@ pub trait EthereumAdapter: Send + Sync + 'static { ) -> Box, Error = Error> + Send>; /// Call the function of a smart contract. A return of `None` indicates - /// that the call reverted. + /// that the call reverted. The returned `CallSource` indicates where + /// the result came from for accounting purposes async fn contract_call( &self, logger: &Logger, call: &EthereumContractCall, cache: Arc, - ) -> Result>, EthereumContractCallError>; + ) -> Result<(Option>, CallSource), EthereumContractCallError>; fn get_balance( &self, diff --git a/chain/ethereum/src/buffered_call_cache.rs b/chain/ethereum/src/buffered_call_cache.rs index 5996425b852..5295cc4ea43 100644 --- a/chain/ethereum/src/buffered_call_cache.rs +++ b/chain/ethereum/src/buffered_call_cache.rs @@ -4,7 +4,7 @@ use std::{ }; use graph::{ - components::store::{CallResult, EthereumCallCache}, + components::store::{CallResult, CallSource, EthereumCallCache}, prelude::{ethabi, BlockPtr, CachedEthereumCall}, }; @@ -42,13 +42,13 @@ impl EthereumCallCache for BufferedCallCache { contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - ) -> Result, graph::prelude::Error> { + ) -> Result, graph::prelude::Error> { self.check_block(&block); { let buffer = self.buffer.lock().unwrap(); if let Some(result) = buffer.get(&(contract_address, encoded_call.to_vec())) { - return Ok(Some(result.clone())); + return Ok(Some((result.clone(), CallSource::Memory))); } } @@ -57,7 +57,7 @@ impl EthereumCallCache for BufferedCallCache { .get_call(contract_address, encoded_call, block)?; let mut buffer = self.buffer.lock().unwrap(); - if let Some(value) = &result { + if let Some((value, _)) = &result { buffer.insert((contract_address, encoded_call.to_vec()), value.clone()); } Ok(result) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index d6c49e7fc9b..c4ce39bcd6f 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Error}; use anyhow::{ensure, Context}; use graph::blockchain::{BlockPtr, TriggerWithHandler}; use graph::components::metrics::subgraph::SubgraphInstanceMetrics; -use graph::components::store::{EthereumCallCache, StoredDynamicDataSource}; +use graph::components::store::{CallSource, EthereumCallCache, StoredDynamicDataSource}; use graph::components::subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError}; use graph::components::trigger_processor::RunnableTriggers; use graph::data::value::Word; @@ -1007,23 +1007,21 @@ impl DecoderHook { traces: false, }))?; - let result = eth_adapter + let (result, source) = match eth_adapter .contract_call(logger, ð_call, self.call_cache.cheap_clone()) - .await; - - let elapsed = start.elapsed(); - - metrics.observe_eth_call_execution_time( - elapsed.as_secs_f64(), - &contract_name, - &function_name, - ); + .await + { + Ok((result, source)) => (Ok(result), source), + Err(e) => (Err(e), CallSource::Rpc), + }; // This error analysis is very much modeled on the one in // `crate::runtime_adapter::eth_call`; it would be better to // make them the same but there are subtle differences (return // type, error type) - match result { + // + // For errors, we set the source to 'Rpc' as that's where errors happen + let result= match result { Ok(r) => Ok(if r.is_some() { 0 } else { 1 }), // Any error reported by the Ethereum node could be due to the block no longer being on @@ -1049,8 +1047,19 @@ impl DecoderHook { contract_name, e ))), + }; - } + if source.observe() { + let elapsed = start.elapsed(); + + metrics.observe_eth_call_execution_time( + elapsed.as_secs_f64(), + &contract_name, + &function_name, + ); + } + + result } } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 557dea955d5..dd226c2273e 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -4,6 +4,7 @@ use graph::blockchain::client::ChainClient; use graph::blockchain::BlockHash; use graph::blockchain::ChainIdentifier; use graph::components::store::CallResult; +use graph::components::store::CallSource; use graph::components::transaction_receipt::LightTransactionReceipt; use graph::data::store::scalar; use graph::data::subgraph::UnifiedMappingApiVersion; @@ -1315,7 +1316,7 @@ impl EthereumAdapterTrait for EthereumAdapter { logger: &Logger, call: &EthereumContractCall, cache: Arc, - ) -> Result>, EthereumContractCallError> { + ) -> Result<(Option>, CallSource), EthereumContractCallError> { // Emit custom error for type mismatches. for (token, kind) in call .args @@ -1345,13 +1346,13 @@ impl EthereumAdapterTrait for EthereumAdapter { ); // Check if we have it cached, if not do the call and cache. - match cache + let (output, source) = match cache .get_call(call.address, &call_data, call.block_ptr.clone()) .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) .ok() .flatten() { - Some(result) => Ok(result), + Some(result) => result, None => { let cache = cache.clone(); let call = call.clone(); @@ -1375,32 +1376,31 @@ impl EthereumAdapterTrait for EthereumAdapter { "error" => e.to_string()) }) }); - Ok(result) + (result, CallSource::Rpc) } - } + }; + // Decode the return values according to the ABI - .and_then(move |output| { - use CallResult::*; - match output { - Value(output) => match call.function.decode_output(&output) { - Ok(tokens) => Ok(Some(tokens)), - Err(e) => { - // Decode failures are reverts. The reasoning is that if Solidity fails to - // decode an argument, that's a revert, so the same goes for the output. - let reason = format!("failed to decode output: {}", e); - info!(logger, "Contract call reverted"; "reason" => reason); - Ok(None) - } - }, - Null => { - // We got a `0x` response. For old Geth, this can mean a revert. It can also be - // that the contract actually returned an empty response. A view call is meant - // to return something, so we treat empty responses the same as reverts. - info!(logger, "Contract call reverted"; "reason" => "empty response"); - Ok(None) + use CallResult::*; + match output { + Value(output) => match call.function.decode_output(&output) { + Ok(tokens) => Ok((Some(tokens), source)), + Err(e) => { + // Decode failures are reverts. The reasoning is that if Solidity fails to + // decode an argument, that's a revert, so the same goes for the output. + let reason = format!("failed to decode output: {}", e); + info!(logger, "Contract call reverted"; "reason" => reason); + Ok((None, CallSource::Rpc)) } + }, + Null => { + // We got a `0x` response. For old Geth, this can mean a revert. It can also be + // that the contract actually returned an empty response. A view call is meant + // to return something, so we treat empty responses the same as reverts. + info!(logger, "Contract call reverted"; "reason" => "empty response"); + Ok((None, CallSource::Rpc)) } - }) + } } /// Load Ethereum blocks in bulk, returning results as they come back as a Stream. diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index df3378e8f0b..d39dc894515 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -10,6 +10,7 @@ use crate::{ use anyhow::{anyhow, Context, Error}; use blockchain::HostFn; use graph::blockchain::ChainIdentifier; +use graph::components::store::CallSource; use graph::components::subgraph::HostMetrics; use graph::data::store::scalar::BigInt; use graph::data::subgraph::API_VERSION_0_0_9; @@ -234,9 +235,12 @@ fn eth_call( // Run Ethereum call in tokio runtime let logger1 = logger.clone(); let call_cache = call_cache.clone(); - let result = match graph::block_on( - eth_adapter.contract_call(&logger1, &call, call_cache) - ) { + let (result, source) = + match graph::block_on(eth_adapter.contract_call(&logger1, &call, call_cache)) { + Ok((result, source)) => (Ok(result), source), + Err(e) => (Err(e), CallSource::Rpc), + }; + let result = match result { Ok(res) => Ok(res), // Any error reported by the Ethereum node could be due to the block no longer being on @@ -266,11 +270,13 @@ fn eth_call( let elapsed = start_time.elapsed(); - metrics.observe_eth_call_execution_time( - elapsed.as_secs_f64(), - &unresolved_call.contract_name, - &unresolved_call.function_name, - ); + if source.observe() { + metrics.observe_eth_call_execution_time( + elapsed.as_secs_f64(), + &unresolved_call.contract_name, + &unresolved_call.function_name, + ); + } trace!(logger, "Contract call finished"; "address" => &unresolved_call.contract_address.to_string(), diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index e27f2f480de..6a470a8ef66 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -544,6 +544,23 @@ impl CallResult { } } +/// Indication of where the result of an ethereum call comes from. We +/// unfortunately need that so we can avoid double-counting declared calls +/// as they are accessed as normal eth calls and we'd count them twice +/// without this. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CallSource { + Memory, + Store, + Rpc, +} + +impl CallSource { + pub fn observe(&self) -> bool { + matches!(self, CallSource::Rpc | CallSource::Store) + } +} + pub trait EthereumCallCache: Send + Sync + 'static { /// Returns the return value of the provided Ethereum call, if present /// in the cache. A return of `None` indicates that we know nothing @@ -553,7 +570,7 @@ pub trait EthereumCallCache: Send + Sync + 'static { contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - ) -> Result, Error>; + ) -> Result, Error>; /// Returns all cached calls for a given `block`. This method does *not* /// update the last access time of the returned cached calls. diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index 93b22dc2522..2584445d937 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -3,7 +3,7 @@ use diesel::prelude::*; use diesel::r2d2::{ConnectionManager, PooledConnection}; use diesel::sql_types::Text; use diesel::{insert_into, update}; -use graph::components::store::CallResult; +use graph::components::store::{CallResult, CallSource}; use graph::env::ENV_VARS; use graph::parking_lot::RwLock; use graph::prelude::MetricsRegistry; @@ -2395,10 +2395,10 @@ impl EthereumCallCache for ChainStore { contract_address: ethabi::Address, encoded_call: &[u8], block: BlockPtr, - ) -> Result, Error> { + ) -> Result, Error> { let id = contract_call_id(&contract_address, encoded_call, &block); let conn = &mut *self.get_conn()?; - if let Some(call_output) = conn.transaction::<_, Error, _>(|conn| { + let return_value = conn.transaction::<_, Error, _>(|conn| { if let Some((return_value, update_accessed_at)) = self.storage.get_call_and_access(conn, id.as_ref())? { @@ -2410,11 +2410,8 @@ impl EthereumCallCache for ChainStore { } else { Ok(None) } - })? { - Ok(Some(CallResult::Value(call_output))) - } else { - Ok(None) - } + })?; + Ok(return_value.map(|return_value| (CallResult::Value(return_value), CallSource::Store))) } fn get_calls_in_block(&self, block: BlockPtr) -> Result, Error> { diff --git a/store/test-store/tests/postgres/chain_head.rs b/store/test-store/tests/postgres/chain_head.rs index 517ce96800c..ed163709d97 100644 --- a/store/test-store/tests/postgres/chain_head.rs +++ b/store/test-store/tests/postgres/chain_head.rs @@ -396,6 +396,7 @@ fn eth_call_cache() { .get_call(address, &call, BLOCK_ONE.block_ptr()) .unwrap() .unwrap() + .0 .unwrap(); assert_eq!(&return_value, ret.as_slice()); @@ -417,6 +418,7 @@ fn eth_call_cache() { .get_call(address, &call, BLOCK_TWO.block_ptr()) .unwrap() .unwrap() + .0 .unwrap(); assert_eq!(&new_return_value, ret.as_slice()); From d94916a2d3a299cf941418bce44453e3439a6106 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 6 Mar 2024 14:50:05 -0800 Subject: [PATCH 30/51] chain/ethereum: Deduplicate declared eth calls --- chain/ethereum/src/data_source.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index c4ce39bcd6f..3745c275184 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -911,7 +911,7 @@ impl DataSource { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct DeclaredCall { contract_name: String, address: Address, @@ -1102,6 +1102,25 @@ impl blockchain::DecoderHook for DecoderHook { .flatten() .collect(); + // Deduplicate calls. Unfortunately, we can't get `DeclaredCall` to + // implement `Hash` or `Ord` easily, so we can only deduplicate by + // comparing the whole call not with a `HashSet` or `BTreeSet`. + // Since that can be inefficient, we don't deduplicate if we have an + // enormous amount of calls; in that case though, things will likely + // blow up because of the amount of I/O that many calls cause. + // Cutting off at 1000 is fairly arbitrary + let calls = if calls.len() < 1000 { + let mut uniq_calls = Vec::new(); + for (metrics, call) in calls { + if !uniq_calls.iter().any(|(_, c)| c == &call) { + uniq_calls.push((metrics, call)); + } + } + uniq_calls + } else { + calls + }; + let calls_count = calls.len(); let fail_count: usize = graph::prelude::futures03::future::try_join_all( From 5681367073f00e75846d1b61f927591aebdf08fa Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 6 Mar 2024 15:13:30 -0800 Subject: [PATCH 31/51] chain/ethereum: Provide the correct amount of gas for declared calls --- chain/ethereum/src/chain.rs | 4 +++ chain/ethereum/src/data_source.rs | 9 ++++--- chain/ethereum/src/runtime/runtime_adapter.rs | 25 +++++++++++-------- graph/src/components/store/traits.rs | 5 +++- store/postgres/src/chain_store.rs | 4 +++ 5 files changed, 32 insertions(+), 15 deletions(-) diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index a977b26330e..f1d454ccc26 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -42,6 +42,7 @@ use crate::data_source::DataSourceTemplate; use crate::data_source::UnresolvedDataSourceTemplate; use crate::ingestor::PollingBlockIngestor; use crate::network::EthereumNetworkAdapters; +use crate::runtime::runtime_adapter::eth_call_gas; use crate::EthereumAdapter; use crate::NodeCapabilities; use crate::{ @@ -511,9 +512,12 @@ impl Blockchain for Chain { } fn decoder_hook(&self) -> Self::DecoderHook { + let eth_call_gas = eth_call_gas(self.chain_store.chain_identifier()); + crate::data_source::DecoderHook::new( self.eth_adapters.cheap_clone(), self.call_cache.cheap_clone(), + eth_call_gas, ) } } diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 3745c275184..5d203ef0a1c 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -960,14 +960,14 @@ impl DeclaredCall { Ok(calls) } - fn as_eth_call(self, block_ptr: BlockPtr) -> (EthereumContractCall, String) { + fn as_eth_call(self, block_ptr: BlockPtr, gas: Option) -> (EthereumContractCall, String) { ( EthereumContractCall { address: self.address, block_ptr, function: self.function, args: self.args, - gas: None, + gas, }, self.contract_name, ) @@ -977,16 +977,19 @@ impl DeclaredCall { pub struct DecoderHook { eth_adapters: Arc, call_cache: Arc, + eth_call_gas: Option, } impl DecoderHook { pub fn new( eth_adapters: Arc, call_cache: Arc, + eth_call_gas: Option, ) -> Self { Self { eth_adapters, call_cache, + eth_call_gas, } } } @@ -1001,7 +1004,7 @@ impl DecoderHook { ) -> Result { let start = Instant::now(); let function_name = call.function.name.clone(); - let (eth_call, contract_name) = call.as_eth_call(block_ptr.clone()); + let (eth_call, contract_name) = call.as_eth_call(block_ptr.clone(), self.eth_call_gas); let eth_adapter = self.eth_adapters.call_or_cheapest(Some(&NodeCapabilities { archive: true, traces: false, diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index d39dc894515..6e7a4269ef1 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -61,23 +61,26 @@ pub struct RuntimeAdapter { pub chain_identifier: Arc, } +pub fn eth_call_gas(chain_identifier: &ChainIdentifier) -> Option { + // Check if the current network version is in the eth_call_no_gas list + let should_skip_gas = ENV_VARS + .eth_call_no_gas + .contains(&chain_identifier.net_version); + + if should_skip_gas { + None + } else { + Some(ETH_CALL_GAS) + } +} + impl blockchain::RuntimeAdapter for RuntimeAdapter { fn host_fns(&self, ds: &DataSource) -> Result, Error> { let abis = ds.mapping.abis.clone(); let call_cache = self.call_cache.cheap_clone(); let eth_adapters = self.eth_adapters.cheap_clone(); let archive = ds.mapping.requires_archive()?; - - // Check if the current network version is in the eth_call_no_gas list - let should_skip_gas = ENV_VARS - .eth_call_no_gas - .contains(&self.chain_identifier.net_version); - - let eth_call_gas = if should_skip_gas { - None - } else { - Some(ETH_CALL_GAS) - }; + let eth_call_gas = eth_call_gas(&self.chain_identifier); let ethereum_call = HostFn { name: "ethereum.call", diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index 6a470a8ef66..c52b6bb6973 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -6,7 +6,7 @@ use web3::types::{Address, H256}; use super::*; use crate::blockchain::block_stream::FirehoseCursor; -use crate::blockchain::BlockTime; +use crate::blockchain::{BlockTime, ChainIdentifier}; use crate::components::metrics::stopwatch::StopwatchMetrics; use crate::components::server::index_node::VersionInfo; use crate::components::subgraph::SubgraphVersionSwitchingMode; @@ -523,6 +523,9 @@ pub trait ChainStore: Send + Sync + 'static { /// Clears call cache of the chain for the given `from` and `to` block number. async fn clear_call_cache(&self, from: BlockNumber, to: BlockNumber) -> Result<(), Error>; + + /// Return the chain identifier for this store. + fn chain_identifier(&self) -> &ChainIdentifier; } /// The result of an ethereum call. `Null` indicates that we made the call diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index 2584445d937..30fee46ab97 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -2198,6 +2198,10 @@ impl ChainStoreTrait for ChainStore { }) .await } + + fn chain_identifier(&self) -> &ChainIdentifier { + &self.chain_identifier + } } mod recent_blocks_cache { From 1910a1168751ab41a952b64c9c0a41d62284579d Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Thu, 7 Mar 2024 10:50:44 -0800 Subject: [PATCH 32/51] chain, graph, store: Address race condition in caching eth calls We need to make sure that the result of an eth call is entered into the in-memory cache synchronously, before handlers are run to ensure that they will find the call result. Only the part that writes it to the store can happen asynchronously. --- chain/ethereum/src/buffered_call_cache.rs | 32 ++++++++++++++++--- chain/ethereum/src/ethereum_adapter.rs | 27 ++++++++-------- graph/src/components/store/traits.rs | 3 +- store/postgres/src/chain_store.rs | 5 +-- store/test-store/tests/postgres/chain_head.rs | 21 ++++++++++-- 5 files changed, 64 insertions(+), 24 deletions(-) diff --git a/chain/ethereum/src/buffered_call_cache.rs b/chain/ethereum/src/buffered_call_cache.rs index 5295cc4ea43..b1dc1801d25 100644 --- a/chain/ethereum/src/buffered_call_cache.rs +++ b/chain/ethereum/src/buffered_call_cache.rs @@ -4,8 +4,10 @@ use std::{ }; use graph::{ + cheap_clone::CheapClone, components::store::{CallResult, CallSource, EthereumCallCache}, prelude::{ethabi, BlockPtr, CachedEthereumCall}, + slog::{error, Logger}, }; /// A wrapper around an Ethereum call cache that buffers call results in @@ -72,18 +74,38 @@ impl EthereumCallCache for BufferedCallCache { fn set_call( &self, + logger: &Logger, contract_address: ethabi::Address, - encoded_call: &[u8], + encoded_call: Arc>, block: BlockPtr, return_value: CallResult, ) -> Result<(), graph::prelude::Error> { self.check_block(&block); - self.call_cache - .set_call(contract_address, encoded_call, block, return_value.clone())?; + // Enter the call into the in-memory cache immediately so that + // handlers will find it, but add it to the underlying cache in the + // background so we do not have to wait for that as it will be a + // cache backed by the database + { + let mut buffer = self.buffer.lock().unwrap(); + buffer.insert( + (contract_address, encoded_call.to_vec()), + return_value.clone(), + ); + } + + let cache = self.call_cache.cheap_clone(); + let logger = logger.cheap_clone(); + let _ = graph::spawn_blocking_allow_panic(move || { + cache + .set_call(&logger, contract_address, encoded_call, block, return_value) + .map_err(|e| { + error!(logger, "BufferedCallCache: call cache set error"; + "contract_address" => format!("{:?}", contract_address), + "error" => e.to_string()) + }) + }); - let mut buffer = self.buffer.lock().unwrap(); - buffer.insert((contract_address, encoded_call.to_vec()), return_value); Ok(()) } } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index dd226c2273e..699c15d0f9f 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1354,9 +1354,6 @@ impl EthereumAdapterTrait for EthereumAdapter { { Some(result) => result, None => { - let cache = cache.clone(); - let call = call.clone(); - let logger = logger.clone(); let result = self .call( logger.clone(), @@ -1366,16 +1363,20 @@ impl EthereumAdapterTrait for EthereumAdapter { call.gas, ) .await?; - // Don't block handler execution on writing to the cache. - let for_cache = result.clone(); - let _ = graph::spawn_blocking_allow_panic(move || { - cache - .set_call(call.address, &call_data, call.block_ptr, for_cache) - .map_err(|e| { - error!(logger, "call cache set error"; - "error" => e.to_string()) - }) - }); + let _ = cache + .set_call( + &logger, + call.address, + Arc::new(call_data), + call.block_ptr.cheap_clone(), + result.clone(), + ) + .map_err(|e| { + error!(logger, "EthereumAdapter: call cache set error"; + "contract_address" => format!("{:?}", call.address), + "error" => e.to_string()) + }); + (result, CallSource::Rpc) } }; diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index c52b6bb6973..623dcbf75bd 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -582,8 +582,9 @@ pub trait EthereumCallCache: Send + Sync + 'static { /// Stores the provided Ethereum call in the cache. fn set_call( &self, + logger: &Logger, contract_address: ethabi::Address, - encoded_call: &[u8], + encoded_call: Arc>, block: BlockPtr, return_value: CallResult, ) -> Result<(), Error>; diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index 30fee46ab97..0fe3ae7407a 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -2425,8 +2425,9 @@ impl EthereumCallCache for ChainStore { fn set_call( &self, + _: &Logger, contract_address: ethabi::Address, - encoded_call: &[u8], + encoded_call: Arc>, block: BlockPtr, return_value: CallResult, ) -> Result<(), Error> { @@ -2437,7 +2438,7 @@ impl EthereumCallCache for ChainStore { // where calls first failed and later succeeded return Ok(()); }; - let id = contract_call_id(&contract_address, encoded_call, &block); + let id = contract_call_id(&contract_address, &encoded_call, &block); let conn = &mut *self.get_conn()?; conn.transaction(|conn| { self.storage.set_call( diff --git a/store/test-store/tests/postgres/chain_head.rs b/store/test-store/tests/postgres/chain_head.rs index ed163709d97..13e44c8f19d 100644 --- a/store/test-store/tests/postgres/chain_head.rs +++ b/store/test-store/tests/postgres/chain_head.rs @@ -375,16 +375,24 @@ fn eth_call_cache() { let chain = vec![&*GENESIS_BLOCK, &*BLOCK_ONE, &*BLOCK_TWO]; run_test(chain, |store, _| { + let logger = LOGGER.cheap_clone(); fn ccr(value: &[u8]) -> CallResult { CallResult::Value(Bytes::from(value)) } let address = H160([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); let call: [u8; 6] = [1, 2, 3, 4, 5, 6]; + let call = Arc::new(call.to_vec()); let return_value: [u8; 3] = [7, 8, 9]; store - .set_call(address, &call, BLOCK_ONE.block_ptr(), ccr(&return_value)) + .set_call( + &logger, + address, + call.cheap_clone(), + BLOCK_ONE.block_ptr(), + ccr(&return_value), + ) .unwrap(); let ret = store @@ -408,8 +416,9 @@ fn eth_call_cache() { let new_return_value: [u8; 3] = [10, 11, 12]; store .set_call( + &logger, address, - &call, + call.cheap_clone(), BLOCK_TWO.block_ptr(), ccr(&new_return_value), ) @@ -423,7 +432,13 @@ fn eth_call_cache() { assert_eq!(&new_return_value, ret.as_slice()); store - .set_call(address, &call, BLOCK_THREE.block_ptr(), CallResult::Null) + .set_call( + &logger, + address, + call.cheap_clone(), + BLOCK_THREE.block_ptr(), + CallResult::Null, + ) .unwrap(); let ret = store .get_call(address, &call, BLOCK_THREE.block_ptr()) From c74cbbaf4304b030c38b771afdc5b5e8309b9e82 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Wed, 6 Mar 2024 16:47:53 -0800 Subject: [PATCH 33/51] chain, graph: Change how we log eth calls Log eth calls in a form that's easy to plug into other tools (like foundry's cast) Demote the more internal logging of encoded eth call data to trace --- chain/ethereum/src/ethereum_adapter.rs | 2 +- chain/ethereum/src/runtime/runtime_adapter.rs | 31 +++++++++++++++---- graph/src/components/store/traits.rs | 10 ++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 699c15d0f9f..c3706201fc3 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1337,7 +1337,7 @@ impl EthereumAdapterTrait for EthereumAdapter { Err(e) => return Err(EthereumContractCallError::EncodingError(e)), }; - debug!(logger, "eth_call"; + trace!(logger, "eth_call"; "fn" => &call.function.name, "address" => hex::encode(call.address), "data" => hex::encode(&call_data), diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index 6e7a4269ef1..51cfb9ef2e0 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -17,6 +17,7 @@ use graph::data::subgraph::API_VERSION_0_0_9; use graph::prelude::web3::types::H160; use graph::runtime::gas::Gas; use graph::runtime::{AscIndexId, IndexForAscTypeId}; +use graph::slog::debug; use graph::{ blockchain::{self, BlockPtr, HostFnCtx}, cheap_clone::CheapClone, @@ -26,9 +27,10 @@ use graph::{ }, runtime::{asc_get, asc_new, AscPtr, HostExportError}, semver::Version, - slog::{trace, Logger}, + slog::Logger, }; use graph_runtime_wasm::asc_abi::class::{AscBigInt, AscEnumArray, EthereumValueKind}; +use itertools::Itertools; use super::abi::{AscUnresolvedContractCall, AscUnresolvedContractCall_0_0_4}; @@ -204,6 +206,19 @@ fn eth_call( eth_call_gas: Option, metrics: Arc, ) -> Result>, HostExportError> { + // Helpers to log the result of the call at the end + fn tokens_as_string(tokens: &[Token]) -> String { + tokens.iter().map(|arg| arg.to_string()).join(", ") + } + + fn result_as_string(result: &Result>, HostExportError>) -> String { + match result { + Ok(Some(tokens)) => format!("({})", tokens_as_string(&tokens)), + Ok(None) => "none".to_string(), + Err(_) => "error".to_string(), + } + } + let start_time = Instant::now(); // Obtain the path to the contract ABI @@ -281,12 +296,16 @@ fn eth_call( ); } - trace!(logger, "Contract call finished"; - "address" => &unresolved_call.contract_address.to_string(), + debug!(logger, "Contract call finished"; + "address" => format!("0x{:x}", &unresolved_call.contract_address), "contract" => &unresolved_call.contract_name, - "function" => &unresolved_call.function_name, - "function_signature" => &unresolved_call.function_signature, - "time" => format!("{}ms", elapsed.as_millis())); + "signature" => &unresolved_call.function_signature, + "args" => format!("[{}]", tokens_as_string(&unresolved_call.function_args)), + "time_ms" => format!("{}ms", elapsed.as_millis()), + "result" => result_as_string(&result), + "block_hash" => block_ptr.hash_hex(), + "block_number" => block_ptr.block_number(), + "source" => source.to_string()); result } diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index 623dcbf75bd..d8425133964 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -564,6 +564,16 @@ impl CallSource { } } +impl std::fmt::Display for CallSource { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + CallSource::Memory => write!(f, "memory"), + CallSource::Store => write!(f, "store"), + CallSource::Rpc => write!(f, "rpc"), + } + } +} + pub trait EthereumCallCache: Send + Sync + 'static { /// Returns the return value of the provided Ethereum call, if present /// in the cache. A return of `None` indicates that we know nothing From 7405863ecd0a18361ecc685b0792b8d0c43fc7f6 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Thu, 7 Mar 2024 15:28:29 -0800 Subject: [PATCH 34/51] chain: Log failures of declared calls --- chain/ethereum/src/data_source.rs | 85 ++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 17 deletions(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 5d203ef0a1c..bd737dd1567 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -14,14 +14,15 @@ use graph::prelude::futures03::future::try_join; use graph::prelude::futures03::stream::FuturesOrdered; use graph::prelude::regex::Regex; use graph::prelude::{Link, SubgraphManifestValidationError}; -use graph::slog::{info, o, trace}; +use graph::slog::{debug, error, o, trace}; +use itertools::Itertools; use lazy_static::lazy_static; use serde::de; use std::collections::HashSet; use std::num::NonZeroU32; use std::str::FromStr; use std::sync::Arc; -use std::time::Instant; +use std::time::{Duration, Instant}; use tiny_keccak::{keccak256, Keccak}; use graph::{ @@ -913,6 +914,8 @@ impl DataSource { #[derive(Clone, Debug, PartialEq)] pub struct DeclaredCall { + /// The user-supplied label from the manifest + label: String, contract_name: String, address: Address, function: Function, @@ -949,6 +952,7 @@ impl DeclaredCall { let args = decl.args(log, params)?; let call = DeclaredCall { + label: decl.label.clone(), contract_name, address, function: function.clone(), @@ -960,7 +964,11 @@ impl DeclaredCall { Ok(calls) } - fn as_eth_call(self, block_ptr: BlockPtr, gas: Option) -> (EthereumContractCall, String) { + fn as_eth_call( + self, + block_ptr: BlockPtr, + gas: Option, + ) -> (EthereumContractCall, String, String) { ( EthereumContractCall { address: self.address, @@ -970,6 +978,7 @@ impl DeclaredCall { gas, }, self.contract_name, + self.label, ) } } @@ -1001,10 +1010,11 @@ impl DecoderHook { block_ptr: &BlockPtr, metrics: Arc, call: DeclaredCall, - ) -> Result { + ) -> Result, MappingError> { let start = Instant::now(); let function_name = call.function.name.clone(); - let (eth_call, contract_name) = call.as_eth_call(block_ptr.clone(), self.eth_call_gas); + let (eth_call, contract_name, label) = + call.as_eth_call(block_ptr.clone(), self.eth_call_gas); let eth_adapter = self.eth_adapters.call_or_cheapest(Some(&NodeCapabilities { archive: true, traces: false, @@ -1025,7 +1035,7 @@ impl DecoderHook { // // For errors, we set the source to 'Rpc' as that's where errors happen let result= match result { - Ok(r) => Ok(if r.is_some() { 0 } else { 1 }), + Ok(r) => Ok(if r.is_some() { None } else { Some(label) }), // Any error reported by the Ethereum node could be due to the block no longer being on // the main chain. This is very unespecific but we don't want to risk failing a @@ -1075,6 +1085,55 @@ impl blockchain::DecoderHook for DecoderHook { runnables: Vec>, metrics: &Arc, ) -> Result>, MappingError> { + /// Log information about failed eth calls. 'Failure' here simply + /// means that the call was reverted; outright errors lead to a real + /// error. For reverted calls, `self.eth_call` returns the label + /// from the manifest for that call. + /// + /// One reason why declared calls can fail is if they are attached + /// to the wrong handler, or if arguments are specified incorrectly. + /// Calls that revert every once in a while might be ok and what the + /// user intended, but we want to clearly log so that users can spot + /// mistakes in their manifest, which will lead to unnecessary eth + /// calls + fn log_results( + logger: &Logger, + results: &[Option], + trigger_count: usize, + elapsed: Duration, + ) { + let calls_count = results.len(); + let fail_count = results.iter().filter(|r| r.is_some()).count(); + + if fail_count > 0 { + let mut counts: Vec<_> = results + .iter() + .filter_map(|r| r.as_ref()) + .counts() + .into_iter() + .collect(); + counts.sort_by_key(|(label, _)| *label); + let counts = counts + .into_iter() + .map(|(label, count)| { + let times = if count == 1 { "time" } else { "times" }; + format!("{label} ({count} {times})") + }) + .join(", "); + error!(logger, "Declared calls failed"; + "triggers" => trigger_count, + "calls_count" => calls_count, + "fail_count" => fail_count, + "calls_ms" => elapsed.as_millis(), + "failures" => format!("[{}]", counts)); + } else { + debug!(logger, "Declared calls"; + "triggers" => trigger_count, + "calls_count" => calls_count, + "calls_ms" => elapsed.as_millis()); + } + } + if ENV_VARS.mappings.disable_declared_calls { return Ok(runnables); } @@ -1124,21 +1183,13 @@ impl blockchain::DecoderHook for DecoderHook { calls }; - let calls_count = calls.len(); - - let fail_count: usize = graph::prelude::futures03::future::try_join_all( + let results = graph::prelude::futures03::future::try_join_all( calls .into_iter() .map(|(metrics, call)| self.eth_call(logger, block_ptr, metrics, call)), ) - .await? - .into_iter() - .sum(); - - // TODO: Remove this logging before merging - if calls_count > 0 { - info!(logger, "After decode hook"; "runnables" => runnables.len(), "calls_count" => calls_count, "fail_count" => fail_count, "calls_ms" => start.elapsed().as_millis()); - } + .await?; + log_results(logger, &results, runnables.len(), start.elapsed()); Ok(runnables) } From b37e583d318af7fd15a2da35ef9534ace2d5c0c3 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 09:00:05 -0800 Subject: [PATCH 35/51] docs: Add declared calls to the manifest spec --- docs/subgraph-manifest.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/subgraph-manifest.md b/docs/subgraph-manifest.md index e595690e9f3..4f766829cfc 100644 --- a/docs/subgraph-manifest.md +++ b/docs/subgraph-manifest.md @@ -74,6 +74,7 @@ The `mapping` field may be one of the following supported mapping manifests: | **event** | *String* | An identifier for an event that will be handled in the mapping script. For Ethereum contracts, this must be the full event signature to distinguish from events that may share the same name. No alias types can be used. For example, uint will not work, uint256 must be used.| | **handler** | *String* | The name of an exported function in the mapping script that should handle the specified event. | | **topic0** | optional *String* | A `0x` prefixed hex string. If provided, events whose topic0 is equal to this value will be processed by the given handler. When topic0 is provided, _only_ the topic0 value will be matched, and not the hash of the event signature. This is useful for processing anonymous events in Solidity, which can have their topic0 set to anything. By default, topic0 is equal to the hash of the event signature. | +| **calls** | optional [*CallDecl*](#153-declaring-calls) | A list of predeclared `eth_calls` that will be made before running the handler | #### 1.5.2.3 CallHandler @@ -95,6 +96,30 @@ The `mapping` field may be one of the following supported mapping manifests: | --- | --- | --- | | **kind** | *String* | The selected block handler filter. Only option for now: `call`: This will only run the handler if the block contains at least one call to the data source contract. | +### 1.5.3 Declaring calls + +_Available from spec version 1.2.0_ + +Declared calls are performed in parallel before the handler is run and can +greatly speed up syncing. Mappings access the call results simply by using +`ethereum.call` from the mappings. The **calls** are a map of key value pairs: + +| Field | Type | Description | +| --- | --- | --- | +| **label** | *String* | A label for the call for error messages etc. | +| **call** | *String* | See below | + +Each call is of the form `[
].()`: + +| Field | Type | Description | +| --- | --- | --- | +| **ABI** | *String* | The name of an ABI from the `abis` section | +| **address** | *Expr* | The address of a contract that follows the `ABI` | +| **function** | *String* | The name of a view function in the contract | +| **args** | *[Expr]* | The arguments to pass to the function | + +The `Expr` can be either `event.address` or `event.params.`. + ## 1.6 Path A path has one field `path`, which either refers to a path of a file on the local dev machine or an [IPLD link](https://github.com/ipld/specs/). From 272aa361cde016cd5e7bb61ad2f54a83d2c35d0b Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 09:53:34 -0800 Subject: [PATCH 36/51] all: Introduce a struct for the data needed for an eth_call --- chain/ethereum/src/buffered_call_cache.rs | 29 +++++++---------- chain/ethereum/src/ethereum_adapter.rs | 27 ++++++++-------- graph/src/components/store/traits.rs | 25 ++++++++++++--- graph/src/data/store/scalar.rs | 8 ++++- store/postgres/src/chain_store.rs | 32 ++++++++----------- store/test-store/tests/postgres/chain_head.rs | 23 ++++--------- 6 files changed, 73 insertions(+), 71 deletions(-) diff --git a/chain/ethereum/src/buffered_call_cache.rs b/chain/ethereum/src/buffered_call_cache.rs index b1dc1801d25..572b8bed9ae 100644 --- a/chain/ethereum/src/buffered_call_cache.rs +++ b/chain/ethereum/src/buffered_call_cache.rs @@ -5,8 +5,8 @@ use std::{ use graph::{ cheap_clone::CheapClone, - components::store::{CallResult, CallSource, EthereumCallCache}, - prelude::{ethabi, BlockPtr, CachedEthereumCall}, + components::store::{CallData, CallResult, CallSource, EthereumCallCache}, + prelude::{BlockPtr, CachedEthereumCall}, slog::{error, Logger}, }; @@ -16,7 +16,7 @@ use graph::{ /// call, the buffer is cleared. pub struct BufferedCallCache { call_cache: Arc, - buffer: Arc), CallResult>>>, + buffer: Arc>>, block: Arc>>, } @@ -41,26 +41,23 @@ impl BufferedCallCache { impl EthereumCallCache for BufferedCallCache { fn get_call( &self, - contract_address: ethabi::Address, - encoded_call: &[u8], + call: &CallData, block: BlockPtr, ) -> Result, graph::prelude::Error> { self.check_block(&block); { let buffer = self.buffer.lock().unwrap(); - if let Some(result) = buffer.get(&(contract_address, encoded_call.to_vec())) { + if let Some(result) = buffer.get(&call) { return Ok(Some((result.clone(), CallSource::Memory))); } } - let result = self - .call_cache - .get_call(contract_address, encoded_call, block)?; + let result = self.call_cache.get_call(&call, block)?; let mut buffer = self.buffer.lock().unwrap(); if let Some((value, _)) = &result { - buffer.insert((contract_address, encoded_call.to_vec()), value.clone()); + buffer.insert(call.cheap_clone(), value.clone()); } Ok(result) } @@ -75,8 +72,7 @@ impl EthereumCallCache for BufferedCallCache { fn set_call( &self, logger: &Logger, - contract_address: ethabi::Address, - encoded_call: Arc>, + call: CallData, block: BlockPtr, return_value: CallResult, ) -> Result<(), graph::prelude::Error> { @@ -88,20 +84,17 @@ impl EthereumCallCache for BufferedCallCache { // cache backed by the database { let mut buffer = self.buffer.lock().unwrap(); - buffer.insert( - (contract_address, encoded_call.to_vec()), - return_value.clone(), - ); + buffer.insert(call.cheap_clone(), return_value.clone()); } let cache = self.call_cache.cheap_clone(); let logger = logger.cheap_clone(); let _ = graph::spawn_blocking_allow_panic(move || { cache - .set_call(&logger, contract_address, encoded_call, block, return_value) + .set_call(&logger, call.cheap_clone(), block, return_value) .map_err(|e| { error!(logger, "BufferedCallCache: call cache set error"; - "contract_address" => format!("{:?}", contract_address), + "contract_address" => format!("{:?}", call.address), "error" => e.to_string()) }) }); diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index c3706201fc3..5b15cf747c4 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -3,6 +3,7 @@ use futures03::{future::BoxFuture, stream::FuturesUnordered}; use graph::blockchain::client::ChainClient; use graph::blockchain::BlockHash; use graph::blockchain::ChainIdentifier; +use graph::components::store::CallData; use graph::components::store::CallResult; use graph::components::store::CallSource; use graph::components::transaction_receipt::LightTransactionReceipt; @@ -458,8 +459,7 @@ impl EthereumAdapter { async fn call( &self, logger: Logger, - contract_address: Address, - call_data: Bytes, + call_data: CallData, block_ptr: BlockPtr, gas: Option, ) -> Result { @@ -491,9 +491,9 @@ impl EthereumAdapter { let logger = logger.cheap_clone(); async move { let req = CallRequest { - to: Some(contract_address), + to: Some(call_data.address), gas: gas.map(|val| web3::types::U256::from(val)), - data: Some(call_data.clone()), + data: Some(Bytes::from(call_data.encoded_call.to_vec())), from: None, gas_price: None, value: None, @@ -1332,22 +1332,25 @@ impl EthereumAdapterTrait for EthereumAdapter { } // Encode the call parameters according to the ABI - let call_data = match call.function.encode_input(&call.args) { - Ok(data) => data, - Err(e) => return Err(EthereumContractCallError::EncodingError(e)), + let call_data = { + let encoded_call = call + .function + .encode_input(&call.args) + .map_err(EthereumContractCallError::EncodingError)?; + CallData::new(call.address, encoded_call) }; trace!(logger, "eth_call"; "fn" => &call.function.name, "address" => hex::encode(call.address), - "data" => hex::encode(&call_data), + "data" => hex::encode(call_data.encoded_call.as_ref()), "block_hash" => call.block_ptr.hash_hex(), "block_number" => call.block_ptr.block_number() ); // Check if we have it cached, if not do the call and cache. let (output, source) = match cache - .get_call(call.address, &call_data, call.block_ptr.clone()) + .get_call(&call_data, call.block_ptr.clone()) .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) .ok() .flatten() @@ -1357,8 +1360,7 @@ impl EthereumAdapterTrait for EthereumAdapter { let result = self .call( logger.clone(), - call.address, - Bytes(call_data.clone()), + call_data.cheap_clone(), call.block_ptr.clone(), call.gas, ) @@ -1366,8 +1368,7 @@ impl EthereumAdapterTrait for EthereumAdapter { let _ = cache .set_call( &logger, - call.address, - Arc::new(call_data), + call_data.cheap_clone(), call.block_ptr.cheap_clone(), result.clone(), ) diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index d8425133964..567b2db48d1 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -574,14 +574,32 @@ impl std::fmt::Display for CallSource { } } +/// The address and encoded name and parms for an `eth_call`. Because we +/// cache this, it gets cloned a lot and needs to remain cheap to clone. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CallData { + pub address: ethabi::Address, + pub encoded_call: Arc, +} + +impl CheapClone for CallData {} + +impl CallData { + pub fn new(address: ethabi::Address, encoded_call: Vec) -> Self { + CallData { + address, + encoded_call: Arc::new(Bytes::from(encoded_call)), + } + } +} + pub trait EthereumCallCache: Send + Sync + 'static { /// Returns the return value of the provided Ethereum call, if present /// in the cache. A return of `None` indicates that we know nothing /// about the call. fn get_call( &self, - contract_address: ethabi::Address, - encoded_call: &[u8], + call: &CallData, block: BlockPtr, ) -> Result, Error>; @@ -593,8 +611,7 @@ pub trait EthereumCallCache: Send + Sync + 'static { fn set_call( &self, logger: &Logger, - contract_address: ethabi::Address, - encoded_call: Arc>, + call: CallData, block: BlockPtr, return_value: CallResult, ) -> Result<(), Error>; diff --git a/graph/src/data/store/scalar.rs b/graph/src/data/store/scalar.rs index 78aa2b1b800..55b6ede03eb 100644 --- a/graph/src/data/store/scalar.rs +++ b/graph/src/data/store/scalar.rs @@ -589,7 +589,7 @@ impl GasSizeOf for BigInt { } /// A byte array that's serialized as a hex string prefixed by `0x`. -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Bytes(Box<[u8]>); impl Deref for Bytes { @@ -678,6 +678,12 @@ impl From> for Bytes { } } +impl AsRef<[u8]> for Bytes { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + impl ToSql for Bytes { fn to_sql<'b>( &'b self, diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index 0fe3ae7407a..b2e1c72ea6f 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -3,7 +3,7 @@ use diesel::prelude::*; use diesel::r2d2::{ConnectionManager, PooledConnection}; use diesel::sql_types::Text; use diesel::{insert_into, update}; -use graph::components::store::{CallResult, CallSource}; +use graph::components::store::{CallData, CallResult, CallSource}; use graph::env::ENV_VARS; use graph::parking_lot::RwLock; use graph::prelude::MetricsRegistry; @@ -23,9 +23,9 @@ use graph::blockchain::{Block, BlockHash, ChainIdentifier}; use graph::cheap_clone::CheapClone; use graph::prelude::web3::types::H256; use graph::prelude::{ - async_trait, ethabi, serde_json as json, transaction_receipt::LightTransactionReceipt, - BlockNumber, BlockPtr, CachedEthereumCall, CancelableError, ChainStore as ChainStoreTrait, - Error, EthereumCallCache, StoreError, + async_trait, serde_json as json, transaction_receipt::LightTransactionReceipt, BlockNumber, + BlockPtr, CachedEthereumCall, CancelableError, ChainStore as ChainStoreTrait, Error, + EthereumCallCache, StoreError, }; use graph::{constraint_violation, ensure}; @@ -2396,11 +2396,10 @@ fn try_parse_timestamp(ts: Option) -> Result, StoreError> { impl EthereumCallCache for ChainStore { fn get_call( &self, - contract_address: ethabi::Address, - encoded_call: &[u8], + call: &CallData, block: BlockPtr, ) -> Result, Error> { - let id = contract_call_id(&contract_address, encoded_call, &block); + let id = contract_call_id(call, &block); let conn = &mut *self.get_conn()?; let return_value = conn.transaction::<_, Error, _>(|conn| { if let Some((return_value, update_accessed_at)) = @@ -2408,7 +2407,7 @@ impl EthereumCallCache for ChainStore { { if update_accessed_at { self.storage - .update_accessed_at(conn, contract_address.as_ref())?; + .update_accessed_at(conn, call.address.as_ref())?; } Ok(Some(return_value)) } else { @@ -2426,8 +2425,7 @@ impl EthereumCallCache for ChainStore { fn set_call( &self, _: &Logger, - contract_address: ethabi::Address, - encoded_call: Arc>, + call: CallData, block: BlockPtr, return_value: CallResult, ) -> Result<(), Error> { @@ -2438,13 +2436,13 @@ impl EthereumCallCache for ChainStore { // where calls first failed and later succeeded return Ok(()); }; - let id = contract_call_id(&contract_address, &encoded_call, &block); + let id = contract_call_id(&call, &block); let conn = &mut *self.get_conn()?; conn.transaction(|conn| { self.storage.set_call( conn, id.as_ref(), - contract_address.as_ref(), + call.address.as_ref(), block.number, &return_value, ) @@ -2455,14 +2453,10 @@ impl EthereumCallCache for ChainStore { /// The id is the hashed encoded_call + contract_address + block hash to uniquely identify the call. /// 256 bits of output, and therefore 128 bits of security against collisions, are needed since this /// could be targeted by a birthday attack. -fn contract_call_id( - contract_address: ðabi::Address, - encoded_call: &[u8], - block: &BlockPtr, -) -> [u8; 32] { +fn contract_call_id(call: &CallData, block: &BlockPtr) -> [u8; 32] { let mut hash = blake3::Hasher::new(); - hash.update(encoded_call); - hash.update(contract_address.as_ref()); + hash.update(&call.encoded_call); + hash.update(call.address.as_ref()); hash.update(block.hash_slice()); *hash.finalize().as_bytes() } diff --git a/store/test-store/tests/postgres/chain_head.rs b/store/test-store/tests/postgres/chain_head.rs index 13e44c8f19d..53b99cdc8b2 100644 --- a/store/test-store/tests/postgres/chain_head.rs +++ b/store/test-store/tests/postgres/chain_head.rs @@ -2,7 +2,7 @@ //! the chain head pointer gets updated in various situations use graph::blockchain::{BlockHash, BlockPtr}; -use graph::components::store::CallResult; +use graph::components::store::{CallData, CallResult}; use graph::data::store::scalar::Bytes; use graph::env::ENV_VARS; use graph::prelude::futures03::executor; @@ -382,49 +382,43 @@ fn eth_call_cache() { let address = H160([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); let call: [u8; 6] = [1, 2, 3, 4, 5, 6]; - let call = Arc::new(call.to_vec()); let return_value: [u8; 3] = [7, 8, 9]; + let call = CallData::new(address, call.to_vec()); store .set_call( &logger, - address, call.cheap_clone(), BLOCK_ONE.block_ptr(), ccr(&return_value), ) .unwrap(); - let ret = store - .get_call(address, &call, GENESIS_BLOCK.block_ptr()) - .unwrap(); + let ret = store.get_call(&call, GENESIS_BLOCK.block_ptr()).unwrap(); assert!(ret.is_none()); let ret = store - .get_call(address, &call, BLOCK_ONE.block_ptr()) + .get_call(&call, BLOCK_ONE.block_ptr()) .unwrap() .unwrap() .0 .unwrap(); assert_eq!(&return_value, ret.as_slice()); - let ret = store - .get_call(address, &call, BLOCK_TWO.block_ptr()) - .unwrap(); + let ret = store.get_call(&call, BLOCK_TWO.block_ptr()).unwrap(); assert!(ret.is_none()); let new_return_value: [u8; 3] = [10, 11, 12]; store .set_call( &logger, - address, call.cheap_clone(), BLOCK_TWO.block_ptr(), ccr(&new_return_value), ) .unwrap(); let ret = store - .get_call(address, &call, BLOCK_TWO.block_ptr()) + .get_call(&call, BLOCK_TWO.block_ptr()) .unwrap() .unwrap() .0 @@ -434,15 +428,12 @@ fn eth_call_cache() { store .set_call( &logger, - address, call.cheap_clone(), BLOCK_THREE.block_ptr(), CallResult::Null, ) .unwrap(); - let ret = store - .get_call(address, &call, BLOCK_THREE.block_ptr()) - .unwrap(); + let ret = store.get_call(&call, BLOCK_THREE.block_ptr()).unwrap(); assert_eq!(None, ret); Ok(()) From 02bec4356e2cbdf44527dca85fb788a881dde7d6 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 10:52:40 -0800 Subject: [PATCH 37/51] all: Move some of the helper structs for handling eth calls around This also renames some of them, but has no functional change --- chain/ethereum/src/adapter.rs | 4 +- chain/ethereum/src/buffered_call_cache.rs | 15 ++-- chain/ethereum/src/data_source.rs | 5 +- chain/ethereum/src/ethereum_adapter.rs | 26 +++---- chain/ethereum/src/runtime/runtime_adapter.rs | 4 +- graph/src/components/store/traits.rs | 74 ++---------------- graph/src/data/store/ethereum.rs | 77 +++++++++++++++++++ store/postgres/src/chain_store.rs | 17 ++-- store/test-store/tests/postgres/chain_head.rs | 10 +-- 9 files changed, 123 insertions(+), 109 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index c934b82ff74..9541e500d7d 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -2,7 +2,7 @@ use anyhow::Error; use ethabi::{Error as ABIError, Function, ParamType, Token}; use futures::Future; use graph::blockchain::ChainIdentifier; -use graph::components::store::CallSource; +use graph::data::store::ethereum::call; use graph::firehose::CallToFilter; use graph::firehose::CombinedFilter; use graph::firehose::LogFilter; @@ -971,7 +971,7 @@ pub trait EthereumAdapter: Send + Sync + 'static { logger: &Logger, call: &EthereumContractCall, cache: Arc, - ) -> Result<(Option>, CallSource), EthereumContractCallError>; + ) -> Result<(Option>, call::Source), EthereumContractCallError>; fn get_balance( &self, diff --git a/chain/ethereum/src/buffered_call_cache.rs b/chain/ethereum/src/buffered_call_cache.rs index 572b8bed9ae..d73248cfe53 100644 --- a/chain/ethereum/src/buffered_call_cache.rs +++ b/chain/ethereum/src/buffered_call_cache.rs @@ -5,7 +5,8 @@ use std::{ use graph::{ cheap_clone::CheapClone, - components::store::{CallData, CallResult, CallSource, EthereumCallCache}, + components::store::EthereumCallCache, + data::store::ethereum::call, prelude::{BlockPtr, CachedEthereumCall}, slog::{error, Logger}, }; @@ -16,7 +17,7 @@ use graph::{ /// call, the buffer is cleared. pub struct BufferedCallCache { call_cache: Arc, - buffer: Arc>>, + buffer: Arc>>, block: Arc>>, } @@ -41,15 +42,15 @@ impl BufferedCallCache { impl EthereumCallCache for BufferedCallCache { fn get_call( &self, - call: &CallData, + call: &call::Request, block: BlockPtr, - ) -> Result, graph::prelude::Error> { + ) -> Result, graph::prelude::Error> { self.check_block(&block); { let buffer = self.buffer.lock().unwrap(); if let Some(result) = buffer.get(&call) { - return Ok(Some((result.clone(), CallSource::Memory))); + return Ok(Some((result.clone(), call::Source::Memory))); } } @@ -72,9 +73,9 @@ impl EthereumCallCache for BufferedCallCache { fn set_call( &self, logger: &Logger, - call: CallData, + call: call::Request, block: BlockPtr, - return_value: CallResult, + return_value: call::Retval, ) -> Result<(), graph::prelude::Error> { self.check_block(&block); diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index bd737dd1567..81c5d9f6763 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -2,9 +2,10 @@ use anyhow::{anyhow, Error}; use anyhow::{ensure, Context}; use graph::blockchain::{BlockPtr, TriggerWithHandler}; use graph::components::metrics::subgraph::SubgraphInstanceMetrics; -use graph::components::store::{CallSource, EthereumCallCache, StoredDynamicDataSource}; +use graph::components::store::{EthereumCallCache, StoredDynamicDataSource}; use graph::components::subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError}; use graph::components::trigger_processor::RunnableTriggers; +use graph::data::store::ethereum::call; use graph::data::value::Word; use graph::data_source::CausalityRegion; use graph::env::ENV_VARS; @@ -1025,7 +1026,7 @@ impl DecoderHook { .await { Ok((result, source)) => (Ok(result), source), - Err(e) => (Err(e), CallSource::Rpc), + Err(e) => (Err(e), call::Source::Rpc), }; // This error analysis is very much modeled on the one in diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 5b15cf747c4..acf78d8eee1 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -3,10 +3,8 @@ use futures03::{future::BoxFuture, stream::FuturesUnordered}; use graph::blockchain::client::ChainClient; use graph::blockchain::BlockHash; use graph::blockchain::ChainIdentifier; -use graph::components::store::CallData; -use graph::components::store::CallResult; -use graph::components::store::CallSource; use graph::components::transaction_receipt::LightTransactionReceipt; +use graph::data::store::ethereum::call; use graph::data::store::scalar; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::data::subgraph::API_VERSION_0_0_7; @@ -459,16 +457,16 @@ impl EthereumAdapter { async fn call( &self, logger: Logger, - call_data: CallData, + call_data: call::Request, block_ptr: BlockPtr, gas: Option, - ) -> Result { + ) -> Result { fn reverted( logger: &Logger, reason: &str, - ) -> Result { + ) -> Result { info!(logger, "Contract call reverted"; "reason" => reason); - Ok(CallResult::Null) + Ok(call::Retval::Null) } let web3 = self.web3.clone(); @@ -564,7 +562,7 @@ impl EthereumAdapter { match result { // A successful response. - Ok(bytes) => Ok(CallResult::Value(scalar::Bytes::from(bytes))), + Ok(bytes) => Ok(call::Retval::Value(scalar::Bytes::from(bytes))), // Check for Geth revert. Err(web3::Error::Rpc(rpc_error)) @@ -1316,7 +1314,7 @@ impl EthereumAdapterTrait for EthereumAdapter { logger: &Logger, call: &EthereumContractCall, cache: Arc, - ) -> Result<(Option>, CallSource), EthereumContractCallError> { + ) -> Result<(Option>, call::Source), EthereumContractCallError> { // Emit custom error for type mismatches. for (token, kind) in call .args @@ -1337,7 +1335,7 @@ impl EthereumAdapterTrait for EthereumAdapter { .function .encode_input(&call.args) .map_err(EthereumContractCallError::EncodingError)?; - CallData::new(call.address, encoded_call) + call::Request::new(call.address, encoded_call) }; trace!(logger, "eth_call"; @@ -1378,12 +1376,12 @@ impl EthereumAdapterTrait for EthereumAdapter { "error" => e.to_string()) }); - (result, CallSource::Rpc) + (result, call::Source::Rpc) } }; // Decode the return values according to the ABI - use CallResult::*; + use call::Retval::*; match output { Value(output) => match call.function.decode_output(&output) { Ok(tokens) => Ok((Some(tokens), source)), @@ -1392,7 +1390,7 @@ impl EthereumAdapterTrait for EthereumAdapter { // decode an argument, that's a revert, so the same goes for the output. let reason = format!("failed to decode output: {}", e); info!(logger, "Contract call reverted"; "reason" => reason); - Ok((None, CallSource::Rpc)) + Ok((None, call::Source::Rpc)) } }, Null => { @@ -1400,7 +1398,7 @@ impl EthereumAdapterTrait for EthereumAdapter { // that the contract actually returned an empty response. A view call is meant // to return something, so we treat empty responses the same as reverts. info!(logger, "Contract call reverted"; "reason" => "empty response"); - Ok((None, CallSource::Rpc)) + Ok((None, call::Source::Rpc)) } } } diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index 51cfb9ef2e0..80b40218e8f 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -10,8 +10,8 @@ use crate::{ use anyhow::{anyhow, Context, Error}; use blockchain::HostFn; use graph::blockchain::ChainIdentifier; -use graph::components::store::CallSource; use graph::components::subgraph::HostMetrics; +use graph::data::store::ethereum::call; use graph::data::store::scalar::BigInt; use graph::data::subgraph::API_VERSION_0_0_9; use graph::prelude::web3::types::H160; @@ -256,7 +256,7 @@ fn eth_call( let (result, source) = match graph::block_on(eth_adapter.contract_call(&logger1, &call, call_cache)) { Ok((result, source)) => (Ok(result), source), - Err(e) => (Err(e), CallSource::Rpc), + Err(e) => (Err(e), call::Source::Rpc), }; let result = match result { Ok(res) => Ok(res), diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index 567b2db48d1..ba76cd1fb79 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -13,6 +13,7 @@ use crate::components::subgraph::SubgraphVersionSwitchingMode; use crate::components::transaction_receipt; use crate::components::versions::ApiVersion; use crate::data::query::Trace; +use crate::data::store::ethereum::call; use crate::data::store::QueryObject; use crate::data::subgraph::{status, DeploymentFeatures}; use crate::data::{query::QueryTarget, subgraph::schema::*}; @@ -528,80 +529,15 @@ pub trait ChainStore: Send + Sync + 'static { fn chain_identifier(&self) -> &ChainIdentifier; } -/// The result of an ethereum call. `Null` indicates that we made the call -/// but didn't get a value back (including when we get the error 'call -/// reverted') -#[derive(Debug, Clone, PartialEq)] -pub enum CallResult { - Null, - Value(Bytes), -} - -impl CallResult { - pub fn unwrap(self) -> Bytes { - use CallResult::*; - match self { - Value(val) => val, - Null => panic!("called `CachedCallResult::unwrap()` on a `Null` value"), - } - } -} - -/// Indication of where the result of an ethereum call comes from. We -/// unfortunately need that so we can avoid double-counting declared calls -/// as they are accessed as normal eth calls and we'd count them twice -/// without this. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum CallSource { - Memory, - Store, - Rpc, -} - -impl CallSource { - pub fn observe(&self) -> bool { - matches!(self, CallSource::Rpc | CallSource::Store) - } -} - -impl std::fmt::Display for CallSource { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - CallSource::Memory => write!(f, "memory"), - CallSource::Store => write!(f, "store"), - CallSource::Rpc => write!(f, "rpc"), - } - } -} - -/// The address and encoded name and parms for an `eth_call`. Because we -/// cache this, it gets cloned a lot and needs to remain cheap to clone. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct CallData { - pub address: ethabi::Address, - pub encoded_call: Arc, -} - -impl CheapClone for CallData {} - -impl CallData { - pub fn new(address: ethabi::Address, encoded_call: Vec) -> Self { - CallData { - address, - encoded_call: Arc::new(Bytes::from(encoded_call)), - } - } -} - pub trait EthereumCallCache: Send + Sync + 'static { /// Returns the return value of the provided Ethereum call, if present /// in the cache. A return of `None` indicates that we know nothing /// about the call. fn get_call( &self, - call: &CallData, + call: &call::Request, block: BlockPtr, - ) -> Result, Error>; + ) -> Result, Error>; /// Returns all cached calls for a given `block`. This method does *not* /// update the last access time of the returned cached calls. @@ -611,9 +547,9 @@ pub trait EthereumCallCache: Send + Sync + 'static { fn set_call( &self, logger: &Logger, - call: CallData, + call: call::Request, block: BlockPtr, - return_value: CallResult, + return_value: call::Retval, ) -> Result<(), Error>; } diff --git a/graph/src/data/store/ethereum.rs b/graph/src/data/store/ethereum.rs index 7bbf2f17542..84e7fcc8229 100644 --- a/graph/src/data/store/ethereum.rs +++ b/graph/src/data/store/ethereum.rs @@ -37,3 +37,80 @@ impl From for Value { Value::BigInt(BigInt::from(n)) } } + +/// Helper structs for dealing with ethereum calls +pub mod call { + use std::sync::Arc; + + use crate::data::store::scalar::Bytes; + + use super::CheapClone; + + /// The return value of an ethereum call. `Null` indicates that we made + /// the call but didn't get a value back (including when we get the + /// error 'call reverted') + #[derive(Debug, Clone, PartialEq)] + pub enum Retval { + Null, + Value(Bytes), + } + + impl Retval { + pub fn unwrap(self) -> Bytes { + use Retval::*; + match self { + Value(val) => val, + Null => panic!("called `call::Retval::unwrap()` on a `Null` value"), + } + } + } + + /// Indication of where the result of an ethereum call comes from. We + /// unfortunately need that so we can avoid double-counting declared calls + /// as they are accessed as normal eth calls and we'd count them twice + /// without this. + #[derive(Debug, Clone, Copy, PartialEq)] + pub enum Source { + Memory, + Store, + Rpc, + } + + impl Source { + /// Return `true` if calls from this source should be observed, + /// i.e., counted as actual calls + pub fn observe(&self) -> bool { + matches!(self, Source::Rpc | Source::Store) + } + } + + impl std::fmt::Display for Source { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Source::Memory => write!(f, "memory"), + Source::Store => write!(f, "store"), + Source::Rpc => write!(f, "rpc"), + } + } + } + + /// The address and encoded name and parms for an `eth_call`, the raw + /// ingredients to make an `eth_call` request. Because we cache this, it + /// gets cloned a lot and needs to remain cheap to clone. + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + pub struct Request { + pub address: ethabi::Address, + pub encoded_call: Arc, + } + + impl CheapClone for Request {} + + impl Request { + pub fn new(address: ethabi::Address, encoded_call: Vec) -> Self { + Request { + address, + encoded_call: Arc::new(Bytes::from(encoded_call)), + } + } + } +} diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index b2e1c72ea6f..633b112adca 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -3,7 +3,7 @@ use diesel::prelude::*; use diesel::r2d2::{ConnectionManager, PooledConnection}; use diesel::sql_types::Text; use diesel::{insert_into, update}; -use graph::components::store::{CallData, CallResult, CallSource}; +use graph::data::store::ethereum::call; use graph::env::ENV_VARS; use graph::parking_lot::RwLock; use graph::prelude::MetricsRegistry; @@ -2396,9 +2396,9 @@ fn try_parse_timestamp(ts: Option) -> Result, StoreError> { impl EthereumCallCache for ChainStore { fn get_call( &self, - call: &CallData, + call: &call::Request, block: BlockPtr, - ) -> Result, Error> { + ) -> Result, Error> { let id = contract_call_id(call, &block); let conn = &mut *self.get_conn()?; let return_value = conn.transaction::<_, Error, _>(|conn| { @@ -2414,7 +2414,8 @@ impl EthereumCallCache for ChainStore { Ok(None) } })?; - Ok(return_value.map(|return_value| (CallResult::Value(return_value), CallSource::Store))) + Ok(return_value + .map(|return_value| (call::Retval::Value(return_value), call::Source::Store))) } fn get_calls_in_block(&self, block: BlockPtr) -> Result, Error> { @@ -2425,11 +2426,11 @@ impl EthereumCallCache for ChainStore { fn set_call( &self, _: &Logger, - call: CallData, + call: call::Request, block: BlockPtr, - return_value: CallResult, + return_value: call::Retval, ) -> Result<(), Error> { - let CallResult::Value(return_value) = return_value else { + let call::Retval::Value(return_value) = return_value else { // We do not want to cache unsuccessful calls as some RPC nodes // have weird behavior near the chain head. The details are lost // to time, but we had issues with some RPC clients in the past @@ -2453,7 +2454,7 @@ impl EthereumCallCache for ChainStore { /// The id is the hashed encoded_call + contract_address + block hash to uniquely identify the call. /// 256 bits of output, and therefore 128 bits of security against collisions, are needed since this /// could be targeted by a birthday attack. -fn contract_call_id(call: &CallData, block: &BlockPtr) -> [u8; 32] { +fn contract_call_id(call: &call::Request, block: &BlockPtr) -> [u8; 32] { let mut hash = blake3::Hasher::new(); hash.update(&call.encoded_call); hash.update(call.address.as_ref()); diff --git a/store/test-store/tests/postgres/chain_head.rs b/store/test-store/tests/postgres/chain_head.rs index 53b99cdc8b2..7d330252074 100644 --- a/store/test-store/tests/postgres/chain_head.rs +++ b/store/test-store/tests/postgres/chain_head.rs @@ -2,7 +2,7 @@ //! the chain head pointer gets updated in various situations use graph::blockchain::{BlockHash, BlockPtr}; -use graph::components::store::{CallData, CallResult}; +use graph::data::store::ethereum::call; use graph::data::store::scalar::Bytes; use graph::env::ENV_VARS; use graph::prelude::futures03::executor; @@ -376,15 +376,15 @@ fn eth_call_cache() { run_test(chain, |store, _| { let logger = LOGGER.cheap_clone(); - fn ccr(value: &[u8]) -> CallResult { - CallResult::Value(Bytes::from(value)) + fn ccr(value: &[u8]) -> call::Retval { + call::Retval::Value(Bytes::from(value)) } let address = H160([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]); let call: [u8; 6] = [1, 2, 3, 4, 5, 6]; let return_value: [u8; 3] = [7, 8, 9]; - let call = CallData::new(address, call.to_vec()); + let call = call::Request::new(address, call.to_vec()); store .set_call( &logger, @@ -430,7 +430,7 @@ fn eth_call_cache() { &logger, call.cheap_clone(), BLOCK_THREE.block_ptr(), - CallResult::Null, + call::Retval::Null, ) .unwrap(); let ret = store.get_call(&call, BLOCK_THREE.block_ptr()).unwrap(); From e4aa6f0148431b80c84dba3da19d3c285273d1bd Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 11:05:47 -0800 Subject: [PATCH 38/51] all: Struct for return value and source for a eth_call response --- chain/ethereum/src/buffered_call_cache.rs | 18 ++++++++++++----- chain/ethereum/src/ethereum_adapter.rs | 20 +++++++++++-------- graph/src/components/store/traits.rs | 2 +- graph/src/data/store/ethereum.rs | 16 +++++++++++++++ store/postgres/src/chain_store.rs | 14 +++++++------ store/test-store/tests/postgres/chain_head.rs | 4 ++-- 6 files changed, 52 insertions(+), 22 deletions(-) diff --git a/chain/ethereum/src/buffered_call_cache.rs b/chain/ethereum/src/buffered_call_cache.rs index d73248cfe53..c0c6e69f05f 100644 --- a/chain/ethereum/src/buffered_call_cache.rs +++ b/chain/ethereum/src/buffered_call_cache.rs @@ -44,21 +44,29 @@ impl EthereumCallCache for BufferedCallCache { &self, call: &call::Request, block: BlockPtr, - ) -> Result, graph::prelude::Error> { + ) -> Result, graph::prelude::Error> { self.check_block(&block); { let buffer = self.buffer.lock().unwrap(); - if let Some(result) = buffer.get(&call) { - return Ok(Some((result.clone(), call::Source::Memory))); + if let Some(result) = buffer.get(call) { + return Ok(Some( + call.cheap_clone() + .response(result.clone(), call::Source::Memory), + )); } } let result = self.call_cache.get_call(&call, block)?; let mut buffer = self.buffer.lock().unwrap(); - if let Some((value, _)) = &result { - buffer.insert(call.cheap_clone(), value.clone()); + if let Some(call::Response { + retval, + req: _, + source: _, + }) = &result + { + buffer.insert(call.cheap_clone(), retval.clone()); } Ok(result) } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index acf78d8eee1..0b4b8fc6507 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1330,7 +1330,7 @@ impl EthereumAdapterTrait for EthereumAdapter { } // Encode the call parameters according to the ABI - let call_data = { + let req = { let encoded_call = call .function .encode_input(&call.args) @@ -1341,14 +1341,18 @@ impl EthereumAdapterTrait for EthereumAdapter { trace!(logger, "eth_call"; "fn" => &call.function.name, "address" => hex::encode(call.address), - "data" => hex::encode(call_data.encoded_call.as_ref()), + "data" => hex::encode(req.encoded_call.as_ref()), "block_hash" => call.block_ptr.hash_hex(), "block_number" => call.block_ptr.block_number() ); // Check if we have it cached, if not do the call and cache. - let (output, source) = match cache - .get_call(&call_data, call.block_ptr.clone()) + let call::Response { + retval, + source, + req: _, + } = match cache + .get_call(&req, call.block_ptr.clone()) .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) .ok() .flatten() @@ -1358,7 +1362,7 @@ impl EthereumAdapterTrait for EthereumAdapter { let result = self .call( logger.clone(), - call_data.cheap_clone(), + req.cheap_clone(), call.block_ptr.clone(), call.gas, ) @@ -1366,7 +1370,7 @@ impl EthereumAdapterTrait for EthereumAdapter { let _ = cache .set_call( &logger, - call_data.cheap_clone(), + req.cheap_clone(), call.block_ptr.cheap_clone(), result.clone(), ) @@ -1376,13 +1380,13 @@ impl EthereumAdapterTrait for EthereumAdapter { "error" => e.to_string()) }); - (result, call::Source::Rpc) + req.response(result, call::Source::Rpc) } }; // Decode the return values according to the ABI use call::Retval::*; - match output { + match retval { Value(output) => match call.function.decode_output(&output) { Ok(tokens) => Ok((Some(tokens), source)), Err(e) => { diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index ba76cd1fb79..1c0d31d1269 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -537,7 +537,7 @@ pub trait EthereumCallCache: Send + Sync + 'static { &self, call: &call::Request, block: BlockPtr, - ) -> Result, Error>; + ) -> Result, Error>; /// Returns all cached calls for a given `block`. This method does *not* /// update the last access time of the returned cached calls. diff --git a/graph/src/data/store/ethereum.rs b/graph/src/data/store/ethereum.rs index 84e7fcc8229..c39b58926ba 100644 --- a/graph/src/data/store/ethereum.rs +++ b/graph/src/data/store/ethereum.rs @@ -112,5 +112,21 @@ pub mod call { encoded_call: Arc::new(Bytes::from(encoded_call)), } } + + /// Create a response struct for this request + pub fn response(self, retval: Retval, source: Source) -> Response { + Response { + req: self, + retval, + source, + } + } + } + + #[derive(Debug, Clone, PartialEq)] + pub struct Response { + pub req: Request, + pub retval: Retval, + pub source: Source, } } diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index 633b112adca..482ae88adcf 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -2396,10 +2396,10 @@ fn try_parse_timestamp(ts: Option) -> Result, StoreError> { impl EthereumCallCache for ChainStore { fn get_call( &self, - call: &call::Request, + req: &call::Request, block: BlockPtr, - ) -> Result, Error> { - let id = contract_call_id(call, &block); + ) -> Result, Error> { + let id = contract_call_id(req, &block); let conn = &mut *self.get_conn()?; let return_value = conn.transaction::<_, Error, _>(|conn| { if let Some((return_value, update_accessed_at)) = @@ -2407,15 +2407,17 @@ impl EthereumCallCache for ChainStore { { if update_accessed_at { self.storage - .update_accessed_at(conn, call.address.as_ref())?; + .update_accessed_at(conn, req.address.as_ref())?; } Ok(Some(return_value)) } else { Ok(None) } })?; - Ok(return_value - .map(|return_value| (call::Retval::Value(return_value), call::Source::Store))) + Ok(return_value.map(|return_value| { + req.cheap_clone() + .response(call::Retval::Value(return_value), call::Source::Store) + })) } fn get_calls_in_block(&self, block: BlockPtr) -> Result, Error> { diff --git a/store/test-store/tests/postgres/chain_head.rs b/store/test-store/tests/postgres/chain_head.rs index 7d330252074..a0f61595139 100644 --- a/store/test-store/tests/postgres/chain_head.rs +++ b/store/test-store/tests/postgres/chain_head.rs @@ -401,7 +401,7 @@ fn eth_call_cache() { .get_call(&call, BLOCK_ONE.block_ptr()) .unwrap() .unwrap() - .0 + .retval .unwrap(); assert_eq!(&return_value, ret.as_slice()); @@ -421,7 +421,7 @@ fn eth_call_cache() { .get_call(&call, BLOCK_TWO.block_ptr()) .unwrap() .unwrap() - .0 + .retval .unwrap(); assert_eq!(&new_return_value, ret.as_slice()); From 2703680abacca299842462f9afff7e4a2c7d2791 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 12:11:18 -0800 Subject: [PATCH 39/51] chain/ethereum: Factor out generating the eth_call request --- chain/ethereum/src/ethereum_adapter.rs | 62 +++++++++++++++----------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 0b4b8fc6507..9f2d8b0f0bc 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1315,36 +1315,44 @@ impl EthereumAdapterTrait for EthereumAdapter { call: &EthereumContractCall, cache: Arc, ) -> Result<(Option>, call::Source), EthereumContractCallError> { - // Emit custom error for type mismatches. - for (token, kind) in call - .args - .iter() - .zip(call.function.inputs.iter().map(|p| &p.kind)) - { - if !token.type_check(kind) { - return Err(EthereumContractCallError::TypeError( - token.clone(), - kind.clone(), - )); + fn as_req( + logger: &Logger, + call: &EthereumContractCall, + ) -> Result { + // Emit custom error for type mismatches. + for (token, kind) in call + .args + .iter() + .zip(call.function.inputs.iter().map(|p| &p.kind)) + { + if !token.type_check(kind) { + return Err(EthereumContractCallError::TypeError( + token.clone(), + kind.clone(), + )); + } } - } - // Encode the call parameters according to the ABI - let req = { - let encoded_call = call - .function - .encode_input(&call.args) - .map_err(EthereumContractCallError::EncodingError)?; - call::Request::new(call.address, encoded_call) - }; + // Encode the call parameters according to the ABI + let req = { + let encoded_call = call + .function + .encode_input(&call.args) + .map_err(EthereumContractCallError::EncodingError)?; + call::Request::new(call.address, encoded_call) + }; - trace!(logger, "eth_call"; - "fn" => &call.function.name, - "address" => hex::encode(call.address), - "data" => hex::encode(req.encoded_call.as_ref()), - "block_hash" => call.block_ptr.hash_hex(), - "block_number" => call.block_ptr.block_number() - ); + trace!(logger, "eth_call"; + "fn" => &call.function.name, + "address" => hex::encode(call.address), + "data" => hex::encode(req.encoded_call.as_ref()), + "block_hash" => call.block_ptr.hash_hex(), + "block_number" => call.block_ptr.block_number() + ); + Ok(req) + } + + let req = as_req(logger, call)?; // Check if we have it cached, if not do the call and cache. let call::Response { From cb737f95babb1c0ac86779156e3ebd39cdfd6f42 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 14:01:12 -0800 Subject: [PATCH 40/51] chain/ethereum: Further factor EthereumAdapter.contract_call --- chain/ethereum/src/ethereum_adapter.rs | 115 ++++++++++++++----------- 1 file changed, 65 insertions(+), 50 deletions(-) diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 9f2d8b0f0bc..7ed87ba7411 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -617,6 +617,36 @@ impl EthereumAdapter { .await } + async fn call_and_cache( + &self, + logger: &Logger, + call: &EthereumContractCall, + req: call::Request, + cache: Arc, + ) -> Result { + let result = self + .call( + logger.clone(), + req.cheap_clone(), + call.block_ptr.clone(), + call.gas, + ) + .await?; + let _ = cache + .set_call( + &logger, + req.cheap_clone(), + call.block_ptr.cheap_clone(), + result.clone(), + ) + .map_err(|e| { + error!(logger, "EthereumAdapter: call cache set error"; + "contract_address" => format!("{:?}", req.address), + "error" => e.to_string()) + }); + + Ok(req.response(result, call::Source::Rpc)) + } /// Request blocks by hash through JSON-RPC. fn load_blocks_rpc( &self, @@ -1352,67 +1382,52 @@ impl EthereumAdapterTrait for EthereumAdapter { Ok(req) } + fn decode( + logger: &Logger, + resp: call::Response, + call: &EthereumContractCall, + ) -> Result<(Option>, call::Source), EthereumContractCallError> { + let call::Response { + retval, + source, + req: _, + } = resp; + use call::Retval::*; + match retval { + Value(output) => match call.function.decode_output(&output) { + Ok(tokens) => Ok((Some(tokens), source)), + Err(e) => { + // Decode failures are reverts. The reasoning is that if Solidity fails to + // decode an argument, that's a revert, so the same goes for the output. + let reason = format!("failed to decode output: {}", e); + info!(logger, "Contract call reverted"; "reason" => reason); + Ok((None, call::Source::Rpc)) + } + }, + Null => { + // We got a `0x` response. For old Geth, this can mean a revert. It can also be + // that the contract actually returned an empty response. A view call is meant + // to return something, so we treat empty responses the same as reverts. + info!(logger, "Contract call reverted"; "reason" => "empty response"); + Ok((None, call::Source::Rpc)) + } + } + } + let req = as_req(logger, call)?; // Check if we have it cached, if not do the call and cache. - let call::Response { - retval, - source, - req: _, - } = match cache + let resp = match cache .get_call(&req, call.block_ptr.clone()) .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) .ok() .flatten() { Some(result) => result, - None => { - let result = self - .call( - logger.clone(), - req.cheap_clone(), - call.block_ptr.clone(), - call.gas, - ) - .await?; - let _ = cache - .set_call( - &logger, - req.cheap_clone(), - call.block_ptr.cheap_clone(), - result.clone(), - ) - .map_err(|e| { - error!(logger, "EthereumAdapter: call cache set error"; - "contract_address" => format!("{:?}", call.address), - "error" => e.to_string()) - }); - - req.response(result, call::Source::Rpc) - } + None => self.call_and_cache(logger, call, req, cache).await?, }; - // Decode the return values according to the ABI - use call::Retval::*; - match retval { - Value(output) => match call.function.decode_output(&output) { - Ok(tokens) => Ok((Some(tokens), source)), - Err(e) => { - // Decode failures are reverts. The reasoning is that if Solidity fails to - // decode an argument, that's a revert, so the same goes for the output. - let reason = format!("failed to decode output: {}", e); - info!(logger, "Contract call reverted"; "reason" => reason); - Ok((None, call::Source::Rpc)) - } - }, - Null => { - // We got a `0x` response. For old Geth, this can mean a revert. It can also be - // that the contract actually returned an empty response. A view call is meant - // to return something, so we treat empty responses the same as reverts. - info!(logger, "Contract call reverted"; "reason" => "empty response"); - Ok((None, call::Source::Rpc)) - } - } + decode(logger, resp, call) } /// Load Ethereum blocks in bulk, returning results as they come back as a Stream. From 70f719fd0a21ff98567c3aa7b0c2c1fef7dd8d2f Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 14:22:00 -0800 Subject: [PATCH 41/51] chain, graph, store: Relate call requests to calls via an index We need to relate requests to EthereumCalls that we were given even when the requests are reshuffled from the initial calls --- chain/ethereum/src/ethereum_adapter.rs | 63 ++++++++++++++----- graph/src/data/store/ethereum.rs | 30 ++++++++- store/test-store/tests/postgres/chain_head.rs | 2 +- 3 files changed, 76 insertions(+), 19 deletions(-) diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 7ed87ba7411..03cc68f7a19 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1342,12 +1342,13 @@ impl EthereumAdapterTrait for EthereumAdapter { async fn contract_call( &self, logger: &Logger, - call: &EthereumContractCall, + inp_call: &EthereumContractCall, cache: Arc, ) -> Result<(Option>, call::Source), EthereumContractCallError> { fn as_req( logger: &Logger, call: &EthereumContractCall, + index: u32, ) -> Result { // Emit custom error for type mismatches. for (token, kind) in call @@ -1369,7 +1370,7 @@ impl EthereumAdapterTrait for EthereumAdapter { .function .encode_input(&call.args) .map_err(EthereumContractCallError::EncodingError)?; - call::Request::new(call.address, encoded_call) + call::Request::new(call.address, encoded_call, index) }; trace!(logger, "eth_call"; @@ -1414,20 +1415,52 @@ impl EthereumAdapterTrait for EthereumAdapter { } } - let req = as_req(logger, call)?; - - // Check if we have it cached, if not do the call and cache. - let resp = match cache - .get_call(&req, call.block_ptr.clone()) - .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) - .ok() - .flatten() - { - Some(result) => result, - None => self.call_and_cache(logger, call, req, cache).await?, - }; + let calls = vec![inp_call]; + let reqs: Vec<_> = calls + .iter() + .enumerate() + .map(|(index, call)| as_req(logger, call, index as u32)) + .collect::>()?; + + let mut resps = Vec::new(); + let mut missing = Vec::new(); + for req in reqs { + let call = &calls[req.index as usize]; + match cache + .get_call(&req, call.block_ptr.clone()) + .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) + .ok() + .flatten() + { + Some(resp) => { + resps.push(resp); + } + None => { + missing.push(req); + } + } + } + + for req in missing { + let call = calls[req.index as usize]; + let resp = self + .call_and_cache(logger, call, req, cache.clone()) + .await?; + resps.push(resp); + } - decode(logger, resp, call) + // unwrap: we either looked up the response from the cache, or it + // went into `missing`, in which case the previous loop would have + // pushed it into `resps`. Remember that for now, we only ever have + // one call. + resps + .into_iter() + .map(|resp| { + let call = calls[resp.req.index as usize]; + decode(logger, resp, call) + }) + .next() + .unwrap() } /// Load Ethereum blocks in bulk, returning results as they come back as a Stream. diff --git a/graph/src/data/store/ethereum.rs b/graph/src/data/store/ethereum.rs index c39b58926ba..6a277583de4 100644 --- a/graph/src/data/store/ethereum.rs +++ b/graph/src/data/store/ethereum.rs @@ -97,19 +97,27 @@ pub mod call { /// The address and encoded name and parms for an `eth_call`, the raw /// ingredients to make an `eth_call` request. Because we cache this, it /// gets cloned a lot and needs to remain cheap to clone. - #[derive(Debug, Clone, PartialEq, Eq, Hash)] + /// + /// For equality and hashing, we only consider the address and the + /// encoded call as the index is set by the caller and has no influence + /// on the call's return value + #[derive(Debug, Clone)] pub struct Request { pub address: ethabi::Address, pub encoded_call: Arc, + /// The index is set by the caller and is used to identify the + /// request in related data structures that the caller might have + pub index: u32, } impl CheapClone for Request {} impl Request { - pub fn new(address: ethabi::Address, encoded_call: Vec) -> Self { + pub fn new(address: ethabi::Address, encoded_call: Vec, index: u32) -> Self { Request { address, encoded_call: Arc::new(Bytes::from(encoded_call)), + index, } } @@ -123,7 +131,23 @@ pub mod call { } } - #[derive(Debug, Clone, PartialEq)] + impl PartialEq for Request { + fn eq(&self, other: &Self) -> bool { + self.address == other.address + && self.encoded_call.as_ref() == other.encoded_call.as_ref() + } + } + + impl Eq for Request {} + + impl std::hash::Hash for Request { + fn hash(&self, state: &mut H) { + self.address.hash(state); + self.encoded_call.as_ref().hash(state); + } + } + + #[derive(Debug, PartialEq)] pub struct Response { pub req: Request, pub retval: Retval, diff --git a/store/test-store/tests/postgres/chain_head.rs b/store/test-store/tests/postgres/chain_head.rs index a0f61595139..230d3ceaa71 100644 --- a/store/test-store/tests/postgres/chain_head.rs +++ b/store/test-store/tests/postgres/chain_head.rs @@ -384,7 +384,7 @@ fn eth_call_cache() { let call: [u8; 6] = [1, 2, 3, 4, 5, 6]; let return_value: [u8; 3] = [7, 8, 9]; - let call = call::Request::new(address, call.to_vec()); + let call = call::Request::new(address, call.to_vec(), 0); store .set_call( &logger, From e6c09c4d2a8f7d096a1248f73deebaa985d7809a Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 15:05:07 -0800 Subject: [PATCH 42/51] chain/ethereum: Return responses in the right order --- chain/ethereum/src/adapter.rs | 2 ++ chain/ethereum/src/ethereum_adapter.rs | 25 +++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index 9541e500d7d..c1a869ca71c 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -66,6 +66,8 @@ pub enum EthereumContractCallError { Web3Error(web3::Error), #[error("ethereum node took too long to perform call")] Timeout, + #[error("internal error: {0}")] + Internal(String), } #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 03cc68f7a19..fc23c7bea8c 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1449,18 +1449,23 @@ impl EthereumAdapterTrait for EthereumAdapter { resps.push(resp); } - // unwrap: we either looked up the response from the cache, or it - // went into `missing`, in which case the previous loop would have - // pushed it into `resps`. Remember that for now, we only ever have - // one call. - resps + // If we make it here, we have a response for every call. + debug_assert_eq!(resps.len(), calls.len()); + + // Bring the responses into the same order as the calls + resps.sort_by_key(|resp| resp.req.index); + + let mut decoded: Vec<_> = resps .into_iter() - .map(|resp| { - let call = calls[resp.req.index as usize]; - decode(logger, resp, call) + .map(|res| { + let call = &calls[res.req.index as usize]; + decode(logger, res, call) }) - .next() - .unwrap() + .collect::>()?; + + // unwrap: we started with one call, so we also have one response + let res = decoded.pop().unwrap(); + Ok(res) } /// Load Ethereum blocks in bulk, returning results as they come back as a Stream. From c8c8161021daf2d933412a4afcef402510869c47 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 15:11:23 -0800 Subject: [PATCH 43/51] chain/ethereum: Add an EthereumAdapter.contract_calls --- chain/ethereum/src/adapter.rs | 9 +++++++++ chain/ethereum/src/ethereum_adapter.rs | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index c1a869ca71c..023af94cb4d 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -975,6 +975,15 @@ pub trait EthereumAdapter: Send + Sync + 'static { cache: Arc, ) -> Result<(Option>, call::Source), EthereumContractCallError>; + /// Make multiple contract calls in a single batch. The returned `Vec` + /// has results in the same order as the calls in `calls` on input + async fn contract_calls( + &self, + logger: &Logger, + calls: &[&EthereumContractCall], + cache: Arc, + ) -> Result>, call::Source)>, EthereumContractCallError>; + fn get_balance( &self, logger: &Logger, diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index fc23c7bea8c..326db54d2c0 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1345,6 +1345,17 @@ impl EthereumAdapterTrait for EthereumAdapter { inp_call: &EthereumContractCall, cache: Arc, ) -> Result<(Option>, call::Source), EthereumContractCallError> { + let mut result = self.contract_calls(logger, &[inp_call], cache).await?; + // unwrap: self.contract_calls returns as many results as there were calls + Ok(result.pop().unwrap()) + } + + async fn contract_calls( + &self, + logger: &Logger, + calls: &[&EthereumContractCall], + cache: Arc, + ) -> Result>, call::Source)>, EthereumContractCallError> { fn as_req( logger: &Logger, call: &EthereumContractCall, @@ -1415,7 +1426,6 @@ impl EthereumAdapterTrait for EthereumAdapter { } } - let calls = vec![inp_call]; let reqs: Vec<_> = calls .iter() .enumerate() @@ -1455,7 +1465,7 @@ impl EthereumAdapterTrait for EthereumAdapter { // Bring the responses into the same order as the calls resps.sort_by_key(|resp| resp.req.index); - let mut decoded: Vec<_> = resps + let decoded: Vec<_> = resps .into_iter() .map(|res| { let call = &calls[res.req.index as usize]; @@ -1463,9 +1473,7 @@ impl EthereumAdapterTrait for EthereumAdapter { }) .collect::>()?; - // unwrap: we started with one call, so we also have one response - let res = decoded.pop().unwrap(); - Ok(res) + Ok(decoded) } /// Load Ethereum blocks in bulk, returning results as they come back as a Stream. From 73e8e6887b2f5fbaa6131f23f4378f52c9fc1588 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 11:59:53 -0800 Subject: [PATCH 44/51] chain, graph, store: Lookup eth_calls in bulk --- chain/ethereum/src/buffered_call_cache.rs | 48 ++++++++++-- graph/src/components/store/traits.rs | 9 +++ store/postgres/src/chain_store.rs | 90 +++++++++++++++++++++++ 3 files changed, 139 insertions(+), 8 deletions(-) diff --git a/chain/ethereum/src/buffered_call_cache.rs b/chain/ethereum/src/buffered_call_cache.rs index c0c6e69f05f..8a51bd9a0a4 100644 --- a/chain/ethereum/src/buffered_call_cache.rs +++ b/chain/ethereum/src/buffered_call_cache.rs @@ -37,6 +37,14 @@ impl BufferedCallCache { self.buffer.lock().unwrap().clear(); } } + + fn get(&self, call: &call::Request) -> Option { + let buffer = self.buffer.lock().unwrap(); + buffer.get(call).map(|retval| { + call.cheap_clone() + .response(retval.clone(), call::Source::Memory) + }) + } } impl EthereumCallCache for BufferedCallCache { @@ -47,14 +55,8 @@ impl EthereumCallCache for BufferedCallCache { ) -> Result, graph::prelude::Error> { self.check_block(&block); - { - let buffer = self.buffer.lock().unwrap(); - if let Some(result) = buffer.get(call) { - return Ok(Some( - call.cheap_clone() - .response(result.clone(), call::Source::Memory), - )); - } + if let Some(value) = self.get(call) { + return Ok(Some(value)); } let result = self.call_cache.get_call(&call, block)?; @@ -71,6 +73,36 @@ impl EthereumCallCache for BufferedCallCache { Ok(result) } + fn get_calls( + &self, + reqs: &[call::Request], + block: BlockPtr, + ) -> Result<(Vec, Vec), graph::prelude::Error> { + self.check_block(&block); + + let mut missing = Vec::new(); + let mut resps = Vec::new(); + + for call in reqs { + match self.get(call) { + Some(resp) => resps.push(resp), + None => missing.push(call.cheap_clone()), + } + } + + let (stored, calls) = self.call_cache.get_calls(&missing, block)?; + + { + let mut buffer = self.buffer.lock().unwrap(); + for resp in &stored { + buffer.insert(resp.req.cheap_clone(), resp.retval.clone()); + } + } + + resps.extend(stored); + Ok((resps, calls)) + } + fn get_calls_in_block( &self, block: BlockPtr, diff --git a/graph/src/components/store/traits.rs b/graph/src/components/store/traits.rs index 1c0d31d1269..b95a6e9d0ea 100644 --- a/graph/src/components/store/traits.rs +++ b/graph/src/components/store/traits.rs @@ -539,6 +539,15 @@ pub trait EthereumCallCache: Send + Sync + 'static { block: BlockPtr, ) -> Result, Error>; + /// Get the return values of many Ethereum calls. For the ones found in + /// the cache, return a `Response`; the ones that were not found are + /// returned as the original `Request` + fn get_calls( + &self, + reqs: &[call::Request], + block: BlockPtr, + ) -> Result<(Vec, Vec), Error>; + /// Returns all cached calls for a given `block`. This method does *not* /// update the last access time of the returned cached calls. fn get_calls_in_block(&self, block: BlockPtr) -> Result, Error>; diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index 482ae88adcf..aff948dce42 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -1163,6 +1163,57 @@ mod data { .map(|row| row.map(|(return_value, expired)| (Bytes::from(return_value), expired))) } + pub(super) fn get_calls_and_access( + &self, + conn: &mut PgConnection, + ids: &[&[u8]], + ) -> Result, Bytes, bool)>, Error> { + let rows = match self { + Storage::Shared => { + use public::eth_call_cache as cache; + use public::eth_call_meta as meta; + + cache::table + .inner_join(meta::table) + .filter(cache::id.eq_any(ids)) + .select(( + cache::id, + cache::return_value, + sql::("CURRENT_DATE > eth_call_meta.accessed_at"), + )) + .load(conn) + .map_err(Error::from) + } + Storage::Private(Schema { + call_cache, + call_meta, + .. + }) => call_cache + .table() + .inner_join( + call_meta.table().on(call_meta + .contract_address() + .eq(call_cache.contract_address())), + ) + .filter(call_cache.id().eq_any(ids)) + .select(( + call_cache.id(), + call_cache.return_value(), + sql::(&format!( + "CURRENT_DATE > {}.{}", + CallMetaTable::TABLE_NAME, + CallMetaTable::ACCESSED_AT + )), + )) + .load::<(Vec, Vec, bool)>(conn) + .map_err(Error::from), + }?; + Ok(rows + .into_iter() + .map(|(id, return_value, expired)| (id, Bytes::from(return_value), expired)) + .collect()) + } + pub(super) fn get_calls_in_block( &self, conn: &mut PgConnection, @@ -2420,6 +2471,45 @@ impl EthereumCallCache for ChainStore { })) } + fn get_calls( + &self, + reqs: &[call::Request], + block: BlockPtr, + ) -> Result<(Vec, Vec), Error> { + let ids: Vec<_> = reqs + .into_iter() + .map(|req| contract_call_id(req, &block)) + .collect(); + let id_refs: Vec<_> = ids.iter().map(|id| id.as_slice()).collect(); + + let conn = &mut *self.get_conn()?; + let rows = conn + .transaction::<_, Error, _>(|conn| self.storage.get_calls_and_access(conn, &id_refs))?; + + let mut found: Vec = Vec::new(); + let mut resps = Vec::new(); + for (id, retval, _) in rows { + let idx = ids.iter().position(|i| i.as_ref() == id).ok_or_else(|| { + constraint_violation!( + "get_calls returned a call id that was not requested: {}", + hex::encode(id) + ) + })?; + found.push(idx); + let resp = reqs[idx] + .cheap_clone() + .response(call::Retval::Value(retval), call::Source::Store); + resps.push(resp); + } + let calls = reqs + .into_iter() + .enumerate() + .filter(|(idx, _)| !found.contains(&idx)) + .map(|(_, call)| call.cheap_clone()) + .collect(); + Ok((resps, calls)) + } + fn get_calls_in_block(&self, block: BlockPtr) -> Result, Error> { let conn = &mut *self.get_conn()?; conn.transaction::<_, Error, _>(|conn| self.storage.get_calls_in_block(conn, block)) From 1ab4a667c514c1f4c44e441c2929c0df2bd91da0 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 15:22:45 -0800 Subject: [PATCH 45/51] chain/ethereum: Look up calls from the cache in bulk --- chain/ethereum/src/adapter.rs | 3 ++- chain/ethereum/src/ethereum_adapter.rs | 33 ++++++++++++-------------- store/postgres/src/chain_store.rs | 4 ++++ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index 023af94cb4d..95e35be53ca 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -976,7 +976,8 @@ pub trait EthereumAdapter: Send + Sync + 'static { ) -> Result<(Option>, call::Source), EthereumContractCallError>; /// Make multiple contract calls in a single batch. The returned `Vec` - /// has results in the same order as the calls in `calls` on input + /// has results in the same order as the calls in `calls` on input. The + /// calls must all be for the same block async fn contract_calls( &self, logger: &Logger, diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 326db54d2c0..cf2b3ac28cd 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1426,30 +1426,27 @@ impl EthereumAdapterTrait for EthereumAdapter { } } + if calls.is_empty() { + return Ok(Vec::new()); + } + + let block_ptr = calls.first().unwrap().block_ptr.clone(); + if calls.iter().any(|call| call.block_ptr != block_ptr) { + return Err(EthereumContractCallError::Internal( + "all calls must have the same block pointer".to_string(), + )); + } + let reqs: Vec<_> = calls .iter() .enumerate() .map(|(index, call)| as_req(logger, call, index as u32)) .collect::>()?; - let mut resps = Vec::new(); - let mut missing = Vec::new(); - for req in reqs { - let call = &calls[req.index as usize]; - match cache - .get_call(&req, call.block_ptr.clone()) - .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) - .ok() - .flatten() - { - Some(resp) => { - resps.push(resp); - } - None => { - missing.push(req); - } - } - } + let (mut resps, missing) = cache + .get_calls(&reqs, block_ptr) + .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) + .unwrap_or_else(|_| (Vec::new(), reqs)); for req in missing { let call = calls[req.index as usize]; diff --git a/store/postgres/src/chain_store.rs b/store/postgres/src/chain_store.rs index aff948dce42..504431fcb5f 100644 --- a/store/postgres/src/chain_store.rs +++ b/store/postgres/src/chain_store.rs @@ -2476,6 +2476,10 @@ impl EthereumCallCache for ChainStore { reqs: &[call::Request], block: BlockPtr, ) -> Result<(Vec, Vec), Error> { + if reqs.is_empty() { + return Ok((Vec::new(), Vec::new())); + } + let ids: Vec<_> = reqs .into_iter() .map(|req| contract_call_id(req, &block)) From ce648248793fc775079a2138e38c6f404445a1a8 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 15:28:54 -0800 Subject: [PATCH 46/51] chain/ethereum: Parallelize RPC calls in EthereumAapter.contract_calls --- chain/ethereum/src/ethereum_adapter.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index cf2b3ac28cd..222466f416c 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -10,6 +10,7 @@ use graph::data::subgraph::UnifiedMappingApiVersion; use graph::data::subgraph::API_VERSION_0_0_7; use graph::prelude::ethabi::ParamType; use graph::prelude::ethabi::Token; +use graph::prelude::futures03::future::try_join_all; use graph::prelude::tokio::try_join; use graph::prelude::web3::types::U256; use graph::slog::o; @@ -1448,13 +1449,11 @@ impl EthereumAdapterTrait for EthereumAdapter { .map_err(|e| error!(logger, "call cache get error"; "error" => e.to_string())) .unwrap_or_else(|_| (Vec::new(), reqs)); - for req in missing { + let futs = missing.into_iter().map(|req| { let call = calls[req.index as usize]; - let resp = self - .call_and_cache(logger, call, req, cache.clone()) - .await?; - resps.push(resp); - } + self.call_and_cache(logger, call, req, cache.clone()) + }); + resps.extend(try_join_all(futs).await?); // If we make it here, we have a response for every call. debug_assert_eq!(resps.len(), calls.len()); From 9f467bca3e2351ecdbfbd418856f53a2c2b1b786 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 17:34:14 -0800 Subject: [PATCH 47/51] chain/ethereum: Strip the `Ethereum` prefix of a few types --- chain/ethereum/src/adapter.rs | 16 ++--- chain/ethereum/src/data_source.rs | 14 ++--- chain/ethereum/src/ethereum_adapter.rs | 58 +++++++++---------- chain/ethereum/src/lib.rs | 2 +- chain/ethereum/src/runtime/runtime_adapter.rs | 19 +++--- 5 files changed, 49 insertions(+), 60 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index 95e35be53ca..21df65965c2 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -37,7 +37,7 @@ pub type EventSignature = H256; pub type FunctionSelector = [u8; 4]; #[derive(Clone, Debug)] -pub struct EthereumContractCall { +pub struct ContractCall { pub address: Address, pub block_ptr: BlockPtr, pub function: Function, @@ -46,7 +46,7 @@ pub struct EthereumContractCall { } #[derive(Error, Debug)] -pub enum EthereumGetBalanceError { +pub enum GetBalanceError { #[error("call error: {0}")] Web3Error(web3::Error), #[error("ethereum node took too long to perform call")] @@ -54,7 +54,7 @@ pub enum EthereumGetBalanceError { } #[derive(Error, Debug)] -pub enum EthereumContractCallError { +pub enum ContractCallError { #[error("ABI error: {0}")] ABIError(#[from] ABIError), /// `Token` is not of expected `ParamType` @@ -971,9 +971,9 @@ pub trait EthereumAdapter: Send + Sync + 'static { async fn contract_call( &self, logger: &Logger, - call: &EthereumContractCall, + call: &ContractCall, cache: Arc, - ) -> Result<(Option>, call::Source), EthereumContractCallError>; + ) -> Result<(Option>, call::Source), ContractCallError>; /// Make multiple contract calls in a single batch. The returned `Vec` /// has results in the same order as the calls in `calls` on input. The @@ -981,16 +981,16 @@ pub trait EthereumAdapter: Send + Sync + 'static { async fn contract_calls( &self, logger: &Logger, - calls: &[&EthereumContractCall], + calls: &[&ContractCall], cache: Arc, - ) -> Result>, call::Source)>, EthereumContractCallError>; + ) -> Result>, call::Source)>, ContractCallError>; fn get_balance( &self, logger: &Logger, address: H160, block_ptr: BlockPtr, - ) -> Box + Send>; + ) -> Box + Send>; } #[cfg(test)] diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 81c5d9f6763..550496c5914 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -47,7 +47,7 @@ use crate::adapter::EthereumAdapter as _; use crate::chain::Chain; use crate::network::EthereumNetworkAdapters; use crate::trigger::{EthereumBlockTriggerType, EthereumTrigger, MappingTrigger}; -use crate::{EthereumContractCall, EthereumContractCallError, NodeCapabilities}; +use crate::{ContractCall, ContractCallError, NodeCapabilities}; // The recommended kind is `ethereum`, `ethereum/contract` is accepted for backwards compatibility. const ETHEREUM_KINDS: &[&str] = &["ethereum/contract", "ethereum"]; @@ -965,13 +965,9 @@ impl DeclaredCall { Ok(calls) } - fn as_eth_call( - self, - block_ptr: BlockPtr, - gas: Option, - ) -> (EthereumContractCall, String, String) { + fn as_eth_call(self, block_ptr: BlockPtr, gas: Option) -> (ContractCall, String, String) { ( - EthereumContractCall { + ContractCall { address: self.address, block_ptr, function: self.function, @@ -1041,7 +1037,7 @@ impl DecoderHook { // Any error reported by the Ethereum node could be due to the block no longer being on // the main chain. This is very unespecific but we don't want to risk failing a // subgraph due to a transient error such as a reorg. - Err(EthereumContractCallError::Web3Error(e)) => Err(MappingError::PossibleReorg(anyhow::anyhow!( + Err(ContractCallError::Web3Error(e)) => Err(MappingError::PossibleReorg(anyhow::anyhow!( "Ethereum node returned an error when calling function \"{}\" of contract \"{}\": {}", function_name, contract_name, @@ -1049,7 +1045,7 @@ impl DecoderHook { ))), // Also retry on timeouts. - Err(EthereumContractCallError::Timeout) => Err(MappingError::PossibleReorg(anyhow::anyhow!( + Err(ContractCallError::Timeout) => Err(MappingError::PossibleReorg(anyhow::anyhow!( "Ethereum node did not respond when calling function \"{}\" of contract \"{}\"", function_name, contract_name, diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 222466f416c..a4132ac559f 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -46,7 +46,7 @@ use std::pin::Pin; use std::sync::Arc; use std::time::Instant; -use crate::adapter::EthereumGetBalanceError; +use crate::adapter::GetBalanceError; use crate::adapter::ProviderStatus; use crate::chain::BlockFinality; use crate::trigger::LogRef; @@ -54,9 +54,9 @@ use crate::Chain; use crate::NodeCapabilities; use crate::{ adapter::{ - EthGetLogsFilter, EthereumAdapter as EthereumAdapterTrait, EthereumBlockFilter, - EthereumCallFilter, EthereumContractCall, EthereumContractCallError, EthereumLogFilter, - ProviderEthRpcMetrics, SubgraphEthRpcMetrics, + ContractCall, ContractCallError, EthGetLogsFilter, EthereumAdapter as EthereumAdapterTrait, + EthereumBlockFilter, EthereumCallFilter, EthereumLogFilter, ProviderEthRpcMetrics, + SubgraphEthRpcMetrics, }, transport::Transport, trigger::{EthereumBlockTriggerType, EthereumTrigger}, @@ -419,7 +419,7 @@ impl EthereumAdapter { logger: &Logger, address: Address, block_ptr: BlockPtr, - ) -> impl Future + Send { + ) -> impl Future + Send { let web3 = self.web3.clone(); let logger = Logger::new(&logger, o!("provider" => self.provider.clone())); @@ -446,11 +446,11 @@ impl EthereumAdapter { web3.eth().balance(address, Some(block_id)).boxed().await; match result { Ok(balance) => Ok(balance), - Err(err) => Err(EthereumGetBalanceError::Web3Error(err)), + Err(err) => Err(GetBalanceError::Web3Error(err)), } } }) - .map_err(|e| e.into_inner().unwrap_or(EthereumGetBalanceError::Timeout)) + .map_err(|e| e.into_inner().unwrap_or(GetBalanceError::Timeout)) .boxed() .compat() } @@ -461,11 +461,8 @@ impl EthereumAdapter { call_data: call::Request, block_ptr: BlockPtr, gas: Option, - ) -> Result { - fn reverted( - logger: &Logger, - reason: &str, - ) -> Result { + ) -> Result { + fn reverted(logger: &Logger, reason: &str) -> Result { info!(logger, "Contract call reverted"; "reason" => reason); Ok(call::Retval::Null) } @@ -602,18 +599,18 @@ impl EthereumAdapter { } // The VM execution error was not identified as a revert. - _ => Err(EthereumContractCallError::Web3Error(web3::Error::Rpc( + _ => Err(ContractCallError::Web3Error(web3::Error::Rpc( rpc_error.clone(), ))), } } // The error was not identified as a revert. - Err(err) => Err(EthereumContractCallError::Web3Error(err)), + Err(err) => Err(ContractCallError::Web3Error(err)), } } }) - .map_err(|e| e.into_inner().unwrap_or(EthereumContractCallError::Timeout)) + .map_err(|e| e.into_inner().unwrap_or(ContractCallError::Timeout)) .boxed() .await } @@ -621,10 +618,10 @@ impl EthereumAdapter { async fn call_and_cache( &self, logger: &Logger, - call: &EthereumContractCall, + call: &ContractCall, req: call::Request, cache: Arc, - ) -> Result { + ) -> Result { let result = self .call( logger.clone(), @@ -1331,7 +1328,7 @@ impl EthereumAdapterTrait for EthereumAdapter { logger: &Logger, address: H160, block_ptr: BlockPtr, - ) -> Box + Send> { + ) -> Box + Send> { debug!( logger, "eth_getBalance"; "address" => format!("{}", address), @@ -1343,9 +1340,9 @@ impl EthereumAdapterTrait for EthereumAdapter { async fn contract_call( &self, logger: &Logger, - inp_call: &EthereumContractCall, + inp_call: &ContractCall, cache: Arc, - ) -> Result<(Option>, call::Source), EthereumContractCallError> { + ) -> Result<(Option>, call::Source), ContractCallError> { let mut result = self.contract_calls(logger, &[inp_call], cache).await?; // unwrap: self.contract_calls returns as many results as there were calls Ok(result.pop().unwrap()) @@ -1354,14 +1351,14 @@ impl EthereumAdapterTrait for EthereumAdapter { async fn contract_calls( &self, logger: &Logger, - calls: &[&EthereumContractCall], + calls: &[&ContractCall], cache: Arc, - ) -> Result>, call::Source)>, EthereumContractCallError> { + ) -> Result>, call::Source)>, ContractCallError> { fn as_req( logger: &Logger, - call: &EthereumContractCall, + call: &ContractCall, index: u32, - ) -> Result { + ) -> Result { // Emit custom error for type mismatches. for (token, kind) in call .args @@ -1369,10 +1366,7 @@ impl EthereumAdapterTrait for EthereumAdapter { .zip(call.function.inputs.iter().map(|p| &p.kind)) { if !token.type_check(kind) { - return Err(EthereumContractCallError::TypeError( - token.clone(), - kind.clone(), - )); + return Err(ContractCallError::TypeError(token.clone(), kind.clone())); } } @@ -1381,7 +1375,7 @@ impl EthereumAdapterTrait for EthereumAdapter { let encoded_call = call .function .encode_input(&call.args) - .map_err(EthereumContractCallError::EncodingError)?; + .map_err(ContractCallError::EncodingError)?; call::Request::new(call.address, encoded_call, index) }; @@ -1398,8 +1392,8 @@ impl EthereumAdapterTrait for EthereumAdapter { fn decode( logger: &Logger, resp: call::Response, - call: &EthereumContractCall, - ) -> Result<(Option>, call::Source), EthereumContractCallError> { + call: &ContractCall, + ) -> Result<(Option>, call::Source), ContractCallError> { let call::Response { retval, source, @@ -1433,7 +1427,7 @@ impl EthereumAdapterTrait for EthereumAdapter { let block_ptr = calls.first().unwrap().block_ptr.clone(); if calls.iter().any(|call| call.block_ptr != block_ptr) { - return Err(EthereumContractCallError::Internal( + return Err(ContractCallError::Internal( "all calls must have the same block pointer".to_string(), )); } diff --git a/chain/ethereum/src/lib.rs b/chain/ethereum/src/lib.rs index 0eb122246a5..934830ecde5 100644 --- a/chain/ethereum/src/lib.rs +++ b/chain/ethereum/src/lib.rs @@ -28,7 +28,7 @@ pub mod network; pub mod trigger; pub use crate::adapter::{ - EthereumAdapter as EthereumAdapterTrait, EthereumContractCall, EthereumContractCallError, + ContractCall, ContractCallError, EthereumAdapter as EthereumAdapterTrait, ProviderEthRpcMetrics, SubgraphEthRpcMetrics, TriggerFilter, }; pub use crate::chain::Chain; diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index 80b40218e8f..fda233d67b4 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -1,11 +1,10 @@ use std::{sync::Arc, time::Instant}; -use crate::adapter::EthereumGetBalanceError; +use crate::adapter::GetBalanceError; use crate::data_source::MappingABI; use crate::{ - capabilities::NodeCapabilities, network::EthereumNetworkAdapters, Chain, DataSource, - EthereumAdapter, EthereumAdapterTrait, EthereumContractCall, EthereumContractCallError, - ENV_VARS, + capabilities::NodeCapabilities, network::EthereumNetworkAdapters, Chain, ContractCall, + ContractCallError, DataSource, EthereumAdapter, EthereumAdapterTrait, ENV_VARS, }; use anyhow::{anyhow, Context, Error}; use blockchain::HostFn; @@ -188,9 +187,9 @@ fn eth_get_balance( Ok(asc_new(ctx.heap, &bigint, &ctx.gas)?) } // Retry on any kind of error - Err(EthereumGetBalanceError::Web3Error(e)) => Err(HostExportError::PossibleReorg(e.into())), - Err(EthereumGetBalanceError::Timeout) => Err(HostExportError::PossibleReorg( - EthereumGetBalanceError::Timeout.into(), + Err(GetBalanceError::Web3Error(e)) => Err(HostExportError::PossibleReorg(e.into())), + Err(GetBalanceError::Timeout) => Err(HostExportError::PossibleReorg( + GetBalanceError::Timeout.into(), )), } } @@ -242,7 +241,7 @@ fn eth_call( ) .map_err(HostExportError::Deterministic)?; - let call = EthereumContractCall { + let call = ContractCall { address: unresolved_call.contract_address, block_ptr: block_ptr.cheap_clone(), function: function.clone(), @@ -264,7 +263,7 @@ fn eth_call( // Any error reported by the Ethereum node could be due to the block no longer being on // the main chain. This is very unespecific but we don't want to risk failing a // subgraph due to a transient error such as a reorg. - Err(EthereumContractCallError::Web3Error(e)) => Err(HostExportError::PossibleReorg(anyhow::anyhow!( + Err(ContractCallError::Web3Error(e)) => Err(HostExportError::PossibleReorg(anyhow::anyhow!( "Ethereum node returned an error when calling function \"{}\" of contract \"{}\": {}", unresolved_call.function_name, unresolved_call.contract_name, @@ -272,7 +271,7 @@ fn eth_call( ))), // Also retry on timeouts. - Err(EthereumContractCallError::Timeout) => Err(HostExportError::PossibleReorg(anyhow::anyhow!( + Err(ContractCallError::Timeout) => Err(HostExportError::PossibleReorg(anyhow::anyhow!( "Ethereum node did not respond when calling function \"{}\" of contract \"{}\"", unresolved_call.function_name, unresolved_call.contract_name, From bd5ae84bf9dcc377d64ba9e75febf0c983a9bedf Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 17:56:29 -0800 Subject: [PATCH 48/51] chain/ethereum: Decoding call results never produces an error --- chain/ethereum/src/ethereum_adapter.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index a4132ac559f..30ce6e92021 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1393,7 +1393,7 @@ impl EthereumAdapterTrait for EthereumAdapter { logger: &Logger, resp: call::Response, call: &ContractCall, - ) -> Result<(Option>, call::Source), ContractCallError> { + ) -> (Option>, call::Source) { let call::Response { retval, source, @@ -1402,13 +1402,13 @@ impl EthereumAdapterTrait for EthereumAdapter { use call::Retval::*; match retval { Value(output) => match call.function.decode_output(&output) { - Ok(tokens) => Ok((Some(tokens), source)), + Ok(tokens) => (Some(tokens), source), Err(e) => { // Decode failures are reverts. The reasoning is that if Solidity fails to // decode an argument, that's a revert, so the same goes for the output. let reason = format!("failed to decode output: {}", e); info!(logger, "Contract call reverted"; "reason" => reason); - Ok((None, call::Source::Rpc)) + (None, call::Source::Rpc) } }, Null => { @@ -1416,7 +1416,7 @@ impl EthereumAdapterTrait for EthereumAdapter { // that the contract actually returned an empty response. A view call is meant // to return something, so we treat empty responses the same as reverts. info!(logger, "Contract call reverted"; "reason" => "empty response"); - Ok((None, call::Source::Rpc)) + (None, call::Source::Rpc) } } } @@ -1461,7 +1461,7 @@ impl EthereumAdapterTrait for EthereumAdapter { let call = &calls[res.req.index as usize]; decode(logger, res, call) }) - .collect::>()?; + .collect(); Ok(decoded) } From 4527ad7add82d22d32ef685027eb916d97699398 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 18:33:44 -0800 Subject: [PATCH 49/51] chain/ethereum: Log errors for individual calls in EthereumAdapter This tries to avoid having to return errors for each call separately from contract_calls, while preserving the distinction between errors that indicate reorgs nad other errors. --- chain/ethereum/src/adapter.rs | 20 ++++++++ chain/ethereum/src/data_source.rs | 48 ++++--------------- chain/ethereum/src/ethereum_adapter.rs | 27 ++++++++++- chain/ethereum/src/runtime/runtime_adapter.rs | 1 + 4 files changed, 55 insertions(+), 41 deletions(-) diff --git a/chain/ethereum/src/adapter.rs b/chain/ethereum/src/adapter.rs index 21df65965c2..7bb40469e6f 100644 --- a/chain/ethereum/src/adapter.rs +++ b/chain/ethereum/src/adapter.rs @@ -2,6 +2,7 @@ use anyhow::Error; use ethabi::{Error as ABIError, Function, ParamType, Token}; use futures::Future; use graph::blockchain::ChainIdentifier; +use graph::components::subgraph::MappingError; use graph::data::store::ethereum::call; use graph::firehose::CallToFilter; use graph::firehose::CombinedFilter; @@ -38,6 +39,7 @@ pub type FunctionSelector = [u8; 4]; #[derive(Clone, Debug)] pub struct ContractCall { + pub contract_name: String, pub address: Address, pub block_ptr: BlockPtr, pub function: Function, @@ -70,6 +72,24 @@ pub enum ContractCallError { Internal(String), } +impl From for MappingError { + fn from(e: ContractCallError) -> Self { + match e { + // Any error reported by the Ethereum node could be due to the block no longer being on + // the main chain. This is very unespecific but we don't want to risk failing a + // subgraph due to a transient error such as a reorg. + ContractCallError::Web3Error(e) => MappingError::PossibleReorg(anyhow::anyhow!( + "Ethereum node returned an error for an eth_call: {e}" + )), + // Also retry on timeouts. + ContractCallError::Timeout => MappingError::PossibleReorg(anyhow::anyhow!( + "Ethereum node did not respond in time to eth_call" + )), + e => MappingError::Unknown(anyhow::anyhow!("Error when making an eth_call: {e}")), + } + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] enum LogFilterNode { Contract(Address), diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index 550496c5914..d3a2b72f2cb 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -47,7 +47,7 @@ use crate::adapter::EthereumAdapter as _; use crate::chain::Chain; use crate::network::EthereumNetworkAdapters; use crate::trigger::{EthereumBlockTriggerType, EthereumTrigger, MappingTrigger}; -use crate::{ContractCall, ContractCallError, NodeCapabilities}; +use crate::{ContractCall, NodeCapabilities}; // The recommended kind is `ethereum`, `ethereum/contract` is accepted for backwards compatibility. const ETHEREUM_KINDS: &[&str] = &["ethereum/contract", "ethereum"]; @@ -965,16 +965,16 @@ impl DeclaredCall { Ok(calls) } - fn as_eth_call(self, block_ptr: BlockPtr, gas: Option) -> (ContractCall, String, String) { + fn as_eth_call(self, block_ptr: BlockPtr, gas: Option) -> (ContractCall, String) { ( ContractCall { + contract_name: self.contract_name, address: self.address, block_ptr, function: self.function, args: self.args, gas, }, - self.contract_name, self.label, ) } @@ -1010,13 +1010,13 @@ impl DecoderHook { ) -> Result, MappingError> { let start = Instant::now(); let function_name = call.function.name.clone(); - let (eth_call, contract_name, label) = - call.as_eth_call(block_ptr.clone(), self.eth_call_gas); + let (eth_call, label) = call.as_eth_call(block_ptr.clone(), self.eth_call_gas); let eth_adapter = self.eth_adapters.call_or_cheapest(Some(&NodeCapabilities { archive: true, traces: false, }))?; + // For errors, we set the source to 'Rpc' as that's where errors happen let (result, source) = match eth_adapter .contract_call(logger, ð_call, self.call_cache.cheap_clone()) .await @@ -1025,46 +1025,16 @@ impl DecoderHook { Err(e) => (Err(e), call::Source::Rpc), }; - // This error analysis is very much modeled on the one in - // `crate::runtime_adapter::eth_call`; it would be better to - // make them the same but there are subtle differences (return - // type, error type) - // - // For errors, we set the source to 'Rpc' as that's where errors happen - let result= match result { - Ok(r) => Ok(if r.is_some() { None } else { Some(label) }), - - // Any error reported by the Ethereum node could be due to the block no longer being on - // the main chain. This is very unespecific but we don't want to risk failing a - // subgraph due to a transient error such as a reorg. - Err(ContractCallError::Web3Error(e)) => Err(MappingError::PossibleReorg(anyhow::anyhow!( - "Ethereum node returned an error when calling function \"{}\" of contract \"{}\": {}", - function_name, - contract_name, - e - ))), - - // Also retry on timeouts. - Err(ContractCallError::Timeout) => Err(MappingError::PossibleReorg(anyhow::anyhow!( - "Ethereum node did not respond when calling function \"{}\" of contract \"{}\"", - function_name, - contract_name, - ))), - - Err(e) => Err(MappingError::Unknown(anyhow::anyhow!( - "Failed to call function \"{}\" of contract \"{}\": {}", - function_name, - contract_name, - e - ))), - }; + let result = result + .map(|r| if r.is_some() { None } else { Some(label) }) + .map_err(MappingError::from); if source.observe() { let elapsed = start.elapsed(); metrics.observe_eth_call_execution_time( elapsed.as_secs_f64(), - &contract_name, + ð_call.contract_name, &function_name, ); } diff --git a/chain/ethereum/src/ethereum_adapter.rs b/chain/ethereum/src/ethereum_adapter.rs index 30ce6e92021..63a3de66a7a 100644 --- a/chain/ethereum/src/ethereum_adapter.rs +++ b/chain/ethereum/src/ethereum_adapter.rs @@ -1421,6 +1421,20 @@ impl EthereumAdapterTrait for EthereumAdapter { } } + fn log_call_error(logger: &Logger, e: &ContractCallError, call: &ContractCall) { + match e { + ContractCallError::Web3Error(e) => error!(logger, + "Ethereum node returned an error when calling function \"{}\" of contract \"{}\": {}", + call.function.name, call.contract_name, e), + ContractCallError::Timeout => error!(logger, + "Ethereum node did not respond when calling function \"{}\" of contract \"{}\"", + call.function.name, call.contract_name), + _ => error!(logger, + "Failed to call function \"{}\" of contract \"{}\": {}", + call.function.name, call.contract_name, e), + } + } + if calls.is_empty() { return Ok(Vec::new()); } @@ -1444,8 +1458,17 @@ impl EthereumAdapterTrait for EthereumAdapter { .unwrap_or_else(|_| (Vec::new(), reqs)); let futs = missing.into_iter().map(|req| { - let call = calls[req.index as usize]; - self.call_and_cache(logger, call, req, cache.clone()) + let cache = cache.clone(); + async move { + let call = calls[req.index as usize]; + match self.call_and_cache(logger, call, req, cache.clone()).await { + Ok(resp) => Ok(resp), + Err(e) => { + log_call_error(logger, &e, call); + Err(e) + } + } + } }); resps.extend(try_join_all(futs).await?); diff --git a/chain/ethereum/src/runtime/runtime_adapter.rs b/chain/ethereum/src/runtime/runtime_adapter.rs index fda233d67b4..2be0dc30042 100644 --- a/chain/ethereum/src/runtime/runtime_adapter.rs +++ b/chain/ethereum/src/runtime/runtime_adapter.rs @@ -242,6 +242,7 @@ fn eth_call( .map_err(HostExportError::Deterministic)?; let call = ContractCall { + contract_name: unresolved_call.contract_name.clone(), address: unresolved_call.contract_address, block_ptr: block_ptr.cheap_clone(), function: function.clone(), From ffd52c2ab40f5b3e625d699d972627cc5a18acae Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Fri, 8 Mar 2024 19:08:57 -0800 Subject: [PATCH 50/51] chain/ethereum: Change how we parallelize eth calls The DecoderHook now relies on EthereumAdapter's contract_calls method. With that, cached call results are looked up with one query from the store. Call results that are not cached are retrieved with eth_calls made in parallel to the ethereum node. --- chain/ethereum/src/data_source.rs | 121 +++++++++++++++++++----------- 1 file changed, 77 insertions(+), 44 deletions(-) diff --git a/chain/ethereum/src/data_source.rs b/chain/ethereum/src/data_source.rs index d3a2b72f2cb..7ebe1d7263f 100644 --- a/chain/ethereum/src/data_source.rs +++ b/chain/ethereum/src/data_source.rs @@ -5,7 +5,6 @@ use graph::components::metrics::subgraph::SubgraphInstanceMetrics; use graph::components::store::{EthereumCallCache, StoredDynamicDataSource}; use graph::components::subgraph::{HostMetrics, InstanceDSTemplateInfo, MappingError}; use graph::components::trigger_processor::RunnableTriggers; -use graph::data::store::ethereum::call; use graph::data::value::Word; use graph::data_source::CausalityRegion; use graph::env::ENV_VARS; @@ -1001,45 +1000,82 @@ impl DecoderHook { } impl DecoderHook { - async fn eth_call( + /// Perform a batch of eth_calls, observing the execution time of each + /// call. Returns a list of the call labels for which we received a + /// `None` response, indicating a revert + async fn eth_calls( &self, logger: &Logger, block_ptr: &BlockPtr, - metrics: Arc, - call: DeclaredCall, - ) -> Result, MappingError> { + calls_and_metrics: Vec<(Arc, DeclaredCall)>, + ) -> Result, MappingError> { + // This check is not just to speed things up, but is also needed to + // make sure the runner tests don't fail; they don't have declared + // eth calls, but without this check we try to get an eth adapter + // even when there are no calls, which fails in the runner test + // setup + if calls_and_metrics.is_empty() { + return Ok(vec![]); + } + let start = Instant::now(); - let function_name = call.function.name.clone(); - let (eth_call, label) = call.as_eth_call(block_ptr.clone(), self.eth_call_gas); + + let (metrics, calls): (Vec<_>, Vec<_>) = calls_and_metrics.into_iter().unzip(); + + let (calls, labels): (Vec<_>, Vec<_>) = calls + .into_iter() + .map(|call| call.as_eth_call(block_ptr.clone(), self.eth_call_gas)) + .unzip(); + let eth_adapter = self.eth_adapters.call_or_cheapest(Some(&NodeCapabilities { archive: true, traces: false, }))?; - // For errors, we set the source to 'Rpc' as that's where errors happen - let (result, source) = match eth_adapter - .contract_call(logger, ð_call, self.call_cache.cheap_clone()) + let call_refs = calls.iter().collect::>(); + let results = eth_adapter + .contract_calls(logger, &call_refs, self.call_cache.cheap_clone()) .await - { - Ok((result, source)) => (Ok(result), source), - Err(e) => (Err(e), call::Source::Rpc), - }; - - let result = result - .map(|r| if r.is_some() { None } else { Some(label) }) - .map_err(MappingError::from); + .map_err(|e| { + // An error happened, everybody gets charged + let elapsed = start.elapsed().as_secs_f64() / call_refs.len() as f64; + for (metrics, call) in metrics.iter().zip(call_refs) { + metrics.observe_eth_call_execution_time( + elapsed, + &call.contract_name, + &call.function.name, + ); + } + MappingError::from(e) + })?; - if source.observe() { - let elapsed = start.elapsed(); + // We don't have time measurements for each call (though that would be nice) + // Use the average time of all calls that we want to observe as the time for + // each call + let to_observe = results.iter().map(|(_, source)| source.observe()).count() as f64; + let elapsed = start.elapsed().as_secs_f64() / to_observe; - metrics.observe_eth_call_execution_time( - elapsed.as_secs_f64(), - ð_call.contract_name, - &function_name, - ); - } + results + .iter() + .zip(metrics) + .zip(calls) + .for_each(|(((_, source), metrics), call)| { + if source.observe() { + metrics.observe_eth_call_execution_time( + elapsed, + &call.contract_name, + &call.function.name, + ); + } + }); - result + let labels = results + .iter() + .zip(labels) + .filter_map(|((res, _), label)| if res.is_none() { Some(label) } else { None }) + .map(|s| s.to_string()) + .collect(); + Ok(labels) } } @@ -1054,7 +1090,7 @@ impl blockchain::DecoderHook for DecoderHook { ) -> Result>, MappingError> { /// Log information about failed eth calls. 'Failure' here simply /// means that the call was reverted; outright errors lead to a real - /// error. For reverted calls, `self.eth_call` returns the label + /// error. For reverted calls, `self.eth_calls` returns the label /// from the manifest for that call. /// /// One reason why declared calls can fail is if they are attached @@ -1065,20 +1101,15 @@ impl blockchain::DecoderHook for DecoderHook { /// calls fn log_results( logger: &Logger, - results: &[Option], + failures: &[String], + calls_count: usize, trigger_count: usize, elapsed: Duration, ) { - let calls_count = results.len(); - let fail_count = results.iter().filter(|r| r.is_some()).count(); + let fail_count = failures.len(); if fail_count > 0 { - let mut counts: Vec<_> = results - .iter() - .filter_map(|r| r.as_ref()) - .counts() - .into_iter() - .collect(); + let mut counts: Vec<_> = failures.iter().counts().into_iter().collect(); counts.sort_by_key(|(label, _)| *label); let counts = counts .into_iter() @@ -1150,13 +1181,15 @@ impl blockchain::DecoderHook for DecoderHook { calls }; - let results = graph::prelude::futures03::future::try_join_all( - calls - .into_iter() - .map(|(metrics, call)| self.eth_call(logger, block_ptr, metrics, call)), - ) - .await?; - log_results(logger, &results, runnables.len(), start.elapsed()); + let calls_count = calls.len(); + let results = self.eth_calls(logger, block_ptr, calls).await?; + log_results( + logger, + &results, + calls_count, + runnables.len(), + start.elapsed(), + ); Ok(runnables) } From 19739853629fcbbe4362cc2d797be5e239bebb99 Mon Sep 17 00:00:00 2001 From: David Lutterkort Date: Tue, 12 Mar 2024 16:21:14 -0700 Subject: [PATCH 51/51] all: Change initialization of RuntimeAdapter and DecoderHook The DecoderHook and the RuntimeAdapter for ethereum need to share the same call cache, a separate one for each subgraph, since the BufferedCallCache needs to be a different one for each subgraph - if subgraphs shared a BufferedCallCache, they would clobber each others declared calls. --- chain/arweave/src/chain.rs | 8 +-- chain/cosmos/src/chain.rs | 8 +-- chain/ethereum/src/chain.rs | 75 +++++++++++++++++++------- chain/near/src/chain.rs | 8 +-- chain/starknet/src/chain.rs | 8 +-- chain/substreams/src/chain.rs | 8 +-- core/src/subgraph/instance_manager.rs | 5 +- core/src/subgraph/trigger_processor.rs | 4 +- graph/src/blockchain/mock.rs | 6 +-- graph/src/blockchain/mod.rs | 4 +- node/src/main.rs | 21 +++----- node/src/manager/commands/run.rs | 16 +++--- tests/src/fixture/ethereum.rs | 6 +-- tests/src/fixture/mod.rs | 17 +++++- 14 files changed, 107 insertions(+), 87 deletions(-) diff --git a/chain/arweave/src/chain.rs b/chain/arweave/src/chain.rs index 8b9b25d8a6a..1612d1cd9b5 100644 --- a/chain/arweave/src/chain.rs +++ b/chain/arweave/src/chain.rs @@ -163,8 +163,8 @@ impl Blockchain for Chain { .map_err(Into::into) } - fn runtime_adapter(&self) -> Arc> { - Arc::new(NoopRuntimeAdapter::default()) + fn runtime(&self) -> (Arc>, Self::DecoderHook) { + (Arc::new(NoopRuntimeAdapter::default()), NoopDecoderHook) } fn chain_client(&self) -> Arc> { @@ -181,10 +181,6 @@ impl Blockchain for Chain { ); Ok(Box::new(ingestor)) } - - fn decoder_hook(&self) -> Self::DecoderHook { - NoopDecoderHook - } } pub struct TriggersAdapter {} diff --git a/chain/cosmos/src/chain.rs b/chain/cosmos/src/chain.rs index 9b7414fc9ce..6d5ee27269e 100644 --- a/chain/cosmos/src/chain.rs +++ b/chain/cosmos/src/chain.rs @@ -157,8 +157,8 @@ impl Blockchain for Chain { .map_err(Into::into) } - fn runtime_adapter(&self) -> Arc> { - Arc::new(NoopRuntimeAdapter::default()) + fn runtime(&self) -> (Arc>, Self::DecoderHook) { + (Arc::new(NoopRuntimeAdapter::default()), NoopDecoderHook) } fn chain_client(&self) -> Arc> { @@ -175,10 +175,6 @@ impl Blockchain for Chain { ); Ok(Box::new(ingestor)) } - - fn decoder_hook(&self) -> Self::DecoderHook { - NoopDecoderHook - } } pub struct TriggersAdapter {} diff --git a/chain/ethereum/src/chain.rs b/chain/ethereum/src/chain.rs index f1d454ccc26..375b6b5f0ec 100644 --- a/chain/ethereum/src/chain.rs +++ b/chain/ethereum/src/chain.rs @@ -2,7 +2,9 @@ use anyhow::{anyhow, bail, Result}; use anyhow::{Context, Error}; use graph::blockchain::client::ChainClient; use graph::blockchain::firehose_block_ingestor::{FirehoseBlockIngestor, Transforms}; -use graph::blockchain::{BlockIngestor, BlockTime, BlockchainKind, TriggersAdapterSelector}; +use graph::blockchain::{ + BlockIngestor, BlockTime, BlockchainKind, ChainIdentifier, TriggersAdapterSelector, +}; use graph::components::store::DeploymentCursorTracker; use graph::data::subgraph::UnifiedMappingApiVersion; use graph::firehose::{FirehoseEndpoint, ForkStep}; @@ -43,8 +45,6 @@ use crate::data_source::UnresolvedDataSourceTemplate; use crate::ingestor::PollingBlockIngestor; use crate::network::EthereumNetworkAdapters; use crate::runtime::runtime_adapter::eth_call_gas; -use crate::EthereumAdapter; -use crate::NodeCapabilities; use crate::{ adapter::EthereumAdapter as _, codec, @@ -55,6 +55,8 @@ use crate::{ }, SubgraphEthRpcMetrics, TriggerFilter, ENV_VARS, }; +use crate::{BufferedCallCache, NodeCapabilities}; +use crate::{EthereumAdapter, RuntimeAdapter}; use graph::blockchain::block_stream::{ BlockStream, BlockStreamBuilder, BlockStreamError, BlockStreamMapper, FirehoseCursor, }; @@ -252,10 +254,40 @@ impl TriggersAdapterSelector for EthereumAdapterSelector { } } +/// We need this so that the runner tests can use a `NoopRuntimeAdapter` +/// instead of the `RuntimeAdapter` from this crate to avoid needing +/// ethereum adapters +pub trait RuntimeAdapterBuilder: Send + Sync + 'static { + fn build( + &self, + eth_adapters: Arc, + call_cache: Arc, + chain_identifier: Arc, + ) -> Arc>; +} + +pub struct EthereumRuntimeAdapterBuilder {} + +impl RuntimeAdapterBuilder for EthereumRuntimeAdapterBuilder { + fn build( + &self, + eth_adapters: Arc, + call_cache: Arc, + chain_identifier: Arc, + ) -> Arc> { + Arc::new(RuntimeAdapter { + eth_adapters, + call_cache, + chain_identifier, + }) + } +} + pub struct Chain { logger_factory: LoggerFactory, name: String, node_id: NodeId, + chain_identifier: Arc, registry: Arc, client: Arc>, chain_store: Arc, @@ -267,7 +299,7 @@ pub struct Chain { block_stream_builder: Arc>, block_refetcher: Arc>, adapter_selector: Arc>, - runtime_adapter: Arc>, + runtime_adapter_builder: Arc, eth_adapters: Arc, } @@ -291,16 +323,18 @@ impl Chain { block_stream_builder: Arc>, block_refetcher: Arc>, adapter_selector: Arc>, - runtime_adapter: Arc>, + runtime_adapter_builder: Arc, eth_adapters: Arc, reorg_threshold: BlockNumber, polling_ingestor_interval: Duration, is_ingestible: bool, ) -> Self { + let chain_identifier = Arc::new(chain_store.chain_identifier().clone()); Chain { logger_factory, name, node_id, + chain_identifier, registry, client, chain_store, @@ -309,7 +343,7 @@ impl Chain { block_stream_builder, block_refetcher, adapter_selector, - runtime_adapter, + runtime_adapter_builder, eth_adapters, reorg_threshold, is_ingestible, @@ -449,8 +483,23 @@ impl Blockchain for Chain { self.block_refetcher.get_block(self, logger, cursor).await } - fn runtime_adapter(&self) -> Arc> { - self.runtime_adapter.clone() + fn runtime(&self) -> (Arc>, Self::DecoderHook) { + let call_cache = Arc::new(BufferedCallCache::new(self.call_cache.cheap_clone())); + + let builder = self.runtime_adapter_builder.build( + self.eth_adapters.cheap_clone(), + call_cache.cheap_clone(), + self.chain_identifier.cheap_clone(), + ); + let eth_call_gas = eth_call_gas(&self.chain_identifier); + + let decoder_hook = crate::data_source::DecoderHook::new( + self.eth_adapters.cheap_clone(), + call_cache, + eth_call_gas, + ); + + (builder, decoder_hook) } fn chain_client(&self) -> Arc> { @@ -510,16 +559,6 @@ impl Blockchain for Chain { Ok(ingestor) } - - fn decoder_hook(&self) -> Self::DecoderHook { - let eth_call_gas = eth_call_gas(self.chain_store.chain_identifier()); - - crate::data_source::DecoderHook::new( - self.eth_adapters.cheap_clone(), - self.call_cache.cheap_clone(), - eth_call_gas, - ) - } } /// This is used in `EthereumAdapter::triggers_in_block`, called when re-processing a block for diff --git a/chain/near/src/chain.rs b/chain/near/src/chain.rs index b94ee48c551..db2474a25cf 100644 --- a/chain/near/src/chain.rs +++ b/chain/near/src/chain.rs @@ -286,8 +286,8 @@ impl Blockchain for Chain { .await } - fn runtime_adapter(&self) -> Arc> { - Arc::new(NoopRuntimeAdapter::default()) + fn runtime(&self) -> (Arc>, Self::DecoderHook) { + (Arc::new(NoopRuntimeAdapter::default()), NoopDecoderHook) } fn chain_client(&self) -> Arc> { @@ -304,10 +304,6 @@ impl Blockchain for Chain { ); Ok(Box::new(ingestor)) } - - fn decoder_hook(&self) -> Self::DecoderHook { - NoopDecoderHook - } } pub struct TriggersAdapter {} diff --git a/chain/starknet/src/chain.rs b/chain/starknet/src/chain.rs index d9831636ee4..9f5097a8c6b 100644 --- a/chain/starknet/src/chain.rs +++ b/chain/starknet/src/chain.rs @@ -155,8 +155,8 @@ impl Blockchain for Chain { .await } - fn runtime_adapter(&self) -> Arc> { - Arc::new(NoopRuntimeAdapter::default()) + fn runtime(&self) -> (Arc>, Self::DecoderHook) { + (Arc::new(NoopRuntimeAdapter::default()), NoopDecoderHook) } fn chain_client(&self) -> Arc> { @@ -173,10 +173,6 @@ impl Blockchain for Chain { ); Ok(Box::new(ingestor)) } - - fn decoder_hook(&self) -> Self::DecoderHook { - NoopDecoderHook - } } #[async_trait] diff --git a/chain/substreams/src/chain.rs b/chain/substreams/src/chain.rs index bb3c2229a98..a871d813e08 100644 --- a/chain/substreams/src/chain.rs +++ b/chain/substreams/src/chain.rs @@ -181,8 +181,8 @@ impl Blockchain for Chain { number, }) } - fn runtime_adapter(&self) -> Arc> { - Arc::new(NoopRuntimeAdapter::default()) + fn runtime(&self) -> (Arc>, Self::DecoderHook) { + (Arc::new(NoopRuntimeAdapter::default()), NoopDecoderHook) } fn chain_client(&self) -> Arc> { @@ -198,10 +198,6 @@ impl Blockchain for Chain { self.metrics_registry.cheap_clone(), ))) } - - fn decoder_hook(&self) -> Self::DecoderHook { - NoopDecoderHook - } } impl blockchain::BlockchainBuilder for BasicBlockchainBuilder { diff --git a/core/src/subgraph/instance_manager.rs b/core/src/subgraph/instance_manager.rs index 83021bddd4b..223b855d132 100644 --- a/core/src/subgraph/instance_manager.rs +++ b/core/src/subgraph/instance_manager.rs @@ -390,8 +390,9 @@ impl SubgraphInstanceManager { let deployment_head = store.block_ptr().map(|ptr| ptr.number).unwrap_or(0) as f64; block_stream_metrics.deployment_head.set(deployment_head); + let (runtime_adapter, decoder_hook) = chain.runtime(); let host_builder = graph_runtime_wasm::RuntimeHostBuilder::new( - chain.runtime_adapter(), + runtime_adapter, self.link_resolver.cheap_clone(), subgraph_store.ens_lookup(), ); @@ -409,7 +410,7 @@ impl SubgraphInstanceManager { let instrument = self.subgraph_store.instrument(&deployment)?; - let decoder = Box::new(Decoder::new(chain.as_ref())); + let decoder = Box::new(Decoder::new(decoder_hook)); let inputs = IndexingInputs { deployment: deployment.clone(), diff --git a/core/src/subgraph/trigger_processor.rs b/core/src/subgraph/trigger_processor.rs index 6b19505c57c..0057e9e1354 100644 --- a/core/src/subgraph/trigger_processor.rs +++ b/core/src/subgraph/trigger_processor.rs @@ -107,9 +107,9 @@ where C: Blockchain, T: RuntimeHostBuilder, { - pub fn new(chain: &C) -> Self { + pub fn new(hook: C::DecoderHook) -> Self { Decoder { - hook: chain.decoder_hook(), + hook, _builder: PhantomData, } } diff --git a/graph/src/blockchain/mock.rs b/graph/src/blockchain/mock.rs index abfb67658b7..6bde388b99a 100644 --- a/graph/src/blockchain/mock.rs +++ b/graph/src/blockchain/mock.rs @@ -371,7 +371,7 @@ impl Blockchain for MockBlockchain { todo!() } - fn runtime_adapter(&self) -> std::sync::Arc> { + fn runtime(&self) -> (std::sync::Arc>, Self::DecoderHook) { todo!() } @@ -382,8 +382,4 @@ impl Blockchain for MockBlockchain { fn block_ingestor(&self) -> anyhow::Result> { todo!() } - - fn decoder_hook(&self) -> Self::DecoderHook { - todo!() - } } diff --git a/graph/src/blockchain/mod.rs b/graph/src/blockchain/mod.rs index 92df139a90f..b4ff1026590 100644 --- a/graph/src/blockchain/mod.rs +++ b/graph/src/blockchain/mod.rs @@ -206,13 +206,11 @@ pub trait Blockchain: Debug + Sized + Send + Sync + Unpin + 'static { fn is_refetch_block_required(&self) -> bool; - fn runtime_adapter(&self) -> Arc>; + fn runtime(&self) -> (Arc>, Self::DecoderHook); fn chain_client(&self) -> Arc>; fn block_ingestor(&self) -> anyhow::Result>; - - fn decoder_hook(&self) -> Self::DecoderHook; } #[derive(Error, Debug)] diff --git a/node/src/main.rs b/node/src/main.rs index d37a8190930..912ea9e86db 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,6 +1,9 @@ use clap::Parser as _; -use ethereum::chain::{EthereumAdapterSelector, EthereumBlockRefetcher, EthereumStreamBuilder}; -use ethereum::{BlockIngestor, EthereumNetworks, RuntimeAdapter}; +use ethereum::chain::{ + EthereumAdapterSelector, EthereumBlockRefetcher, EthereumRuntimeAdapterBuilder, + EthereumStreamBuilder, +}; +use ethereum::{BlockIngestor, EthereumNetworks}; use git_testament::{git_testament, render_testament}; use graph::blockchain::client::ChainClient; use graph_chain_ethereum::codec::HeaderOnlyBlock; @@ -867,17 +870,7 @@ fn ethereum_networks_as_chains( chain_store.clone(), ); - // Use a call cache that buffers in memory. It is important that - // the `RuntimeAdapter` and the `Chain` use the same call cache - // as that is used to pass the results of declared calls into - // mappings. - let call_cache = Arc::new(ethereum::BufferedCallCache::new(chain_store.cheap_clone())); - - let runtime_adapter = Arc::new(RuntimeAdapter { - eth_adapters: Arc::new(eth_adapters.clone()), - call_cache: call_cache.cheap_clone(), - chain_identifier: Arc::new(chain_store.chain_identifier.clone()), - }); + let call_cache = chain_store.cheap_clone(); let chain_config = config.chains.chains.get(network_name).unwrap(); let chain = ethereum::Chain::new( @@ -892,7 +885,7 @@ fn ethereum_networks_as_chains( Arc::new(EthereumStreamBuilder {}), Arc::new(EthereumBlockRefetcher {}), Arc::new(adapter_selector), - runtime_adapter, + Arc::new(EthereumRuntimeAdapterBuilder {}), Arc::new(eth_adapters.clone()), ENV_VARS.reorg_threshold, chain_config.polling_interval, diff --git a/node/src/manager/commands/run.rs b/node/src/manager/commands/run.rs index bce87d4449d..025c74b4a13 100644 --- a/node/src/manager/commands/run.rs +++ b/node/src/manager/commands/run.rs @@ -10,8 +10,11 @@ use crate::config::Config; use crate::manager::PanicSubscriptionManager; use crate::store_builder::StoreBuilder; use crate::MetricsContext; -use ethereum::chain::{EthereumAdapterSelector, EthereumBlockRefetcher, EthereumStreamBuilder}; -use ethereum::{ProviderEthRpcMetrics, RuntimeAdapter as EthereumRuntimeAdapter}; +use ethereum::chain::{ + EthereumAdapterSelector, EthereumBlockRefetcher, EthereumRuntimeAdapterBuilder, + EthereumStreamBuilder, +}; +use ethereum::ProviderEthRpcMetrics; use graph::anyhow::{bail, format_err}; use graph::blockchain::client::ChainClient; use graph::blockchain::{BlockchainKind, BlockchainMap}; @@ -125,7 +128,6 @@ pub async fn run( }; let eth_adapters2 = eth_adapters.clone(); - let eth_adapters3 = eth_adapters.clone(); let (_, ethereum_idents) = connect_ethereum_networks(&logger, eth_networks).await?; // let (near_networks, near_idents) = connect_firehose_networks::( // &logger, @@ -167,12 +169,8 @@ pub async fn run( metrics_registry.clone(), chain_store.cheap_clone(), )), - Arc::new(EthereumRuntimeAdapter { - call_cache, - eth_adapters: Arc::new(eth_adapters2), - chain_identifier: Arc::new(chain_store.chain_identifier.clone()), - }), - Arc::new(eth_adapters3), + Arc::new(EthereumRuntimeAdapterBuilder {}), + Arc::new(eth_adapters2), graph::env::ENV_VARS.reorg_threshold, chain_config.polling_interval, // We assume the tested chain is always ingestible for now diff --git a/tests/src/fixture/ethereum.rs b/tests/src/fixture/ethereum.rs index 8e0b4537164..faa100be7b2 100644 --- a/tests/src/fixture/ethereum.rs +++ b/tests/src/fixture/ethereum.rs @@ -3,8 +3,8 @@ use std::sync::{Arc, Mutex}; use std::time::Duration; use super::{ - test_ptr, CommonChainConfig, MutexBlockStreamBuilder, NoopAdapterSelector, NoopRuntimeAdapter, - StaticBlockRefetcher, StaticStreamBuilder, Stores, TestChain, + test_ptr, CommonChainConfig, MutexBlockStreamBuilder, NoopAdapterSelector, + NoopRuntimeAdapterBuilder, StaticBlockRefetcher, StaticStreamBuilder, Stores, TestChain, }; use graph::blockchain::client::ChainClient; use graph::blockchain::{BlockPtr, TriggersAdapterSelector}; @@ -59,7 +59,7 @@ pub async fn chain( block_stream_builder.clone(), Arc::new(StaticBlockRefetcher { x: PhantomData }), triggers_adapter, - Arc::new(NoopRuntimeAdapter { x: PhantomData }), + Arc::new(NoopRuntimeAdapterBuilder {}), eth_adapters, ENV_VARS.reorg_threshold, ENV_VARS.ingestor_polling_interval, diff --git a/tests/src/fixture/mod.rs b/tests/src/fixture/mod.rs index b83c9387c3a..19b8de69fe0 100644 --- a/tests/src/fixture/mod.rs +++ b/tests/src/fixture/mod.rs @@ -20,7 +20,7 @@ use graph::blockchain::{ use graph::cheap_clone::CheapClone; use graph::components::link_resolver::{ArweaveClient, ArweaveResolver, FileSizeLimit}; use graph::components::metrics::MetricsRegistry; -use graph::components::store::{BlockStore, DeploymentLocator}; +use graph::components::store::{BlockStore, DeploymentLocator, EthereumCallCache}; use graph::components::subgraph::Settings; use graph::data::graphql::load_manager::LoadManager; use graph::data::query::{Query, QueryTarget}; @@ -38,6 +38,8 @@ use graph::prelude::{ SubgraphStore as _, SubgraphVersionSwitchingMode, TriggerProcessor, }; use graph::schema::InputSchema; +use graph_chain_ethereum::chain::RuntimeAdapterBuilder; +use graph_chain_ethereum::network::EthereumNetworkAdapters; use graph_chain_ethereum::Chain; use graph_core::polling_monitor::{arweave_service, ipfs_service}; use graph_core::{ @@ -869,6 +871,19 @@ impl RuntimeAdapter for NoopRuntimeAdapter { } } +struct NoopRuntimeAdapterBuilder {} + +impl RuntimeAdapterBuilder for NoopRuntimeAdapterBuilder { + fn build( + &self, + _: Arc, + _: Arc, + _: Arc, + ) -> Arc + 'static> { + Arc::new(NoopRuntimeAdapter { x: PhantomData }) + } +} + pub struct NoopAdapterSelector { pub x: PhantomData, pub triggers_in_block_sleep: Duration,