Skip to content

Commit c2730db

Browse files
authored
Reduce the number of RPC calls (#203)
This PR reduces the number of RPC calls to index non-finalized blocks by optimistically fetching a block range.
2 parents 4714bf3 + be588b3 commit c2730db

12 files changed

Lines changed: 214 additions & 86 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "rpc: reduce the number of rpc calls",
4+
"packageName": "@apibara/evm-rpc",
5+
"email": "francesco@ceccon.me",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "rpc: reduce the number of rpc calls",
4+
"packageName": "@apibara/protocol",
5+
"email": "francesco@ceccon.me",
6+
"dependentChangeType": "patch"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "rpc: reduce the number of rpc calls",
4+
"packageName": "@apibara/starknet",
5+
"email": "francesco@ceccon.me",
6+
"dependentChangeType": "patch"
7+
}

examples/evm-client/src/main-rpc.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const command = defineCommand({
1919
},
2020
contract: {
2121
type: "string",
22-
default: "0xf08A50178dfcDe18524640EA6618a1f965821715",
22+
// default: "0xf08A50178dfcDe18524640EA6618a1f965821715",
23+
default: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
2324
description: "Contract address to monitor",
2425
},
2526
startBlock: {
@@ -29,7 +30,7 @@ const command = defineCommand({
2930
},
3031
finality: {
3132
type: "string",
32-
default: "finalized",
33+
default: "accepted",
3334
description: "Block finality (finalized, accepted)",
3435
},
3536
},
@@ -51,7 +52,11 @@ const command = defineCommand({
5152
new EvmRpcStream(viemClient, {
5253
// This parameter changes based on the rpc provider.
5354
// The stream automatically shrinks the batch size when the provider returns an error.
54-
getLogsRangeSize: 1_000n,
55+
// getLogsRangeSize: 1_000n,
56+
getLogsRangeSize: 100n,
57+
mergeGetLogsFilter: "always",
58+
// alwaysSendAcceptedHeaders: true,
59+
headRefreshIntervalMs: 500,
5560
}),
5661
);
5762

packages/evm-rpc/src/stream-config.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
type FetchBlockRangeResult,
88
type FetchBlockResult,
99
type FetchCursorArgs,
10+
type FetchCursorRangeArgs,
1011
RpcStreamConfig,
1112
type ValidateFilterResult,
1213
} from "@apibara/protocol/rpc";
@@ -123,14 +124,33 @@ export class EvmRpcStream extends RpcStreamConfig<Filter, Block> {
123124
};
124125
}
125126

127+
async fetchCursorRange({
128+
startBlockNumber,
129+
endBlockNumber,
130+
}: FetchCursorRangeArgs): Promise<BlockInfo[]> {
131+
const requestCount = Number(endBlockNumber - startBlockNumber + 1n);
132+
return await Promise.all(
133+
Array.from({ length: requestCount }, async (_, i) => {
134+
const blockNumber = startBlockNumber + BigInt(i);
135+
const info = await this.fetchCursor({ blockNumber });
136+
if (!info) {
137+
throw new Error(
138+
`RPC returned null block for block number ${blockNumber}`,
139+
);
140+
}
141+
return info;
142+
}),
143+
);
144+
}
145+
126146
async fetchBlockRange({
127147
startBlock,
128-
finalizedBlock,
148+
maxBlock,
129149
force,
130150
filter,
131151
}: FetchBlockRangeArgs<Filter>): Promise<FetchBlockRangeResult<Block>> {
132152
const { start: fromBlock, end: toBlock } = this.blockRangeOracle.clampRange(
133-
{ start: startBlock, end: finalizedBlock },
153+
{ start: startBlock, end: maxBlock },
134154
);
135155

136156
// console.log("Fetching block range", fromBlock, toBlock, filter);

packages/evm-rpc/tests/merge-get-logs.integration.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -340,14 +340,14 @@ describe("EvmRpcStream", () => {
340340

341341
const merged = await mergedStream.fetchBlockRange({
342342
startBlock,
343-
finalizedBlock: endBlock,
343+
maxBlock: endBlock,
344344
force: true,
345345
filter,
346346
});
347347

348348
const standard = await standardStream.fetchBlockRange({
349349
startBlock,
350-
finalizedBlock: endBlock,
350+
maxBlock: endBlock,
351351
force: true,
352352
filter,
353353
});

packages/protocol/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"scripts": {
3939
"build": "pnpm build:proto && unbuild",
4040
"build:proto": "buf generate proto",
41+
"buf": "buf",
4142
"typecheck": "tsc --noEmit",
4243
"test": "vitest",
4344
"test:ci": "vitest run",

packages/protocol/src/rpc/chain-tracker.ts

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { fromHex } from "viem";
22
import type { Bytes, Cursor } from "../common";
3-
import type { BlockInfo } from "./config";
3+
import type { BlockInfo, FetchCursorRangeArgs } from "./config";
44
import { blockInfoToCursor } from "./helpers";
55

66
type UpdateHeadArgs = {
77
newHead: BlockInfo;
88
fetchCursorByHash: (hash: Bytes) => Promise<BlockInfo | null>;
9+
fetchCursorRange: (args: FetchCursorRangeArgs) => Promise<BlockInfo[]>;
910
};
1011

1112
type UpdateHeadResult =
@@ -24,10 +25,21 @@ export class ChainTracker {
2425
#finalized: BlockInfo;
2526
#head: BlockInfo;
2627
#canonical: Map<bigint, BlockInfo>;
28+
#batchSize: bigint;
2729

28-
constructor({ head, finalized }: { finalized: BlockInfo; head: BlockInfo }) {
30+
constructor({
31+
head,
32+
finalized,
33+
batchSize,
34+
}: {
35+
finalized: BlockInfo;
36+
head: BlockInfo;
37+
batchSize: bigint;
38+
}) {
2939
this.#finalized = finalized;
3040
this.#head = head;
41+
this.#batchSize = batchSize;
42+
3143
this.#canonical = new Map([
3244
[finalized.blockNumber, finalized],
3345
[head.blockNumber, head],
@@ -106,6 +118,7 @@ export class ChainTracker {
106118
async updateHead({
107119
newHead,
108120
fetchCursorByHash,
121+
fetchCursorRange,
109122
}: UpdateHeadArgs): Promise<UpdateHeadResult> {
110123
// console.debug(
111124
// `updateHead: new=${newHead.blockNumber} old=${this.#head.blockNumber}`,
@@ -192,10 +205,47 @@ export class ChainTracker {
192205
// The new chain is longer and we need the missing blocks.
193206
// This may result in reorgs.
194207

195-
let current = newHead;
196-
let reorgDetected = false;
197-
const blocksToApply = [newHead];
208+
// console.log(
209+
// `Moving from ${this.#head.blockNumber} to ${newHead.blockNumber} (${newHead.blockNumber - this.#head.blockNumber} blocks)`,
210+
// );
211+
212+
let currentBlockNumber = this.#head.blockNumber + 1n;
213+
214+
while (true) {
215+
let endBlockNumber = currentBlockNumber + this.#batchSize;
216+
if (endBlockNumber > newHead.blockNumber) {
217+
endBlockNumber = newHead.blockNumber;
218+
}
219+
220+
const missing = await fetchCursorRange({
221+
startBlockNumber: currentBlockNumber,
222+
endBlockNumber,
223+
});
224+
225+
for (const block of missing) {
226+
const canonicalParent = this.#canonical.get(block.blockNumber - 1n);
227+
if (
228+
!canonicalParent ||
229+
canonicalParent.blockHash !== block.parentBlockHash
230+
) {
231+
throw new Error(
232+
"Chain reorganization detected. Recovery not implemented",
233+
);
234+
}
235+
236+
this.#canonical.set(block.blockNumber, block);
237+
238+
// console.log(`Applied block ${block.blockNumber}`);
239+
}
240+
241+
if (endBlockNumber === newHead.blockNumber) {
242+
break;
243+
}
244+
245+
currentBlockNumber = endBlockNumber + 1n;
246+
}
198247

248+
/*
199249
while (true) {
200250
const parent = await fetchCursorByHash(current.parentBlockHash);
201251
@@ -226,24 +276,39 @@ export class ChainTracker {
226276
blocksToApply.push(parent);
227277
current = parent;
228278
}
279+
*/
280+
// throw new Error("FUCK IT");
229281

230-
for (const block of blocksToApply.reverse()) {
231-
this.#canonical.set(block.blockNumber, block);
232-
}
282+
// for (const block of blocksToApply.reverse()) {
283+
// this.#canonical.set(block.blockNumber, block);
284+
// }
233285

234-
const previousHead = this.#head;
286+
// const previousHead = this.#head;
235287
this.#head = newHead;
236288

237-
if (reorgDetected) {
238-
return {
239-
status: "reorg",
240-
cursor: blockInfoToCursor(previousHead),
241-
};
242-
}
289+
// if (reorgDetected) {
290+
// return {
291+
// status: "reorg",
292+
// cursor: blockInfoToCursor(previousHead),
293+
// };
294+
// }
243295

244296
return { status: "success" };
245297
}
246298

299+
isCanonical({ orderKey, uniqueKey }: Cursor) {
300+
if (!uniqueKey) {
301+
return true;
302+
}
303+
304+
const block = this.#canonical.get(orderKey);
305+
if (!block) {
306+
return true;
307+
}
308+
309+
return block.blockHash === uniqueKey;
310+
}
311+
247312
async initializeStartingCursor({
248313
cursor,
249314
fetchCursor,
@@ -319,9 +384,11 @@ export class ChainTracker {
319384
export function createChainTracker({
320385
head,
321386
finalized,
387+
batchSize,
322388
}: {
323389
head: BlockInfo;
324390
finalized: BlockInfo;
391+
batchSize: bigint;
325392
}): ChainTracker {
326-
return new ChainTracker({ finalized, head });
393+
return new ChainTracker({ finalized, head, batchSize });
327394
}

packages/protocol/src/rpc/config.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Bytes, Cursor } from "../common";
22

33
export type FetchBlockRangeArgs<TFilter> = {
44
startBlock: bigint;
5-
finalizedBlock: bigint;
5+
maxBlock: bigint;
66
force: boolean;
77
filter: TFilter;
88
};
@@ -43,6 +43,11 @@ export type FetchBlockByNumberResult<TBlock> =
4343
blockInfo: BlockInfo;
4444
};
4545

46+
export type FetchCursorRangeArgs = {
47+
startBlockNumber: bigint;
48+
endBlockNumber: bigint;
49+
};
50+
4651
export type FetchCursorArgs =
4752
| {
4853
blockTag: "latest" | "finalized";
@@ -74,6 +79,7 @@ export abstract class RpcStreamConfig<TFilter, TBlock> {
7479
abstract headRefreshIntervalMs(): number;
7580
abstract finalizedRefreshIntervalMs(): number;
7681

82+
abstract fetchCursorRange(args: FetchCursorRangeArgs): Promise<BlockInfo[]>;
7783
abstract fetchCursor(args: FetchCursorArgs): Promise<BlockInfo | null>;
7884

7985
abstract validateFilter(filter: TFilter): ValidateFilterResult;

0 commit comments

Comments
 (0)