Skip to content

Commit 173d694

Browse files
authored
[TC] feat: get pool deployment block from external sync if avail (#236)
1 parent 6552a5a commit 173d694

7 files changed

Lines changed: 135 additions & 14 deletions

File tree

packages/plugins/src/host/index.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,18 @@ export type ExternalSyncProvider = {
5959
): AsyncIterable<ExternalRawEvent>;
6060

6161
/**
62-
* The highest block this provider has data for on the given pool, or `null`
63-
* if it has none. Consumers use this to decide where the chain must take over.
62+
* The lowest block this provider has data for on the given pool — roughly the
63+
* pool's deployment/registration block. Consumers use it as a scan-start hint.
64+
* @throws if the provider has no data for the pool.
6465
*/
65-
lastCoveredBlock(params: ExternalSyncPoolId): Promise<Hex | null>;
66+
firstCoveredBlock(params: ExternalSyncPoolId): Promise<Hex>;
67+
68+
/**
69+
* The highest block this provider has data for on the given pool. Consumers
70+
* use it to decide where the chain must take over.
71+
* @throws if the provider has no data for the pool.
72+
*/
73+
lastCoveredBlock(params: ExternalSyncPoolId): Promise<Hex>;
6674
};
6775

6876
/**

packages/tornado-cash/src/data/interfaces/sync.service.interface.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ export type ExternalSyncClient = {
1919
getEvents(
2020
params: ExternalSyncPoolId & { fromBlock: Hex; toBlock: Hex },
2121
): Promise<ExternalRawEvent[]>;
22-
lastCoveredBlock(params: ExternalSyncPoolId): Promise<Hex | null>;
22+
/** @throws if the provider has no data for the pool. */
23+
firstCoveredBlock(params: ExternalSyncPoolId): Promise<Hex>;
24+
/** @throws if the provider has no data for the pool. */
25+
lastCoveredBlock(params: ExternalSyncPoolId): Promise<Hex>;
2326
};
2427

2528
export interface SyncServiceParams {
@@ -71,4 +74,11 @@ export interface ISyncService {
7174
getRelayerRegistryEvents(
7275
params: IGetEventsParams,
7376
): Promise<IGetRelayerRegistryEventsResult>;
77+
78+
/**
79+
* Resolves a pool's scan-start block. Uses the external provider's first
80+
* covered block when available (an O(1) lookup), else falls back to the
81+
* on-chain deployment-block binary search.
82+
*/
83+
getPoolDeploymentBlock(params: { chainId: bigint; address: Address }): Promise<bigint>;
7484
}

packages/tornado-cash/src/data/sync.service.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ExternalRawEvent } from "@kohaku-eth/plugins";
22
import { toHex } from "viem";
3+
import { Address } from "../interfaces/types.interface";
34
import {
45
ExternalSyncClient,
56
IGetEventsParams,
@@ -100,6 +101,33 @@ export class SyncService implements ISyncService {
100101
};
101102
}
102103

104+
async getPoolDeploymentBlock({
105+
chainId,
106+
address,
107+
}: {
108+
chainId: bigint;
109+
address: Address;
110+
}): Promise<bigint> {
111+
const provider = this.externalSyncProvider;
112+
113+
if (provider) {
114+
try {
115+
// The provider's first covered block ≈ the pool's registration block, at
116+
// O(1) — much cheaper than the on-chain deployment-block binary search.
117+
return BigInt(
118+
await provider.firstCoveredBlock({
119+
chainId: toHex(chainId),
120+
address: toHex(address, { size: 20 }),
121+
}),
122+
);
123+
} catch {
124+
// Provider has no data for this pool — fall through to on-chain discovery.
125+
}
126+
}
127+
128+
return this.dataService.getContractDeploymentBlock(address);
129+
}
130+
103131
/**
104132
* Decides whether to use the external provider and, if so, streams its raw
105133
* events for `[fromBlock, coverage]`. Event-agnostic: callers parse the raw
@@ -123,14 +151,16 @@ export class SyncService implements ISyncService {
123151

124152
const chainId = toHex(rawChainId);
125153
const hexAddress = toHex(address, { size: 20 });
126-
const rawCoverage = await provider.lastCoveredBlock({ chainId, address: hexAddress });
127154

128-
if (!rawCoverage) {
155+
let coverage: bigint;
156+
157+
try {
158+
coverage = BigInt(await provider.lastCoveredBlock({ chainId, address: hexAddress }));
159+
} catch {
160+
// Provider has no data for this pool (or failed) — fall back to chain-only.
129161
return null;
130162
}
131163

132-
const coverage = BigInt(rawCoverage);
133-
134164
if (coverage <= fromBlock) {
135165
return null;
136166
}

packages/tornado-cash/src/plugin/base.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ export class TornadoCashProtocol implements TCInstance {
7777

7878
return events;
7979
},
80+
firstCoveredBlock: (params) => externalSyncProvider.firstCoveredBlock(params),
8081
lastCoveredBlock: (params) => externalSyncProvider.lastCoveredBlock(params),
8182
};
8283

packages/tornado-cash/src/state/thunks/syncPoolsThunk.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,22 @@ import { IPool } from '../../data/interfaces/events.interface';
33
import { registerPools } from '../slices/poolsSlice';
44
import { RootState } from '../store';
55
import { IDataService } from '../../data/interfaces/data.service.interface';
6+
import { ISyncService } from '../../data/interfaces/sync.service.interface';
67
import { instanceRegistryInfoSelector, poolsSelector } from '../selectors/slices.selectors';
78

89
export interface SyncPoolsThunkParams {
910
dataService: IDataService;
11+
syncService: ISyncService;
1012
}
1113

1214
export const syncPoolsThunk = createAsyncThunk<void, SyncPoolsThunkParams, { state: RootState }>(
1315
'sync/pools',
1416
async ({
15-
dataService
17+
dataService,
18+
syncService,
1619
}, { dispatch, getState }) => {
1720
const state = getState();
18-
const { instanceRegistry: { address: instanceRegistryAddress } } = instanceRegistryInfoSelector(state);
21+
const { chainId, instanceRegistry: { address: instanceRegistryAddress } } = instanceRegistryInfoSelector(state);
1922
const existingPools = poolsSelector(state);
2023

2124
const poolsAddressses = await dataService.getAllPoolsAddresses(instanceRegistryAddress);
@@ -25,7 +28,7 @@ export const syncPoolsThunk = createAsyncThunk<void, SyncPoolsThunkParams, { sta
2528
unsyncedPools.map(async (poolAddress) => {
2629
const [config, registeredBlock] = await Promise.all([
2730
dataService.getPoolConfig(instanceRegistryAddress, poolAddress),
28-
dataService.getContractDeploymentBlock(poolAddress),
31+
syncService.getPoolDeploymentBlock({ chainId, address: poolAddress }),
2932
]);
3033

3134
return { config, registeredBlock };

packages/tornado-cash/src/state/thunks/syncThunk.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const syncThunk = createAsyncThunk<void, SyncThunkParams, { state: RootSt
2929

3030
unwrapResult(await dispatch(syncPoolsThunk({
3131
dataService,
32+
syncService,
3233
})));
3334

3435
unwrapResult(await dispatch(syncRelayersThunk({ dataService, syncService })));

packages/tornado-cash/tests/unit/sync-service.test.ts

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,27 +66,50 @@ const makeDataService = () => {
6666
RelayerRegistered: events.map((e) => relayerRegistered(BigInt(e.blockNumber))),
6767
}));
6868

69+
const getContractDeploymentBlock = vi.fn(async () => 42n);
70+
6971
return {
7072
getBlockNumber: vi.fn(async () => HEAD),
7173
getPoolEvents,
7274
parsePoolEvents,
7375
getRelayerRegistryEvents,
7476
parseRelayerRegistryEvents,
77+
getContractDeploymentBlock,
7578
} as unknown as IDataService & {
7679
getPoolEvents: typeof getPoolEvents;
7780
parsePoolEvents: typeof parsePoolEvents;
7881
getRelayerRegistryEvents: typeof getRelayerRegistryEvents;
7982
parseRelayerRegistryEvents: typeof parseRelayerRegistryEvents;
83+
getContractDeploymentBlock: typeof getContractDeploymentBlock;
8084
};
8185
};
8286

83-
const makeProvider = (coverage: bigint | null, events: ExternalRawEvent[] = []) => {
87+
const makeProvider = (
88+
coverage: bigint | null,
89+
events: ExternalRawEvent[] = [],
90+
firstBlock: bigint | null = null,
91+
) => {
8492
const getEvents = vi.fn(async () => events);
8593

8694
return {
87-
lastCoveredBlock: vi.fn(async () => (coverage == null ? null : toHex(coverage))),
95+
// Coverage methods throw (rather than return null) when the provider has no
96+
// data for the pool.
97+
lastCoveredBlock: vi.fn(async () => {
98+
if (coverage == null) throw new Error('no coverage');
99+
100+
return toHex(coverage);
101+
}),
102+
firstCoveredBlock: vi.fn(async () => {
103+
if (firstBlock == null) throw new Error('no coverage');
104+
105+
return toHex(firstBlock);
106+
}),
88107
getEvents,
89-
} as unknown as ExternalSyncClient & { getEvents: typeof getEvents };
108+
} as unknown as ExternalSyncClient & {
109+
getEvents: typeof getEvents;
110+
lastCoveredBlock: ReturnType<typeof vi.fn>;
111+
firstCoveredBlock: ReturnType<typeof vi.fn>;
112+
};
90113
};
91114

92115
const rawAt = (block: bigint): ExternalRawEvent => ({
@@ -235,3 +258,48 @@ describe('SyncService.getRelayerRegistryEvents', () => {
235258
expect(result.RelayerRegistered.map((r) => r.blockNumber)).toEqual([100n]);
236259
});
237260
});
261+
262+
describe('SyncService.getPoolDeploymentBlock', () => {
263+
it("uses the provider's first covered block, skipping the on-chain search", async () => {
264+
const dataService = makeDataService();
265+
const provider = makeProvider(600n, [], 12345n);
266+
const service = new SyncService({
267+
dataService,
268+
externalSyncProvider: provider,
269+
minExternalSyncBlocksAmount: 100,
270+
});
271+
272+
const block = await service.getPoolDeploymentBlock({ chainId: CHAIN_ID, address: POOL });
273+
274+
expect(block).toBe(12345n);
275+
expect(provider.firstCoveredBlock).toHaveBeenCalledWith(
276+
expect.objectContaining({ chainId: toHex(CHAIN_ID) }),
277+
);
278+
expect(dataService.getContractDeploymentBlock).not.toHaveBeenCalled();
279+
});
280+
281+
it('falls back to the on-chain deployment block when the provider has no coverage', async () => {
282+
const dataService = makeDataService();
283+
const provider = makeProvider(600n, [], null); // firstCoveredBlock throws
284+
const service = new SyncService({
285+
dataService,
286+
externalSyncProvider: provider,
287+
minExternalSyncBlocksAmount: 100,
288+
});
289+
290+
const block = await service.getPoolDeploymentBlock({ chainId: CHAIN_ID, address: POOL });
291+
292+
expect(block).toBe(42n);
293+
expect(dataService.getContractDeploymentBlock).toHaveBeenCalledWith(POOL);
294+
});
295+
296+
it('falls back to the on-chain deployment block when no provider is configured', async () => {
297+
const dataService = makeDataService();
298+
const service = new SyncService({ dataService });
299+
300+
const block = await service.getPoolDeploymentBlock({ chainId: CHAIN_ID, address: POOL });
301+
302+
expect(block).toBe(42n);
303+
expect(dataService.getContractDeploymentBlock).toHaveBeenCalledWith(POOL);
304+
});
305+
});

0 commit comments

Comments
 (0)