Skip to content

emulator: Add support for eth_getBlockReceipts emulation on unsupported chains #6069

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 4 commits into
base: master
Choose a base branch
from
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,7 @@ lcov.info

# Docker volumes and debug logs
.postgres
logfile
logfile

# Node modules
node_modules
44 changes: 41 additions & 3 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,13 @@ can access these via:
Once this is up and running, you can use
[`graph-cli`](https://github.com/graphprotocol/graph-tooling/tree/main/packages/cli) to create and
deploy your subgraph to the running Graph Node.

### Running Graph Node on an Macbook M1

We do not currently build native images for Macbook M1, which can lead to processes being killed due to out-of-memory errors (code 137). Based on the example `docker-compose.yml` is possible to rebuild the image for your M1 by running the following, then running `docker-compose up` as normal:

> **Important** Increase memory limits for the docker engine running on your machine. Otherwise docker build command will fail due to out of memory error. To do that, open docker-desktop and go to Resources/advanced/memory.

```
# Remove the original image
docker rmi graphprotocol/graph-node:latest
Expand All @@ -66,3 +67,40 @@ docker run -it \
-e ethereum=<NETWORK_NAME>:<ETHEREUM_RPC_URL> \
graphprotocol/graph-node:latest
```

## Running with emulator

There are certain chains that do not support the `eth_getBlockReceipts` method which is required for indexing (e.g. Oasis Sapphire). We can run a graph node with an emulator to support this method.

Cd into the `emulator` directory and install dependencies:

```sh
npm install
```

In docker-compose.yml, do the following:

- Uncomment the entire `emulator` service block.
- Replace `<YOUR_CHAIN_RPC_URL>` with your actual RPC URL (e.g. from Chainstack).
- Uncomment the `depends_on` line for emulator.
- Comment out the default Ethereum RPC under `graph-node` and instead uncomment the emulator RPC line.

Example:

```yaml
# emulator:
# build: ./emulator
# ports:
# - '8545:8545'
# environment:
# UPSTREAM_RPC: https://oasis-sapphire-mainnet.core.chainstack.com/<your-key>

# ...
# ethereum: 'oasis:http://emulator:8545' # Use this
```

Once done, save and run:

```sh
docker-compose up
```
24 changes: 18 additions & 6 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
version: '3'
services:
# Emulator for simulating eth_getBlockReceipts method on EVM-compatible chains that do not support it
# Uncomment and replace <YOUR_CHAIN_RPC_URL>
# emulator:
# build: ./emulator
# ports:
# - '8545:8545'
# environment:
# UPSTREAM_RPC: <YOUR_CHAIN_RPC_URL>
graph-node:
image: graphprotocol/graph-node
ports:
Expand All @@ -9,6 +17,7 @@ services:
- '8030:8030'
- '8040:8040'
depends_on:
# - emulator # Uncomment if using emulator
- ipfs
- postgres
extra_hosts:
Expand All @@ -19,7 +28,10 @@ services:
postgres_pass: let-me-in
postgres_db: graph-node
ipfs: 'ipfs:5001'
ethereum: 'mainnet:http://host.docker.internal:8545'
# Without emulator
ethereum: 'mainnet:http://host.docker.internal:8545' # Comment if using emulator
# With emulator
# ethereum: 'oasis:http://emulator:8545'
GRAPH_LOG: info
ipfs:
image: ipfs/kubo:v0.17.0
Expand All @@ -33,9 +45,9 @@ services:
- '5432:5432'
command:
[
"postgres",
"-cshared_preload_libraries=pg_stat_statements",
"-cmax_connections=200"
'postgres',
'-cshared_preload_libraries=pg_stat_statements',
'-cmax_connections=200',
]
environment:
POSTGRES_USER: graph-node
Expand All @@ -44,7 +56,7 @@ services:
# FIXME: remove this env. var. which we shouldn't need. Introduced by
# <https://github.com/graphprotocol/graph-node/pull/3511>, maybe as a
# workaround for https://github.com/docker/for-mac/issues/6270?
PGDATA: "/var/lib/postgresql/data"
POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
PGDATA: '/var/lib/postgresql/data'
POSTGRES_INITDB_ARGS: '-E UTF8 --locale=C'
volumes:
- ./data/postgres:/var/lib/postgresql/data:Z
8 changes: 8 additions & 0 deletions docker/emulator/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FROM node:18

WORKDIR /app
COPY . .

RUN npm install

CMD ["node", "index.js"]
103 changes: 103 additions & 0 deletions docker/emulator/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
const express = require('express');
const bodyParser = require('body-parser');
const axios = require('axios');

require('dotenv').config();

const app = express();
const PORT = 8545;
const NODE_URL = process.env.UPSTREAM_RPC || '<YOUR_FALLBACK_UPSTREAM_RPC>';

app.use(bodyParser.json());

async function getBlockReceipts(block) {
try {
const { data: blockData } = await axios.post(NODE_URL, {
jsonrpc: '2.0',
method: 'eth_getBlockByNumber',
params: [block, false],
id: 1,
});

const transactions = blockData.result?.transactions || [];

const receiptPromises = transactions.map((tx) =>
axios.post(NODE_URL, {
jsonrpc: '2.0',
method: 'eth_getTransactionReceipt',
params: [tx],
id: 1,
})
);

const receiptsRaw = await Promise.all(receiptPromises);

const receipts = receiptsRaw.map(({ data }) => {
const txReceipt = data.result;

// Normalize logs
for (let log of txReceipt.logs) {
let logData = {
address: log.address,
topics: log.topics,
data: log.data,
blockNumber: log.blockNumber,
transactionHash: log.transactionHash,
transactionIndex: log.transactionIndex,
blockHash: log.blockHash,
logIndex: log.logIndex,
removed: log.removed,
id: log.id,
};
txReceipt.logs = logData;
}

return txReceipt;
});

if (receipts.length === 0) {
console.warn(`No receipts found for block ${block}`);
}
return receipts;
} catch (err) {
console.error('Error emulating eth_getBlockReceipts:', err.message);
return null;
}
}

// JSON-RPC handler
app.post('/', async (req, res) => {
const { method, params, id } = req.body;

if (method === 'eth_getBlockReceipts') {
const [blockHashOrNumber] = params;
try {
const receipts = await getBlockReceipts(blockHashOrNumber);
return res.json({ jsonrpc: '2.0', result: receipts, id });
} catch (err) {
console.error(err);
return res.status(500).json({
jsonrpc: '2.0',
id,
error: { code: -32000, message: 'Failed to emulate block receipts' },
});
}
}

// Fallback: proxy other calls to the real RPC
try {
const response = await axios.post(NODE_URL, req.body);
res.json(response.data);
} catch (err) {
console.error('Upstream RPC error:', err.message);
res.status(502).json({
jsonrpc: '2.0',
error: { code: -32000, message: 'Upstream RPC failed' },
id,
});
}
});

app.listen(PORT, '0.0.0.0', () => {
console.log(`Emulator listening on http://localhost:${PORT}`);
});
Loading