Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Fetch logs in batches, with cache-friendly ranges (#43)
Browse files Browse the repository at this point in the history
* Fetch logs in batches, with cache-friendly ranges

* Configure GETLOGS_MAX_RANGE for each project in env

* Simplify getLogs implementation

* Rename variable

* Fix test

* A tiny refactor

* Tiny fix (let->const)

* Add comment

* Add changeset file
  • Loading branch information
adamiak authored Sep 29, 2023
1 parent 02f8ba7 commit 2b3db57
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 16 deletions.
5 changes: 5 additions & 0 deletions .changeset/wild-avocados-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@l2beat/discovery': minor
---

Support fetching logs in batches
39 changes: 38 additions & 1 deletion packages/discovery/src/config/config.discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export function getDiscoveryCliConfig(cli: CliParameters): DiscoveryCliConfig {
const discoveryEnabled = cli.mode === 'discover'
const singleDiscoveryEnabled = cli.mode === 'single-discovery'
const invertEnabled = cli.mode === 'invert'
const chain = getChainConfig(cli.chain)

return {
invert: invertEnabled && {
Expand All @@ -33,12 +34,13 @@ export function getDiscoveryCliConfig(cli: CliParameters): DiscoveryCliConfig {
dryRun: cli.dryRun,
dev: cli.dev,
blockNumber: env.optionalInteger('DISCOVERY_BLOCK_NUMBER'),
getLogsMaxRange: chain.rpcGetLogsMaxRange,
},
singleDiscovery: singleDiscoveryEnabled && {
address: cli.address,
chainId: cli.chain,
},
chain: getChainConfig(cli.chain),
chain,
}
}

Expand All @@ -50,69 +52,99 @@ function getChainConfig(chainId: ChainId): DiscoveryChainConfig {
return {
chainId: ChainId.ETHEREUM,
rpcUrl: env.string('DISCOVERY_ETHEREUM_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_ETHEREUM_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_ETHEREUM_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.etherscan.io/api',
}
case ChainId.ARBITRUM:
return {
chainId: ChainId.ARBITRUM,
rpcUrl: env.string('DISCOVERY_ARBITRUM_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_ARBITRUM_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_ARBITRUM_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.arbiscan.io/api',
}
case ChainId.OPTIMISM:
return {
chainId: ChainId.OPTIMISM,
rpcUrl: env.string('DISCOVERY_OPTIMISM_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_OPTIMISM_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_OPTIMISM_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api-optimistic.etherscan.io/api',
}
case ChainId.POLYGON_POS:
return {
chainId: ChainId.POLYGON_POS,
rpcUrl: env.string('DISCOVERY_POLYGON_POS_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_POLYGON_POS_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_POLYGON_POS_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.polygonscan.com/api',
}
case ChainId.BSC:
return {
chainId: ChainId.BSC,
rpcUrl: env.string('DISCOVERY_BSC_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_BSC_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_BSC_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.bscscan.com/api',
}
case ChainId.AVALANCHE:
return {
chainId: ChainId.AVALANCHE,
rpcUrl: env.string('DISCOVERY_AVALANCHE_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_AVALANCHE_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_AVALANCHE_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.snowtrace.io/api',
}
case ChainId.CELO:
return {
chainId: ChainId.CELO,
rpcUrl: env.string('DISCOVERY_CELO_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_CELO_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_CELO_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.celoscan.io/api',
}
case ChainId.LINEA:
return {
chainId: ChainId.LINEA,
rpcUrl: env.string('DISCOVERY_LINEA_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_LINEA_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_LINEA_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.lineascan.build/api',
}
case ChainId.BASE:
return {
chainId: ChainId.BASE,
rpcUrl: env.string('DISCOVERY_BASE_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_BASE_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_BASE_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.basescan.org/api',
}
case ChainId.POLYGON_ZKEVM:
return {
chainId: ChainId.POLYGON_ZKEVM,
rpcUrl: env.string('DISCOVERY_POLYGON_ZKEVM_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_POLYGON_ZKEVM_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string(
'DISCOVERY_POLYGON_ZKEVM_ETHERSCAN_API_KEY',
),
Expand All @@ -122,6 +154,9 @@ function getChainConfig(chainId: ChainId): DiscoveryChainConfig {
return {
chainId: ChainId.GNOSIS,
rpcUrl: env.string('DISCOVERY_GNOSIS_RPC_URL'),
rpcGetLogsMaxRange: env.optionalInteger(
'DISCOVERY_GNOSIS_RPC_GETLOGS_MAX_RANGE',
),
etherscanApiKey: env.string('DISCOVERY_GNOSIS_ETHERSCAN_API_KEY'),
etherscanUrl: 'https://api.gnosisscan.io/api',
}
Expand All @@ -145,6 +180,7 @@ export interface DiscoveryModuleConfig {
readonly dryRun?: boolean
readonly dev?: boolean
readonly blockNumber?: number
readonly getLogsMaxRange?: number
}

export interface SingleDiscoveryModuleConfig {
Expand All @@ -155,6 +191,7 @@ export interface SingleDiscoveryModuleConfig {
export interface DiscoveryChainConfig {
chainId: ChainId
rpcUrl: string
rpcGetLogsMaxRange?: number
etherscanApiKey: string
etherscanUrl: string
}
Expand Down
5 changes: 5 additions & 0 deletions packages/discovery/src/discovery/DiscoveryLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export class DiscoveryLogger {
)}`,
)
}

logFetchingEvents(fromBlock: number, toBlock: number): void {
const text = `Fetching events in range ${fromBlock} - ${toBlock}`
this.log(` ${chalk.gray(text)}`)
}
}

function dots(length: number): string {
Expand Down
12 changes: 10 additions & 2 deletions packages/discovery/src/discovery/analysis/AddressAnalyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ describe(AddressAnalyzer.name, () => {
const addressAnalyzer = new AddressAnalyzer(
mockObject<DiscoveryProvider>({
getCode: async () => Bytes.fromHex('0x1234'),
getDeploymentTimestamp: async () => new UnixTime(1234),
getDeploymentInfo: async () => ({
timestamp: new UnixTime(1234),
blockNumber: 9876,
}),
}),
mockObject<ProxyDetector>({
detectProxy: async () => ({
Expand Down Expand Up @@ -100,6 +103,7 @@ describe(AddressAnalyzer.name, () => {
derivedName: undefined,
isVerified: true,
deploymentTimestamp: new UnixTime(1234),
deploymentBlockNumber: 9876,
upgradeability: { type: 'EIP1967 proxy', implementation, admin },
implementations: [implementation],
values: { owner: owner.toString() },
Expand Down Expand Up @@ -130,7 +134,10 @@ describe(AddressAnalyzer.name, () => {
const addressAnalyzer = new AddressAnalyzer(
mockObject<DiscoveryProvider>({
getCode: async () => Bytes.fromHex('0x1234'),
getDeploymentTimestamp: async () => new UnixTime(1234),
getDeploymentInfo: async () => ({
timestamp: new UnixTime(1234),
blockNumber: 9876,
}),
}),
mockObject<ProxyDetector>({
detectProxy: async () => ({
Expand Down Expand Up @@ -170,6 +177,7 @@ describe(AddressAnalyzer.name, () => {
address,
isVerified: false,
deploymentTimestamp: new UnixTime(1234),
deploymentBlockNumber: 9876,
upgradeability: { type: 'EIP1967 proxy', implementation, admin },
implementations: [implementation],
values: { owner: owner.toString() },
Expand Down
8 changes: 6 additions & 2 deletions packages/discovery/src/discovery/analysis/AddressAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface AnalyzedContract {
address: EthereumAddress
name: string
deploymentTimestamp: UnixTime
deploymentBlockNumber: number
derivedName: string | undefined
isVerified: boolean
upgradeability: UpgradeabilityParameters
Expand Down Expand Up @@ -55,8 +56,10 @@ export class AddressAnalyzer {
return { analysis: { type: 'EOA', address }, relatives: [] }
}

const deploymentTimestamp =
await this.provider.getDeploymentTimestamp(address)
const {
timestamp: deploymentTimestamp,
blockNumber: deploymentBlockNumber,
} = await this.provider.getDeploymentInfo(address)

const proxy = await this.proxyDetector.detectProxy(
address,
Expand Down Expand Up @@ -86,6 +89,7 @@ export class AddressAnalyzer {
isVerified: sources.isVerified,
address,
deploymentTimestamp,
deploymentBlockNumber,
upgradeability: proxy?.upgradeability ?? { type: 'immutable' },
implementations: proxy?.implementations ?? [],
values: values ?? {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe(processAnalysis.name, () => {
values: {},
isVerified: true,
deploymentTimestamp: new UnixTime(1234),
deploymentBlockNumber: 9876,
upgradeability: { type: 'immutable' } as UpgradeabilityParameters,
implementations: [],
abis: {},
Expand Down
152 changes: 152 additions & 0 deletions packages/discovery/src/discovery/provider/DiscoveryProvider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { expect, mockFn, MockObject, mockObject } from 'earl'
import { providers } from 'ethers'

import { EthereumAddress } from '../../utils/EthereumAddress'
import { EtherscanLikeClient } from '../../utils/EtherscanLikeClient'
import { DiscoveryLogger } from '../DiscoveryLogger'
import { DiscoveryProvider } from './DiscoveryProvider'

const rangesFromCalls = (provider: MockObject<providers.Provider>) =>
provider.getLogs.calls.map((call) => [
call.args[0].fromBlock,
call.args[0].toBlock,
])

const GETLOGS_MAX_RANGE = 10000

describe(DiscoveryProvider.name, () => {
describe(DiscoveryProvider.prototype.getLogs.name, () => {
const etherscanLikeClientMock = mockObject<EtherscanLikeClient>({})
const address = EthereumAddress.random()
const topics = ['testTopic']
let providerMock: MockObject<providers.Provider>
let discoveryProviderMock: DiscoveryProvider

beforeEach(() => {
providerMock = mockObject<providers.Provider>({
getLogs: mockFn().resolvesTo([]),
})
discoveryProviderMock = new DiscoveryProvider(
providerMock,
etherscanLikeClientMock,
DiscoveryLogger.SILENT,
GETLOGS_MAX_RANGE,
)
discoveryProviderMock.getDeploymentInfo = mockFn().resolvesTo({
blockNumber: 0,
timestamp: 0,
})
})

it('handles simple range', async () => {
await discoveryProviderMock.getLogs(address, topics, 5000, 35000)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([
[5000, 9999],
[10000, 19999],
[20000, 29999],
[30000, 35000],
])
})

it('handles range at boundaries', async () => {
await discoveryProviderMock.getLogs(address, topics, 10000, 39999)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([
[10000, 19999],
[20000, 29999],
[30000, 39999],
])
})

it('handles range at +/- 1 of boundaries', async () => {
await discoveryProviderMock.getLogs(address, topics, 9999, 30000)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([
[9999, 9999],
[10000, 19999],
[20000, 29999],
[30000, 30000],
])
})

it('handles range where from and to are equal and multiply or range', async () => {
await discoveryProviderMock.getLogs(address, topics, 10000, 10000)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([[10000, 10000]])
})

it('handles range where from and to are -1 of multiply or range', async () => {
await discoveryProviderMock.getLogs(address, topics, 9999, 9999)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([[9999, 9999]])
})

it('handles range where from and to are +1 of multiply or range', async () => {
await discoveryProviderMock.getLogs(address, topics, 10001, 10001)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([[10001, 10001]])
})

it('handles range [0,0]', async () => {
await discoveryProviderMock.getLogs(address, topics, 0, 0)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([[0, 0]])
})

it('handles range [1,1]', async () => {
await discoveryProviderMock.getLogs(address, topics, 1, 1)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([[1, 1]])
})

it('handles range inside boundaries', async () => {
await discoveryProviderMock.getLogs(address, topics, 1400, 1600)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([[1400, 1600]])
})

it('handles getLogsMaxRange undefined (no range)', async () => {
providerMock = mockObject<providers.Provider>({
getLogs: mockFn().resolvesTo([]),
})
discoveryProviderMock = new DiscoveryProvider(
providerMock,
etherscanLikeClientMock,
DiscoveryLogger.SILENT,
undefined, // PROVIDING UNDEFINED for getLogsMaxRange, so no batching
)
discoveryProviderMock.getDeploymentInfo = mockFn().resolvesTo({
blockNumber: 0,
timestamp: 0,
})
await discoveryProviderMock.getLogs(address, topics, 5000, 35000)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([[5000, 35000]])
})

it('starts with deployment block if bigger then fromBlock', async () => {
providerMock = mockObject<providers.Provider>({
getLogs: mockFn().resolvesTo([]),
})
discoveryProviderMock = new DiscoveryProvider(
providerMock,
etherscanLikeClientMock,
DiscoveryLogger.SILENT,
GETLOGS_MAX_RANGE,
)
discoveryProviderMock.getDeploymentInfo = mockFn().resolvesTo({
blockNumber: 16000,
timestamp: 0,
})

await discoveryProviderMock.getLogs(address, topics, 5000, 35000)
const ranges = rangesFromCalls(providerMock)
expect(ranges).toEqual([
[16000, 19999],
[20000, 29999],
[30000, 35000],
])
})
})
})
Loading

0 comments on commit 2b3db57

Please sign in to comment.