Skip to content

Migrate from rust-web3 to alloy #6063

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d40bb7c
migrate from `ethabi` to `alloy`
isum Feb 14, 2025
1eb13ac
bump crate version
zorancv May 7, 2025
b65b095
Add alloy_rpc_types crate
incrypto32 Jun 18, 2025
4211416
chain/ethereum: Add alloy provider to ethereum adapter
incrypto32 Jun 18, 2025
9226501
chain/ethereum: Fix retval import causing rustfmt issues
incrypto32 Jun 18, 2025
cf1b875
chain/ethereum: migrate get_balance call to use alloy
incrypto32 Jun 18, 2025
d592acc
chain/ethereum: migrate get_code call to use alloy
incrypto32 Jun 18, 2025
3035f4e
chain/ethereum: remove unused block_hash_by_block_number method
incrypto32 Jun 18, 2025
3327f3c
chain/ethereum: migrate chain_id call to use alloy
incrypto32 Jun 18, 2025
004f986
chain/ethereum: migrate net_version call to use alloy
incrypto32 Jun 18, 2025
ed10f44
chain/ethereum: migrate next_existing_ptr_to_number call to use alloy
incrypto32 Jun 18, 2025
8d432a6
chain/ethereum: migrate load_block_ptrs_by_numbers_rpc call to use alloy
incrypto32 Jun 18, 2025
d177734
chain/ethereum: migrate load_block_ptrs_rpc call to use alloy
incrypto32 Jun 18, 2025
d806e62
graph, chain, runtime: re export alloy from graph crate and use it
incrypto32 Jun 18, 2025
40009f8
chain/ethereum: migrate genesis block fetching call to use alloy in n…
incrypto32 Jun 18, 2025
617a847
chain/ethereum: migrate latest_block_header call to use alloy
incrypto32 Jun 18, 2025
ad48ab7
chain/ethereum: rename latest_block_header to latest_block_ptr
incrypto32 Jun 18, 2025
ee073ea
chain/ethereum: remove unused latest_block adapter method
incrypto32 Jun 18, 2025
255d227
chain/ethereum: Refactor eth_call method in ethereum adapter
incrypto32 Jun 19, 2025
28c39c2
chain/ethereum: Move eth_call helper functions to separate module
incrypto32 Jun 19, 2025
2e97467
chain/ethereum: Migrate eth_call to use alloy
incrypto32 Jun 19, 2025
d9c4ecd
graph, chain/ethereum: migrate ContractCall to use alloy address
incrypto32 Jun 19, 2025
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
3,576 changes: 2,634 additions & 942 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ repository = "https://github.com/graphprotocol/graph-node"
license = "MIT OR Apache-2.0"

[workspace.dependencies]
alloy = { version = "0.15.10", features = ["full"] }
alloy-rpc-types = "0.15.10"
anyhow = "1.0"
async-graphql = { version = "7.0.15", features = ["chrono"] }
async-graphql-axum = "7.0.15"
Expand Down
61 changes: 22 additions & 39 deletions chain/ethereum/src/adapter.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
use anyhow::Error;
use ethabi::{Error as ABIError, ParamType, Token};
use graph::abi;
use graph::blockchain::ChainIdentifier;
use graph::components::subgraph::MappingError;
use graph::data::store::ethereum::call;
use graph::data_source::common::ContractCall;
use graph::firehose::CallToFilter;
use graph::firehose::CombinedFilter;
use graph::firehose::LogFilter;
use graph::prelude::web3::types::Bytes;
use graph::prelude::alloy::transports::{RpcError, TransportErrorKind};
use graph::prelude::web3::types::H160;
use graph::prelude::web3::types::U256;
use itertools::Itertools;
use prost::Message;
use prost_types::Any;
Expand All @@ -27,6 +26,8 @@ use graph::{
petgraph::{self, graphmap::GraphMap},
};

use graph::blockchain::BlockPtr;

const COMBINED_FILTER_TYPE_URL: &str =
"type.googleapis.com/sf.ethereum.transform.v1.CombinedFilter";

Expand Down Expand Up @@ -95,21 +96,24 @@ impl EventSignatureWithTopics {
pub enum EthereumRpcError {
#[error("call error: {0}")]
Web3Error(web3::Error),
#[error("call error: {0}")]
AlloyError(RpcError<TransportErrorKind>),
#[error("ethereum node took too long to perform call")]
Timeout,
}

#[derive(Error, Debug)]
pub enum ContractCallError {
#[error("ABI error: {0}")]
ABIError(#[from] ABIError),
/// `Token` is not of expected `ParamType`
#[error("type mismatch, token {0:?} is not of kind {1:?}")]
TypeError(Token, ParamType),
#[error("error encoding input call data: {0}")]
EncodingError(ethabi::Error),
#[error("ABI error: {0:#}")]
ABIError(anyhow::Error),
#[error("type mismatch, decoded value {0:?} is not of kind {1:?}")]
TypeError(abi::DynSolValue, abi::DynSolType),
#[error("error encoding input call data: {0:#}")]
EncodingError(anyhow::Error),
#[error("call error: {0}")]
Web3Error(web3::Error),
#[error("call error: {0}")]
AlloyError(RpcError<TransportErrorKind>),
#[error("ethereum node took too long to perform call")]
Timeout,
#[error("internal error: {0}")]
Expand Down Expand Up @@ -1079,14 +1083,8 @@ pub trait EthereumAdapter: Send + Sync + 'static {
/// connected to.
async fn net_identifiers(&self) -> Result<ChainIdentifier, Error>;

/// Get the latest block, including full transactions.
async fn latest_block(&self, logger: &Logger) -> Result<LightEthereumBlock, bc::IngestorError>;

/// Get the latest block, with only the header and transaction hashes.
async fn latest_block_header(
&self,
logger: &Logger,
) -> Result<web3::types::Block<H256>, bc::IngestorError>;
async fn latest_block_ptr(&self, logger: &Logger) -> Result<BlockPtr, bc::IngestorError>;

async fn load_block(
&self,
Expand Down Expand Up @@ -1123,21 +1121,6 @@ pub trait EthereumAdapter: Send + Sync + 'static {
block: LightEthereumBlock,
) -> Result<EthereumBlock, bc::IngestorError>;

/// Find a block by its number, according to the Ethereum node.
///
/// Careful: don't use this function without considering race conditions.
/// Chain reorgs could happen at any time, and could affect the answer received.
/// Generally, it is only safe to use this function with blocks that have received enough
/// confirmations to guarantee no further reorgs, **and** where the Ethereum node is aware of
/// those confirmations.
/// If the Ethereum node is far behind in processing blocks, even old blocks can be subject to
/// reorgs.
async fn block_hash_by_block_number(
&self,
logger: &Logger,
block_number: BlockNumber,
) -> Result<Option<H256>, Error>;

/// Finds the hash and number of the lowest non-null block with height greater than or equal to
/// the given number.
///
Expand All @@ -1157,7 +1140,7 @@ pub trait EthereumAdapter: Send + Sync + 'static {
logger: &Logger,
call: &ContractCall,
cache: Arc<dyn EthereumCallCache>,
) -> Result<(Option<Vec<Token>>, call::Source), ContractCallError>;
) -> Result<(Option<Vec<abi::DynSolValue>>, 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
Expand All @@ -1167,22 +1150,22 @@ pub trait EthereumAdapter: Send + Sync + 'static {
logger: &Logger,
calls: &[&ContractCall],
cache: Arc<dyn EthereumCallCache>,
) -> Result<Vec<(Option<Vec<Token>>, call::Source)>, ContractCallError>;
) -> Result<Vec<(Option<Vec<abi::DynSolValue>>, call::Source)>, ContractCallError>;

async fn get_balance(
&self,
logger: &Logger,
address: H160,
address: alloy::primitives::Address,
block_ptr: BlockPtr,
) -> Result<U256, EthereumRpcError>;
) -> Result<alloy::primitives::U256, EthereumRpcError>;

// Returns the compiled bytecode of a smart contract
async fn get_code(
&self,
logger: &Logger,
address: H160,
address: alloy::primitives::Address,
block_ptr: BlockPtr,
) -> Result<Bytes, EthereumRpcError>;
) -> Result<alloy::primitives::Bytes, EthereumRpcError>;
}

#[cfg(test)]
Expand All @@ -1196,9 +1179,9 @@ mod tests {
use graph::blockchain::TriggerFilter as _;
use graph::firehose::{CallToFilter, CombinedFilter, LogFilter, MultiLogFilter};
use graph::petgraph::graphmap::GraphMap;
use graph::prelude::ethabi::ethereum_types::H256;
use graph::prelude::web3::types::Address;
use graph::prelude::web3::types::Bytes;
use graph::prelude::web3::types::H256;
use graph::prelude::EthereumCall;
use hex::ToHex;
use itertools::Itertools;
Expand Down
138 changes: 138 additions & 0 deletions chain/ethereum/src/call_helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use crate::{ContractCallError, ENV_VARS};
use graph::{
abi,
data::store::ethereum::call,
prelude::{
alloy::transports::{RpcError, TransportErrorKind},
Logger,
},
slog::info,
};

// ------------------------------------------------------------------
// Constants and helper utilities used across eth_call handling
// ------------------------------------------------------------------

// Try to check if the call was reverted. The JSON-RPC response for reverts is
// not standardized, so we have ad-hoc checks for each Ethereum client.

// 0xfe is the "designated bad instruction" of the EVM, and Solidity uses it for
// asserts.
const PARITY_BAD_INSTRUCTION_FE: &str = "Bad instruction fe";

// 0xfd is REVERT, but on some contracts, and only on older blocks,
// this happens. Makes sense to consider it a revert as well.
const PARITY_BAD_INSTRUCTION_FD: &str = "Bad instruction fd";

const PARITY_BAD_JUMP_PREFIX: &str = "Bad jump";
const PARITY_STACK_LIMIT_PREFIX: &str = "Out of stack";

// See f0af4ab0-6b7c-4b68-9141-5b79346a5f61.
const PARITY_OUT_OF_GAS: &str = "Out of gas";

// Also covers Nethermind reverts
const PARITY_VM_EXECUTION_ERROR: i64 = -32015;
const PARITY_REVERT_PREFIX: &str = "revert";

const XDAI_REVERT: &str = "revert";

// Deterministic Geth execution errors. We might need to expand this as
// subgraphs come across other errors. See
// https://github.com/ethereum/go-ethereum/blob/cd57d5cd38ef692de8fbedaa56598b4e9fbfbabc/core/vm/errors.go
const GETH_EXECUTION_ERRORS: &[&str] = &[
// The "revert" substring covers a few known error messages, including:
// Hardhat: "error: transaction reverted",
// Ganache and Moonbeam: "vm exception while processing transaction: revert",
// Geth: "execution reverted"
// And others.
"revert",
"invalid jump destination",
"invalid opcode",
// Ethereum says 1024 is the stack sizes limit, so this is deterministic.
"stack limit reached 1024",
// See f0af4ab0-6b7c-4b68-9141-5b79346a5f61 for why the gas limit is considered deterministic.
"out of gas",
"stack underflow",
];

/// Helper that checks if a geth style RPC error message corresponds to a revert.
fn is_geth_revert_message(message: &str) -> bool {
let env_geth_call_errors = ENV_VARS.geth_eth_call_errors.iter();
let mut execution_errors = GETH_EXECUTION_ERRORS
.iter()
.copied()
.chain(env_geth_call_errors.map(|s| s.as_str()));
execution_errors.any(|e| message.to_lowercase().contains(e))
}

/// Decode a Solidity revert(reason) payload, returning the reason string when possible.
fn as_solidity_revert_reason(bytes: &[u8]) -> Option<String> {
let selector = &tiny_keccak::keccak256(b"Error(string)")[..4];
if bytes.len() >= 4 && &bytes[..4] == selector {
abi::DynSolType::String
.abi_decode(&bytes[4..])
.ok()
.and_then(|val| val.clone().as_str().map(ToOwned::to_owned))
} else {
None
}
}

/// Interpret the error returned by `eth_call`, distinguishing genuine failures from
/// EVM reverts. Returns `Ok(Null)` for reverts or a proper error otherwise.
pub fn interpret_eth_call_error(
logger: &Logger,
err: RpcError<TransportErrorKind>,
) -> Result<call::Retval, ContractCallError> {
fn reverted(logger: &Logger, reason: &str) -> Result<call::Retval, ContractCallError> {
info!(logger, "Contract call reverted"; "reason" => reason);
Ok(call::Retval::Null)
}

if let RpcError::ErrorResp(rpc_error) = &err {
if is_geth_revert_message(&rpc_error.message) {
return reverted(logger, &rpc_error.message);
}
}

if let RpcError::ErrorResp(rpc_error) = &err {
let code = rpc_error.code;
let data = rpc_error.data.as_ref().map(|d| d.to_string());

if code == PARITY_VM_EXECUTION_ERROR {
if let Some(data) = data {
if is_parity_revert(&data) {
return reverted(logger, &parity_revert_reason(&data));
}
}
}
}

Err(ContractCallError::AlloyError(err))
}

fn is_parity_revert(data: &str) -> bool {
data.to_lowercase().starts_with(PARITY_REVERT_PREFIX)
|| data.starts_with(PARITY_BAD_JUMP_PREFIX)
|| data.starts_with(PARITY_STACK_LIMIT_PREFIX)
|| data == PARITY_BAD_INSTRUCTION_FE
|| data == PARITY_BAD_INSTRUCTION_FD
|| data == PARITY_OUT_OF_GAS
|| data == XDAI_REVERT
}

/// Checks if the given `web3::Error` corresponds to a Parity / Nethermind style EVM
/// revert and, if so, tries to extract a human-readable revert reason. Returns `Some`
/// with the reason when the error is identified as a revert, otherwise `None`.
fn parity_revert_reason(data: &str) -> String {
if data == PARITY_BAD_INSTRUCTION_FE {
return PARITY_BAD_INSTRUCTION_FE.to_owned();
}

// Otherwise try to decode a Solidity revert reason payload.
let payload = data.trim_start_matches(PARITY_REVERT_PREFIX);
hex::decode(payload)
.ok()
.and_then(|decoded| as_solidity_revert_reason(&decoded))
.unwrap_or_else(|| "no reason".to_owned())
}
Loading
Loading