Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
13617c5
all: Expose match_and_decode as a TriggerProcessor method
lutter Mar 1, 2024
dbedcc4
all: Introduce a name for a host and a trigger
lutter Mar 1, 2024
ea98c2b
all: Lift calling match_and_decode out of TriggerProcessor
lutter Mar 1, 2024
a60f36f
core: Refactor how IndexingContext.process_trigger_in_hosts is called
lutter Mar 1, 2024
75b6539
core: Assign the result of calling process_trigger_in_hosts to a vari…
lutter Mar 1, 2024
d277348
core: Inline all calls to IndexingContext.process_trigger_in_hosts
lutter Mar 1, 2024
7d247da
core: Restructure the loop in SubgraphRunner.process_block
lutter Mar 1, 2024
cbf5520
core: Split match_and_decode and processing into separate loops
lutter Mar 1, 2024
a025ad0
test: Use substreams::TriggerProcessor for substreams
lutter Mar 2, 2024
5859c21
chain, core, graph: Put match_and_decode into a different trait
lutter Mar 3, 2024
bb639c0
core: Split match_and_decode and processing for new data sources
lutter Mar 4, 2024
beff28b
core, graph: Simplify handling of match_and_decode result
lutter Mar 4, 2024
ce15f1f
core, graph: Decoder doesn't need to be a trait
lutter Mar 4, 2024
27d62be
all: Pass the spec version to DataSource.validate
lutter Feb 29, 2024
b01c1aa
chain/ethereum: Factor function look up into MappingABI
lutter Mar 5, 2024
dd9f6ec
all: Allow declaring eth calls on ethereum event handlers
lutter Feb 29, 2024
cbfbacb
all: Introduce a Blockchain::DecoderHook
lutter Mar 4, 2024
a293fd8
chain/ethereum: Add a buffered call cache
lutter Mar 5, 2024
a502528
graph, runtime: Expose the HostMetrics of a RuntimeHost
lutter Mar 5, 2024
cf94cf9
all: Add a decoder hook for ethereum
lutter Mar 5, 2024
f25610a
chain, graph: Allow turning off declared calls
lutter Mar 5, 2024
daa25f5
chain: Make eth calls in parallel
lutter Mar 5, 2024
1003ab8
chain/ethereum: Asyncify EthereumAdapter.contract_call
lutter Mar 6, 2024
3e40cbd
chain/ethereum: Pass EthereumCall by reference
lutter Mar 6, 2024
2a54222
chain/ethereum: Report time for declared eth calls in their own section
lutter Mar 6, 2024
b613148
all: Clarify whether an eth call was never made or had no result
lutter Mar 6, 2024
8bfc61e
chain, graph, store: Use Bytes instad of Vec<u8> for eth_call results
lutter Mar 6, 2024
7f5a671
chain/ethereum: Cache failed call results
lutter Mar 6, 2024
2581185
chain/ethereum, graph, store: Avoid double-counting declared calls
lutter Mar 6, 2024
d94916a
chain/ethereum: Deduplicate declared eth calls
lutter Mar 6, 2024
5681367
chain/ethereum: Provide the correct amount of gas for declared calls
lutter Mar 6, 2024
1910a11
chain, graph, store: Address race condition in caching eth calls
lutter Mar 7, 2024
c74cbba
chain, graph: Change how we log eth calls
lutter Mar 7, 2024
7405863
chain: Log failures of declared calls
lutter Mar 7, 2024
b37e583
docs: Add declared calls to the manifest spec
lutter Mar 8, 2024
272aa36
all: Introduce a struct for the data needed for an eth_call
lutter Mar 8, 2024
02bec43
all: Move some of the helper structs for handling eth calls around
lutter Mar 8, 2024
e4aa6f0
all: Struct for return value and source for a eth_call response
lutter Mar 8, 2024
2703680
chain/ethereum: Factor out generating the eth_call request
lutter Mar 8, 2024
cb737f9
chain/ethereum: Further factor EthereumAdapter.contract_call
lutter Mar 8, 2024
70f719f
chain, graph, store: Relate call requests to calls via an index
lutter Mar 8, 2024
e6c09c4
chain/ethereum: Return responses in the right order
lutter Mar 8, 2024
c8c8161
chain/ethereum: Add an EthereumAdapter.contract_calls
lutter Mar 8, 2024
73e8e68
chain, graph, store: Lookup eth_calls in bulk
lutter Mar 8, 2024
1ab4a66
chain/ethereum: Look up calls from the cache in bulk
lutter Mar 8, 2024
ce64824
chain/ethereum: Parallelize RPC calls in EthereumAapter.contract_calls
lutter Mar 8, 2024
9f467bc
chain/ethereum: Strip the `Ethereum` prefix of a few types
lutter Mar 9, 2024
bd5ae84
chain/ethereum: Decoding call results never produces an error
lutter Mar 9, 2024
4527ad7
chain/ethereum: Log errors for individual calls in EthereumAdapter
lutter Mar 9, 2024
ffd52c2
chain/ethereum: Change how we parallelize eth calls
lutter Mar 9, 2024
1973985
all: Change initialization of RuntimeAdapter and DecoderHook
lutter Mar 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions chain/arweave/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,6 +88,8 @@ impl Blockchain for Chain {

type NodeCapabilities = EmptyNodeCapabilities<Self>;

type DecoderHook = NoopDecoderHook;

fn triggers_adapter(
&self,
_loc: &DeploymentLocator,
Expand Down Expand Up @@ -161,8 +163,8 @@ impl Blockchain for Chain {
.map_err(Into::into)
}

fn runtime_adapter(&self) -> Arc<dyn RuntimeAdapterTrait<Self>> {
Arc::new(NoopRuntimeAdapter::default())
fn runtime(&self) -> (Arc<dyn RuntimeAdapterTrait<Self>>, Self::DecoderHook) {
(Arc::new(NoopRuntimeAdapter::default()), NoopDecoderHook)
}

fn chain_client(&self) -> Arc<ChainClient<Self>> {
Expand Down
2 changes: 1 addition & 1 deletion chain/arweave/src/data_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ impl blockchain::DataSource<Chain> for DataSource {
todo!()
}

fn validate(&self) -> Vec<Error> {
fn validate(&self, _: &semver::Version) -> Vec<Error> {
let mut errors = Vec::new();

if self.kind != ARWEAVE_KIND {
Expand Down
8 changes: 5 additions & 3 deletions chain/cosmos/src/chain.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -82,6 +82,8 @@ impl Blockchain for Chain {

type NodeCapabilities = EmptyNodeCapabilities<Self>;

type DecoderHook = NoopDecoderHook;

fn is_refetch_block_required(&self) -> bool {
false
}
Expand Down Expand Up @@ -155,8 +157,8 @@ impl Blockchain for Chain {
.map_err(Into::into)
}

fn runtime_adapter(&self) -> Arc<dyn RuntimeAdapterTrait<Self>> {
Arc::new(NoopRuntimeAdapter::default())
fn runtime(&self) -> (Arc<dyn RuntimeAdapterTrait<Self>>, Self::DecoderHook) {
(Arc::new(NoopRuntimeAdapter::default()), NoopDecoderHook)
}

fn chain_client(&self) -> Arc<ChainClient<Self>> {
Expand Down
8 changes: 4 additions & 4 deletions chain/cosmos/src/data_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ impl blockchain::DataSource<Chain> for DataSource {
Err(anyhow!(DYNAMIC_DATA_SOURCE_ERROR))
}

fn validate(&self) -> Vec<Error> {
fn validate(&self, _: &semver::Version) -> Vec<Error> {
let mut errors = Vec::new();

if self.kind != COSMOS_KIND {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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());

Expand Down Expand Up @@ -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());

Expand Down
53 changes: 43 additions & 10 deletions chain/ethereum/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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;
use graph::firehose::LogFilter;
Expand Down Expand Up @@ -36,7 +38,8 @@ pub type EventSignature = H256;
pub type FunctionSelector = [u8; 4];

#[derive(Clone, Debug)]
pub struct EthereumContractCall {
pub struct ContractCall {
pub contract_name: String,
pub address: Address,
pub block_ptr: BlockPtr,
pub function: Function,
Expand All @@ -45,15 +48,15 @@ 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")]
Timeout,
}

#[derive(Error, Debug)]
pub enum EthereumContractCallError {
pub enum ContractCallError {
#[error("ABI error: {0}")]
ABIError(#[from] ABIError),
/// `Token` is not of expected `ParamType`
Expand All @@ -63,10 +66,28 @@ 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,
#[error("internal error: {0}")]
Internal(String),
}

impl From<ContractCallError> 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)]
Expand Down Expand Up @@ -964,20 +985,32 @@ pub trait EthereumAdapter: Send + Sync + 'static {
block_number: BlockNumber,
) -> Box<dyn Future<Item = Option<H256>, Error = Error> + Send>;

/// Call the function of a smart contract.
fn contract_call(
/// Call the function of a smart contract. A return of `None` indicates
/// that the call reverted. The returned `CallSource` indicates where
/// the result came from for accounting purposes
async fn contract_call(
&self,
logger: &Logger,
call: &ContractCall,
cache: Arc<dyn EthereumCallCache>,
) -> Result<(Option<Vec<Token>>, 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
/// calls must all be for the same block
async fn contract_calls(
&self,
logger: &Logger,
call: EthereumContractCall,
calls: &[&ContractCall],
cache: Arc<dyn EthereumCallCache>,
) -> Box<dyn Future<Item = Vec<Token>, Error = EthereumContractCallError> + Send>;
) -> Result<Vec<(Option<Vec<Token>>, call::Source)>, ContractCallError>;

fn get_balance(
&self,
logger: &Logger,
address: H160,
block_ptr: BlockPtr,
) -> Box<dyn Future<Item = U256, Error = EthereumGetBalanceError> + Send>;
) -> Box<dyn Future<Item = U256, Error = GetBalanceError> + Send>;
}

#[cfg(test)]
Expand Down
145 changes: 145 additions & 0 deletions chain/ethereum/src/buffered_call_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};

use graph::{
cheap_clone::CheapClone,
components::store::EthereumCallCache,
data::store::ethereum::call,
prelude::{BlockPtr, CachedEthereumCall},
slog::{error, Logger},
};

/// 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<dyn EthereumCallCache>,
buffer: Arc<Mutex<HashMap<call::Request, call::Retval>>>,
block: Arc<Mutex<Option<BlockPtr>>>,
}

impl BufferedCallCache {
pub fn new(call_cache: Arc<dyn EthereumCallCache>) -> Self {
Self {
call_cache,
buffer: Arc::new(Mutex::new(HashMap::new())),
block: Arc::new(Mutex::new(None)),
}
}

fn check_block(&self, block: &BlockPtr) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of "check_block" something like "clear_block_if_different" or something would make it more clear while reading callsites

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();
}
}

fn get(&self, call: &call::Request) -> Option<call::Response> {
let buffer = self.buffer.lock().unwrap();
buffer.get(call).map(|retval| {
call.cheap_clone()
.response(retval.clone(), call::Source::Memory)
})
}
}

impl EthereumCallCache for BufferedCallCache {
fn get_call(
&self,
call: &call::Request,
block: BlockPtr,
) -> Result<Option<call::Response>, graph::prelude::Error> {
self.check_block(&block);

if let Some(value) = self.get(call) {
return Ok(Some(value));
}

let result = self.call_cache.get_call(&call, block)?;

let mut buffer = self.buffer.lock().unwrap();
if let Some(call::Response {
retval,
req: _,
source: _,
}) = &result
{
buffer.insert(call.cheap_clone(), retval.clone());
}
Ok(result)
}

fn get_calls(
&self,
reqs: &[call::Request],
block: BlockPtr,
) -> Result<(Vec<call::Response>, Vec<call::Request>), 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,
) -> Result<Vec<CachedEthereumCall>, graph::prelude::Error> {
self.call_cache.get_calls_in_block(block)
}

fn set_call(
&self,
logger: &Logger,
call: call::Request,
block: BlockPtr,
return_value: call::Retval,
) -> Result<(), graph::prelude::Error> {
self.check_block(&block);

// 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(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, call.cheap_clone(), block, return_value)
.map_err(|e| {
error!(logger, "BufferedCallCache: call cache set error";
"contract_address" => format!("{:?}", call.address),
"error" => e.to_string())
})
});

Ok(())
}
}
Loading