Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# NEWS

## Unreleased

### What's New

- **Rust subgraph ABI support (draft, [#6462](https://github.com/graphprotocol/graph-node/pull/6462)).** A new `rust_abi/` module (~1,450 LOC) enables subgraphs compiled from Rust to `wasm32-unknown-unknown`. Selected via `mapping.kind: wasm/rust` in `subgraph.yaml`. Key properties:
- Clean `ptr+len` calling convention — no managed heap, no `AscPtr<T>` juggling.
- TLV entity serialization with a closed tag table; tag bytes are named constants (`tags::*` in `rust_abi/types.rs`).
- Fixed-layout trigger serialization for Ethereum log, call, and block handlers. NEAR has a documented stub with a `0xFF` sentinel.
- Wasmtime fuel metering (10 billion units per handler; `Trap::OutOfFuel` is a deterministic error).
- Language detection via manifest `mapping.kind`; `is_rust_module()` cross-checks by scanning the `graphite` import namespace.
- Bypasses the `parity_wasm` gas-injection pipeline (incompatible with modern WASM opcodes); gas is provided entirely by wasmtime fuel.
- Formal ABI specification: `docs/rust-abi-spec.md`.
- 12 unit tests; ABI test vectors cross-validated against the Graphite SDK.
- Live-tested: USDC Transfer events from Ethereum mainnet indexed end-to-end via GraphQL.

## v0.42.1

### Bug Fixes
Expand Down
12 changes: 12 additions & 0 deletions chain/ethereum/src/ethereum_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,18 @@ impl EthereumAdapter {
.map_err(|e| e.into_inner().unwrap_or(ContractCallError::Timeout))
}

/// Make a raw eth_call without ABI encoding.
/// Used by Rust ABI subgraphs where the SDK handles encoding/decoding.
pub async fn raw_call(
&self,
req: call::Request,
block_ptr: BlockPtr,
gas: Option<u32>,
) -> Result<call::Retval, ContractCallError> {
let logger = self.provider_logger(&self.logger);
self.call(logger, req, block_ptr, gas).await
}

async fn call_and_cache(
&self,
logger: &ProviderLogger,
Expand Down
98 changes: 96 additions & 2 deletions chain/ethereum/src/runtime/runtime_adapter.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::{sync::Arc, time::Instant};

use async_trait::async_trait;

use crate::adapter::EthereumRpcError;
use crate::{
capabilities::NodeCapabilities, network::EthereumNetworkAdapters, Chain, ContractCallError,
Expand All @@ -9,7 +11,7 @@ use anyhow::{anyhow, Context, Error};
use blockchain::HostFn;
use graph::abi;
use graph::abi::DynSolValueExt;
use graph::blockchain::ChainIdentifier;
use graph::blockchain::{ChainIdentifier, RawEthCall};
use graph::components::subgraph::HostMetrics;
use graph::data::store::ethereum::call;
use graph::data::store::scalar::BigInt;
Expand All @@ -18,7 +20,7 @@ use graph::data_source;
use graph::data_source::common::{ContractCall, MappingABI};
use graph::runtime::gas::Gas;
use graph::runtime::{AscIndexId, IndexForAscTypeId};
use graph::slog::debug;
use graph::slog::{debug, o, Discard};
use graph::{
blockchain::{self, BlockPtr, HostFnCtx},
cheap_clone::CheapClone,
Expand Down Expand Up @@ -185,6 +187,98 @@ impl blockchain::RuntimeAdapter<Chain> for RuntimeAdapter {

Ok(host_fns)
}

fn raw_eth_call(&self) -> Option<Arc<dyn RawEthCall>> {
Some(Arc::new(EthereumRawEthCall {
eth_adapters: self.eth_adapters.cheap_clone(),
call_cache: self.call_cache.cheap_clone(),
eth_call_gas: eth_call_gas(&self.chain_identifier),
}))
}
}

/// Implementation of RawEthCall for Ethereum chains.
/// Used by Rust ABI subgraphs for making raw eth_call without ABI encoding.
pub struct EthereumRawEthCall {
eth_adapters: Arc<EthereumNetworkAdapters>,
call_cache: Arc<dyn EthereumCallCache>,
eth_call_gas: Option<u32>,
}

#[async_trait]
impl RawEthCall for EthereumRawEthCall {
async fn call(
&self,
address: [u8; 20],
calldata: &[u8],
block_ptr: &BlockPtr,
gas: Option<u32>,
) -> Result<Option<Vec<u8>>, HostExportError> {
// Get an adapter suitable for calls (non-archive is fine)
let eth_adapter = self
.eth_adapters
.call_or_cheapest(Some(&NodeCapabilities {
archive: false,
traces: false,
}))
.map_err(HostExportError::Unknown)?;

// Create a raw call request
let req = call::Request::new(Address::from(address), calldata.to_vec(), 0);

// Check cache first
let (cached, _missing) = self
.call_cache
.get_calls(&[req.cheap_clone()], block_ptr.cheap_clone())
.await
.unwrap_or_else(|_| (Vec::new(), vec![req.cheap_clone()]));

if let Some(resp) = cached.into_iter().next() {
return match resp.retval {
call::Retval::Value(bytes) => Ok(Some(bytes.to_vec())),
call::Retval::Null => Ok(None),
};
}

// Make the actual call
let result = eth_adapter
.raw_call(
req.cheap_clone(),
block_ptr.cheap_clone(),
gas.or(self.eth_call_gas),
)
.await;

match result {
Ok(retval) => {
// Cache the result
let cache = self.call_cache.cheap_clone();
let _ = cache
.set_call(
&Logger::root(Discard, o!()),
req,
block_ptr.cheap_clone(),
retval.clone(),
)
.await;

match retval {
call::Retval::Value(bytes) => Ok(Some(bytes.to_vec())),
call::Retval::Null => Ok(None),
}
}
Err(ContractCallError::AlloyError(e)) => Err(HostExportError::PossibleReorg(
anyhow::anyhow!("eth_call RPC error: {}", e),
)),
Err(ContractCallError::Timeout) => Err(HostExportError::PossibleReorg(
anyhow::anyhow!("eth_call timed out"),
)),
Err(e) => Err(HostExportError::Unknown(anyhow::anyhow!(
"eth_call failed: {}",
e
))),
}
}
}

/// function ethereum.call(call: SmartContractCall): Array<Token> | null
Expand Down
69 changes: 69 additions & 0 deletions chain/ethereum/src/trigger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ use graph::runtime::AscPtr;
use graph::runtime::HostExportError;
use graph::semver::Version;
use graph_runtime_wasm::module::ToAscPtr;
use graph_runtime_wasm::rust_abi::{
RustBlockTrigger, RustCallTrigger, RustLogTrigger, ToRustBytes,
};
use std::{cmp::Ordering, sync::Arc};

use crate::runtime::abi::AscEthereumBlock;
Expand Down Expand Up @@ -649,3 +652,69 @@ impl<'a> EthereumCallData<'a> {
&self.call.to
}
}

// ============================================================================
// Rust ABI serialization for Graphite SDK
// ============================================================================

impl ToRustBytes for MappingTrigger {
fn to_rust_bytes(&self) -> Vec<u8> {
match self {
MappingTrigger::Log {
block,
transaction,
log,
params: _,
receipt: _,
calls: _,
} => {
let rust_trigger = RustLogTrigger {
address: log.inner.address.0 .0,
tx_hash: transaction.tx_hash().0,
log_index: log.log_index.unwrap_or(0),
block_number: block.number_u64(),
block_timestamp: block.inner().header.timestamp,
topics: log.inner.data.topics().iter().map(|t| t.0).collect(),
data: log.inner.data.data.to_vec(),
};
rust_trigger.to_rust_bytes()
}
MappingTrigger::Call {
block,
transaction,
call,
inputs: _,
outputs: _,
} => {
let rust_trigger = RustCallTrigger {
to: call.to.0 .0,
from: call.from.0 .0,
tx_hash: transaction.tx_hash().0,
block_number: block.number_u64(),
block_timestamp: block.inner().header.timestamp,
block_hash: block.inner().header.hash.0,
input: call.input.to_vec(),
output: call.output.to_vec(),
};
rust_trigger.to_rust_bytes()
}
MappingTrigger::Block { block } => {
// Convert U256 difficulty to big-endian bytes
let difficulty: [u8; 32] = block.inner().header.difficulty.to_be_bytes();

let rust_trigger = RustBlockTrigger {
hash: block.inner().header.hash.0,
parent_hash: block.inner().header.parent_hash.0,
number: block.number_u64(),
timestamp: block.inner().header.timestamp,
author: block.inner().header.beneficiary.0 .0,
gas_used: block.inner().header.gas_used,
gas_limit: block.inner().header.gas_limit,
difficulty,
base_fee_per_gas: block.inner().header.base_fee_per_gas.unwrap_or(0),
};
rust_trigger.to_rust_bytes()
}
}
}
}
47 changes: 47 additions & 0 deletions chain/near/src/trigger.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use graph::prelude::BlockNumber;
use graph::runtime::HostExportError;
use graph::runtime::{asc_new, gas::GasCounter, AscHeap, AscPtr};
use graph_runtime_wasm::module::ToAscPtr;
use graph_runtime_wasm::rust_abi::ToRustBytes;
use std::{cmp::Ordering, sync::Arc};

use crate::codec;
Expand Down Expand Up @@ -143,6 +144,52 @@ impl MappingTriggerTrait for NearTrigger {
}
}

/// Sentinel header byte returned by [`NearTrigger::to_rust_bytes`] to
/// signal "unsupported chain" to any SDK that attempts to decode a NEAR
/// trigger payload. Chosen as `0xFF` because it is outside the current
/// TLV value-tag range (`0x00`..=`0x09`, see
/// `runtime/wasm/src/rust_abi/types.rs::tags`) and therefore guarantees a
/// deterministic decode failure rather than silent misinterpretation.
const UNSUPPORTED_CHAIN_SENTINEL: u8 = 0xFF;

impl ToRustBytes for NearTrigger {
/// Produce a Rust ABI payload for a NEAR trigger.
///
/// NEAR triggers are **not** yet wired up to the Rust ABI. A real
/// implementation would need to serialise, at minimum:
///
/// - For [`NearTrigger::Block`]: the block header fields (hash,
/// prev_hash, height, timestamp, author, gas_price, total_supply,
/// etc.) plus any chunk metadata the SDK exposes to block handlers.
/// - For [`NearTrigger::Receipt`]: the receipt id, predecessor /
/// receiver account ids, the enclosing block's hash and height, the
/// full action list (`CreateAccount`, `DeployContract`,
/// `FunctionCall`, `Transfer`, `Stake`, `AddKey`, `DeleteKey`,
/// `DeleteAccount`), and the execution outcome (status, gas burnt,
/// tokens burnt, logs, emitted receipt ids).
///
/// None of the above is currently serialised. Instead we emit a
/// single-byte sentinel payload (`[UNSUPPORTED_CHAIN_SENTINEL]`, i.e.
/// `[0xFF]`) that is guaranteed to fail decoding on the SDK side
/// because `0xFF` is not a valid TLV value tag (the tag space is
/// `0x00`..=`0x09`; see `runtime/wasm/src/rust_abi/types.rs::tags`).
///
/// This is intentionally louder than the previous `Vec::new()` stub:
/// an empty payload could be silently round-tripped as "no data", and
/// a Rust subgraph targeting NEAR would index garbage without ever
/// realising something was wrong. The sentinel byte forces a
/// deterministic handler failure at the first byte of the payload.
///
/// There is deliberately no logging call here: `to_rust_bytes` has
/// no `Logger` in scope and threading one through would pollute the
/// trait for every chain. Operators will see the SDK-side decode
/// error surfaced as a mapping failure, which is the loud signal we
/// want.
fn to_rust_bytes(&self) -> Vec<u8> {
vec![UNSUPPORTED_CHAIN_SENTINEL]
}
}

pub struct ReceiptWithOutcome {
// REVIEW: Do we want to actually also have those two below behind an `Arc` wrapper?
pub outcome: codec::ExecutionOutcomeWithId,
Expand Down
7 changes: 4 additions & 3 deletions core/src/subgraph/instance_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use graph::env::EnvVars;
use graph::prelude::{SubgraphInstanceManager as SubgraphInstanceManagerTrait, *};
use graph::{blockchain::BlockchainMap, components::store::DeploymentLocator};
use graph_runtime_wasm::module::ToAscPtr;
use graph_runtime_wasm::rust_abi::ToRustBytes;
use graph_runtime_wasm::RuntimeHostBuilder;
use tokio::task;

Expand Down Expand Up @@ -234,7 +235,7 @@ impl<S: SubgraphStore, AC: amp::Client> SubgraphInstanceManager<S, AC> {
) -> anyhow::Result<SubgraphRunner<C, RuntimeHostBuilder<C>>>
where
C: Blockchain,
<C as Blockchain>::MappingTrigger: ToAscPtr,
<C as Blockchain>::MappingTrigger: ToAscPtr + ToRustBytes,
{
self.build_subgraph_runner_inner(
logger,
Expand Down Expand Up @@ -262,7 +263,7 @@ impl<S: SubgraphStore, AC: amp::Client> SubgraphInstanceManager<S, AC> {
) -> anyhow::Result<SubgraphRunner<C, RuntimeHostBuilder<C>>>
where
C: Blockchain,
<C as Blockchain>::MappingTrigger: ToAscPtr,
<C as Blockchain>::MappingTrigger: ToAscPtr + ToRustBytes,
{
let subgraph_store = self.subgraph_store.cheap_clone();
let registry = self.metrics_registry.cheap_clone();
Expand Down Expand Up @@ -568,7 +569,7 @@ impl<S: SubgraphStore, AC: amp::Client> SubgraphInstanceManager<S, AC> {
runner: SubgraphRunner<C, RuntimeHostBuilder<C>>,
) -> Result<(), Error>
where
<C as Blockchain>::MappingTrigger: ToAscPtr,
<C as Blockchain>::MappingTrigger: ToAscPtr + ToRustBytes,
{
let registry = self.metrics_registry.cheap_clone();
let subgraph_metrics = runner.metrics.subgraph.cheap_clone();
Expand Down
Loading
Loading