ponder 0.18.0 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/esm/indexing-store/index.js +8 -4
- package/dist/esm/indexing-store/index.js.map +1 -1
- package/dist/esm/indexing-store/utils.js +3 -1
- package/dist/esm/indexing-store/utils.js.map +1 -1
- package/dist/esm/rpc/actions.js +2 -16
- package/dist/esm/rpc/actions.js.map +1 -1
- package/dist/esm/runtime/filter.js +80 -32
- package/dist/esm/runtime/filter.js.map +1 -1
- package/dist/esm/sync-historical/in-memory.js +1 -2
- package/dist/esm/sync-historical/in-memory.js.map +1 -1
- package/dist/esm/sync-historical/index.js +1 -2
- package/dist/esm/sync-historical/index.js.map +1 -1
- package/dist/esm/sync-realtime/bloom.js +72 -64
- package/dist/esm/sync-realtime/bloom.js.map +1 -1
- package/dist/esm/sync-realtime/index.js +79 -50
- package/dist/esm/sync-realtime/index.js.map +1 -1
- package/dist/esm/utils/copy.js +77 -56
- package/dist/esm/utils/copy.js.map +1 -1
- package/dist/esm/utils/decodeEventLog.js +20 -6
- package/dist/esm/utils/decodeEventLog.js.map +1 -1
- package/dist/types/indexing-store/index.d.ts.map +1 -1
- package/dist/types/indexing-store/utils.d.ts.map +1 -1
- package/dist/types/rpc/actions.d.ts +2 -3
- package/dist/types/rpc/actions.d.ts.map +1 -1
- package/dist/types/runtime/filter.d.ts +24 -1
- package/dist/types/runtime/filter.d.ts.map +1 -1
- package/dist/types/sync-historical/in-memory.d.ts.map +1 -1
- package/dist/types/sync-historical/index.d.ts.map +1 -1
- package/dist/types/sync-realtime/bloom.d.ts +52 -8
- package/dist/types/sync-realtime/bloom.d.ts.map +1 -1
- package/dist/types/sync-realtime/index.d.ts.map +1 -1
- package/dist/types/utils/copy.d.ts +10 -2
- package/dist/types/utils/copy.d.ts.map +1 -1
- package/dist/types/utils/decodeEventLog.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/indexing-store/index.ts +8 -4
- package/src/indexing-store/utils.ts +3 -1
- package/src/rpc/actions.ts +1 -21
- package/src/runtime/filter.ts +116 -52
- package/src/sync-historical/in-memory.ts +0 -2
- package/src/sync-historical/index.ts +0 -2
- package/src/sync-realtime/bloom.ts +129 -79
- package/src/sync-realtime/index.ts +114 -72
- package/src/utils/copy.ts +83 -64
- package/src/utils/decodeEventLog.ts +28 -6
|
@@ -1,102 +1,152 @@
|
|
|
1
|
-
import { type Hex,
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
4
|
-
getFilterFromBlock,
|
|
5
|
-
getFilterToBlock,
|
|
6
|
-
isAddressFactory,
|
|
7
|
-
} from "@/runtime/filter.js";
|
|
1
|
+
import { type Hex, keccak256 } from "viem";
|
|
2
|
+
import type { Factory, LogFilter } from "@/internal/types.js";
|
|
3
|
+
import { isAddressFactory } from "@/runtime/filter.js";
|
|
8
4
|
|
|
9
5
|
export const zeroLogsBloom =
|
|
10
6
|
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
|
|
11
7
|
|
|
12
8
|
const BLOOM_SIZE_BYTES = 256;
|
|
13
9
|
|
|
14
|
-
export
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
10
|
+
export type BloomBits = {
|
|
11
|
+
byte0: number;
|
|
12
|
+
mask0: number;
|
|
13
|
+
byte1: number;
|
|
14
|
+
mask1: number;
|
|
15
|
+
byte2: number;
|
|
16
|
+
mask2: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A `LogFilter` with each address and topic converted to `BloomBits`.
|
|
21
|
+
*
|
|
22
|
+
* `undefined` matches every bloom. An array matches if any item is in the bloom.
|
|
23
|
+
*/
|
|
24
|
+
export type LogFilterBloom = {
|
|
25
|
+
address: BloomBits[] | undefined;
|
|
26
|
+
topics: (BloomBits[] | undefined)[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const getBloomBits = (input: Hex): BloomBits => {
|
|
30
|
+
const hash = keccak256(input, "bytes");
|
|
31
|
+
const bytes = [0, 0, 0];
|
|
32
|
+
const masks = [0, 0, 0];
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < 3; i++) {
|
|
35
|
+
const bit = (hash[i * 2 + 1]! + (hash[i * 2]! << 8)) & 0x7ff;
|
|
36
|
+
bytes[i] = BLOOM_SIZE_BYTES - 1 - Math.floor(bit / 8);
|
|
37
|
+
masks[i] = 1 << (bit % 8);
|
|
26
38
|
}
|
|
27
39
|
|
|
28
|
-
return
|
|
40
|
+
return {
|
|
41
|
+
byte0: bytes[0]!,
|
|
42
|
+
mask0: masks[0]!,
|
|
43
|
+
byte1: bytes[1]!,
|
|
44
|
+
mask1: masks[1]!,
|
|
45
|
+
byte2: bytes[2]!,
|
|
46
|
+
mask2: masks[2]!,
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const isBloomBitsInBloom = (
|
|
51
|
+
bloom: Uint8Array,
|
|
52
|
+
bits: BloomBits,
|
|
53
|
+
): boolean =>
|
|
54
|
+
(bloom[bits.byte0]! & bits.mask0) !== 0 &&
|
|
55
|
+
(bloom[bits.byte1]! & bits.mask1) !== 0 &&
|
|
56
|
+
(bloom[bits.byte2]! & bits.mask2) !== 0;
|
|
57
|
+
|
|
58
|
+
export const isInBloom = (bloom: Uint8Array, input: Hex): boolean =>
|
|
59
|
+
isBloomBitsInBloom(bloom, getBloomBits(input));
|
|
60
|
+
|
|
61
|
+
const isAnyInBloom = (
|
|
62
|
+
bloom: Uint8Array,
|
|
63
|
+
bits: BloomBits[] | undefined,
|
|
64
|
+
): boolean =>
|
|
65
|
+
bits === undefined || bits.some((b) => isBloomBitsInBloom(bloom, b));
|
|
66
|
+
|
|
67
|
+
const toBloomBits = (
|
|
68
|
+
input: Hex | Hex[] | null | undefined,
|
|
69
|
+
): BloomBits[] | undefined => {
|
|
70
|
+
if (input === null || input === undefined) return undefined;
|
|
71
|
+
if (Array.isArray(input)) return input.map(getBloomBits);
|
|
72
|
+
return [getBloomBits(input)];
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Convert `filter` to a `LogFilterBloom`. Call once for each filter,
|
|
77
|
+
* so that the inputs are not hashed again for each block.
|
|
78
|
+
*/
|
|
79
|
+
export const getLogFilterBloom = (filter: LogFilter): LogFilterBloom => {
|
|
80
|
+
let address: BloomBits[] | undefined;
|
|
81
|
+
|
|
82
|
+
if (isAddressFactory(filter.address)) {
|
|
83
|
+
address = undefined;
|
|
84
|
+
} else if (Array.isArray(filter.address) && filter.address.length === 0) {
|
|
85
|
+
address = undefined;
|
|
86
|
+
} else {
|
|
87
|
+
address = toBloomBits(filter.address);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
address,
|
|
92
|
+
topics: [filter.topic0, filter.topic1, filter.topic2, filter.topic3].map(
|
|
93
|
+
toBloomBits,
|
|
94
|
+
),
|
|
95
|
+
};
|
|
29
96
|
};
|
|
30
97
|
|
|
31
98
|
/**
|
|
32
99
|
* Return true if `filter` is in `bloom`.
|
|
33
100
|
*
|
|
34
|
-
* A filter with an address of type `LogFactory`
|
|
35
|
-
*
|
|
36
|
-
* filter is matched (log on child contract).
|
|
101
|
+
* A filter with an address of type `LogFactory` matches all addresses.
|
|
102
|
+
* Use `isFactoryInBloom` to check for new child contracts.
|
|
37
103
|
*
|
|
38
104
|
* Note: False positives are possible.
|
|
39
105
|
*/
|
|
40
106
|
export function isFilterInBloom({
|
|
41
|
-
|
|
107
|
+
bloom,
|
|
42
108
|
filter,
|
|
43
109
|
}: {
|
|
44
|
-
|
|
45
|
-
filter:
|
|
110
|
+
bloom: Uint8Array;
|
|
111
|
+
filter: LogFilterBloom;
|
|
46
112
|
}): boolean {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
113
|
+
return (
|
|
114
|
+
isAnyInBloom(bloom, filter.address) &&
|
|
115
|
+
filter.topics.every((topic) => isAnyInBloom(bloom, topic))
|
|
116
|
+
);
|
|
117
|
+
}
|
|
54
118
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return true;
|
|
63
|
-
} else if (Array.isArray(topic)) {
|
|
64
|
-
return topic.some((t) => isInBloom(block.logsBloom, t));
|
|
65
|
-
} else {
|
|
66
|
-
return isInBloom(block.logsBloom, topic);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
let isAddressInBloom: boolean;
|
|
71
|
-
|
|
72
|
-
if (filter.address === undefined) isAddressInBloom = true;
|
|
73
|
-
else if (isAddressFactory(filter.address)) {
|
|
74
|
-
// Return true if the `Factory` is matched.
|
|
75
|
-
if (
|
|
76
|
-
(filter.address.address === undefined ||
|
|
77
|
-
(Array.isArray(filter.address.address)
|
|
78
|
-
? filter.address.address.some((address) =>
|
|
79
|
-
isInBloom(block.logsBloom, address),
|
|
80
|
-
)
|
|
81
|
-
: isInBloom(block.logsBloom, filter.address.address))) &&
|
|
82
|
-
isInBloom(block.logsBloom, filter.address.eventSelector)
|
|
83
|
-
) {
|
|
84
|
-
return true;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
isAddressInBloom = true;
|
|
88
|
-
} else if (Array.isArray(filter.address)) {
|
|
89
|
-
if (filter.address.length === 0) {
|
|
90
|
-
isAddressInBloom = true;
|
|
91
|
-
} else {
|
|
92
|
-
isAddressInBloom = filter.address.some((address) =>
|
|
93
|
-
isInBloom(block.logsBloom, address),
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
// single address case
|
|
98
|
-
isAddressInBloom = isInBloom(block.logsBloom, filter.address);
|
|
99
|
-
}
|
|
119
|
+
/**
|
|
120
|
+
* A `Factory` with its address and event selector converted to `BloomBits`.
|
|
121
|
+
*/
|
|
122
|
+
export type FactoryBloom = {
|
|
123
|
+
address: BloomBits[] | undefined;
|
|
124
|
+
eventSelector: BloomBits;
|
|
125
|
+
};
|
|
100
126
|
|
|
101
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Convert `factory` to a `FactoryBloom`. Call once for each factory,
|
|
129
|
+
* so that the inputs are not hashed again for each block.
|
|
130
|
+
*/
|
|
131
|
+
export const getFactoryBloom = (factory: Factory): FactoryBloom => ({
|
|
132
|
+
address: toBloomBits(factory.address),
|
|
133
|
+
eventSelector: getBloomBits(factory.eventSelector),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Return true if a log that matches `factory` can be in `bloom`.
|
|
138
|
+
*
|
|
139
|
+
* Note: False positives are possible.
|
|
140
|
+
*/
|
|
141
|
+
export function isFactoryInBloom({
|
|
142
|
+
bloom,
|
|
143
|
+
factory,
|
|
144
|
+
}: {
|
|
145
|
+
bloom: Uint8Array;
|
|
146
|
+
factory: FactoryBloom;
|
|
147
|
+
}): boolean {
|
|
148
|
+
return (
|
|
149
|
+
isAnyInBloom(bloom, factory.address) &&
|
|
150
|
+
isBloomBitsInBloom(bloom, factory.eventSelector)
|
|
151
|
+
);
|
|
102
152
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Address,
|
|
3
3
|
type Hash,
|
|
4
|
+
type Hex,
|
|
5
|
+
hexToBytes,
|
|
4
6
|
hexToNumber,
|
|
5
7
|
numberToHex,
|
|
6
8
|
zeroHash,
|
|
@@ -45,11 +47,13 @@ import {
|
|
|
45
47
|
isAddressFactory,
|
|
46
48
|
isAddressMatched,
|
|
47
49
|
isBlockFilterMatched,
|
|
50
|
+
isBlockInFilter,
|
|
48
51
|
isLogFactoryMatched,
|
|
49
52
|
isLogFilterMatched,
|
|
50
53
|
isTraceFilterMatched,
|
|
51
54
|
isTransactionFilterMatched,
|
|
52
55
|
isTransferFilterMatched,
|
|
56
|
+
mergeLogFiltersToBlockRequest,
|
|
53
57
|
} from "@/runtime/filter.js";
|
|
54
58
|
import {
|
|
55
59
|
getFactoryFragmentId,
|
|
@@ -60,7 +64,14 @@ import { isAsyncExecutionChain } from "@/utils/finality.js";
|
|
|
60
64
|
import { createLock } from "@/utils/mutex.js";
|
|
61
65
|
import { range } from "@/utils/range.js";
|
|
62
66
|
import { startClock } from "@/utils/timer.js";
|
|
63
|
-
import {
|
|
67
|
+
import {
|
|
68
|
+
getFactoryBloom,
|
|
69
|
+
getLogFilterBloom,
|
|
70
|
+
isFactoryInBloom,
|
|
71
|
+
isFilterInBloom,
|
|
72
|
+
isInBloom,
|
|
73
|
+
zeroLogsBloom,
|
|
74
|
+
} from "./bloom.js";
|
|
64
75
|
|
|
65
76
|
export type RealtimeSync = {
|
|
66
77
|
/**
|
|
@@ -176,6 +187,9 @@ export const createRealtimeSync = (
|
|
|
176
187
|
}
|
|
177
188
|
}
|
|
178
189
|
|
|
190
|
+
const logFilterBlooms = logFilters.map(getLogFilterBloom);
|
|
191
|
+
const factoryBlooms = factories.map(getFactoryBloom);
|
|
192
|
+
|
|
179
193
|
const getLatestUnfinalizedBlock = () => {
|
|
180
194
|
if (unfinalizedBlocks.length === 0) {
|
|
181
195
|
return finalizedBlock;
|
|
@@ -211,33 +225,45 @@ export const createRealtimeSync = (
|
|
|
211
225
|
// Logs
|
|
212
226
|
////////
|
|
213
227
|
|
|
214
|
-
// "eth_getLogs" calls can be skipped if no filters match `newHeadBlock.logsBloom`.
|
|
215
228
|
// Async-execution chains cannot reliably use blooms to filter or validate logs.
|
|
229
|
+
// A zero `logsBloom` is treated as missing.
|
|
216
230
|
const isAsyncExecution = isAsyncExecutionChain(args.chain.id);
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
231
|
+
const blockNumber = hexToNumber(maybeBlockHeader.number);
|
|
232
|
+
const bloom = hexToBytes(maybeBlockHeader.logsBloom);
|
|
233
|
+
|
|
234
|
+
const logsRequestFilter = mergeLogFiltersToBlockRequest({
|
|
235
|
+
logFilters: logFilters.filter(
|
|
236
|
+
(logFilter, i) =>
|
|
237
|
+
isBlockInFilter(logFilter, blockNumber) &&
|
|
238
|
+
(isAsyncExecution ||
|
|
239
|
+
maybeBlockHeader.logsBloom === zeroLogsBloom ||
|
|
240
|
+
isFilterInBloom({ bloom, filter: logFilterBlooms[i]! })),
|
|
241
|
+
),
|
|
242
|
+
factories: factories.filter(
|
|
243
|
+
(factory, i) =>
|
|
244
|
+
isBlockInFilter(factory, blockNumber) &&
|
|
245
|
+
(isAsyncExecution ||
|
|
246
|
+
maybeBlockHeader.logsBloom === zeroLogsBloom ||
|
|
247
|
+
isFactoryInBloom({ bloom, factory: factoryBlooms[i]! })),
|
|
248
|
+
),
|
|
249
|
+
});
|
|
250
|
+
const shouldRequestLogs = logsRequestFilter !== undefined;
|
|
223
251
|
|
|
224
252
|
let logs: SyncLog[] = [];
|
|
225
253
|
if (shouldRequestLogs) {
|
|
254
|
+
const logsRequestParams = {
|
|
255
|
+
blockHash: maybeBlockHeader.hash,
|
|
256
|
+
address: logsRequestFilter.address,
|
|
257
|
+
topics: logsRequestFilter.topics,
|
|
258
|
+
};
|
|
259
|
+
|
|
226
260
|
if (block === undefined) {
|
|
227
261
|
[block, logs] = await Promise.all([
|
|
228
262
|
eth_getBlockByHash(args.rpc, [maybeBlockHeader.hash, true], context),
|
|
229
|
-
eth_getLogs(
|
|
230
|
-
args.rpc,
|
|
231
|
-
[{ blockHash: maybeBlockHeader.hash }],
|
|
232
|
-
context,
|
|
233
|
-
),
|
|
263
|
+
eth_getLogs(args.rpc, [logsRequestParams], context),
|
|
234
264
|
]);
|
|
235
265
|
} else {
|
|
236
|
-
logs = await eth_getLogs(
|
|
237
|
-
args.rpc,
|
|
238
|
-
[{ blockHash: block.hash }],
|
|
239
|
-
context,
|
|
240
|
-
);
|
|
266
|
+
logs = await eth_getLogs(args.rpc, [logsRequestParams], context);
|
|
241
267
|
}
|
|
242
268
|
|
|
243
269
|
validateLogsAndBlock(
|
|
@@ -245,7 +271,7 @@ export const createRealtimeSync = (
|
|
|
245
271
|
block,
|
|
246
272
|
{
|
|
247
273
|
method: "eth_getLogs",
|
|
248
|
-
params: [
|
|
274
|
+
params: [logsRequestParams],
|
|
249
275
|
},
|
|
250
276
|
ethGetBlockMethod === "eth_getBlockByNumber"
|
|
251
277
|
? {
|
|
@@ -256,60 +282,8 @@ export const createRealtimeSync = (
|
|
|
256
282
|
method: "eth_getBlockByHash",
|
|
257
283
|
params: [block.hash, true],
|
|
258
284
|
},
|
|
259
|
-
isAsyncExecutionChain(args.chain.id),
|
|
260
285
|
);
|
|
261
286
|
|
|
262
|
-
if (isAsyncExecutionChain(args.chain.id) === false) {
|
|
263
|
-
// Note: Exact `logsBloom` validations were considered too strict to add to `validateLogsAndBlock`.
|
|
264
|
-
let isInvalidLogsBloom = false;
|
|
265
|
-
for (const log of logs) {
|
|
266
|
-
if (isInBloom(block.logsBloom, log.address) === false) {
|
|
267
|
-
isInvalidLogsBloom = true;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
log.topics[0] &&
|
|
272
|
-
isInBloom(block.logsBloom, log.topics[0]) === false
|
|
273
|
-
) {
|
|
274
|
-
isInvalidLogsBloom = true;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
log.topics[1] &&
|
|
279
|
-
isInBloom(block.logsBloom, log.topics[1]) === false
|
|
280
|
-
) {
|
|
281
|
-
isInvalidLogsBloom = true;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
if (
|
|
285
|
-
log.topics[2] &&
|
|
286
|
-
isInBloom(block.logsBloom, log.topics[2]) === false
|
|
287
|
-
) {
|
|
288
|
-
isInvalidLogsBloom = true;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if (
|
|
292
|
-
log.topics[3] &&
|
|
293
|
-
isInBloom(block.logsBloom, log.topics[3]) === false
|
|
294
|
-
) {
|
|
295
|
-
isInvalidLogsBloom = true;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (isInvalidLogsBloom) {
|
|
299
|
-
args.common.logger.warn({
|
|
300
|
-
msg: "Detected inconsistent RPC responses. Log not found in block.logsBloom.",
|
|
301
|
-
action: "fetch_block_data",
|
|
302
|
-
chain: args.chain.name,
|
|
303
|
-
chain_id: args.chain.id,
|
|
304
|
-
number: hexToNumber(block.number),
|
|
305
|
-
hash: block.hash,
|
|
306
|
-
logIndex: hexToNumber(log.logIndex),
|
|
307
|
-
});
|
|
308
|
-
break;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
287
|
for (const log of logs) {
|
|
314
288
|
if (log.transactionHash === zeroHash) {
|
|
315
289
|
args.common.logger.warn({
|
|
@@ -325,7 +299,10 @@ export const createRealtimeSync = (
|
|
|
325
299
|
}
|
|
326
300
|
}
|
|
327
301
|
|
|
328
|
-
if (
|
|
302
|
+
if (
|
|
303
|
+
shouldRequestLogs === false &&
|
|
304
|
+
(logFilters.length > 0 || factories.length > 0)
|
|
305
|
+
) {
|
|
329
306
|
args.common.logger.trace({
|
|
330
307
|
msg: "Skipped eth_getLogs request due to bloom filter result",
|
|
331
308
|
action: "fetch_block_data",
|
|
@@ -387,6 +364,7 @@ export const createRealtimeSync = (
|
|
|
387
364
|
|
|
388
365
|
// Record `blockChildAddresses` that contain factory child addresses
|
|
389
366
|
const blockChildAddresses: BlockWithEventData["childAddresses"] = new Map();
|
|
367
|
+
const factoryLogs = new Set<SyncLog>();
|
|
390
368
|
|
|
391
369
|
const childAddressDecodeFailureIds = new Set<string>();
|
|
392
370
|
let childAddressDecodeFailureCount = 0;
|
|
@@ -395,6 +373,7 @@ export const createRealtimeSync = (
|
|
|
395
373
|
for (const factory of factories) {
|
|
396
374
|
for (const log of logs) {
|
|
397
375
|
if (isLogFactoryMatched({ factory, log })) {
|
|
376
|
+
factoryLogs.add(log);
|
|
398
377
|
let address: Address;
|
|
399
378
|
try {
|
|
400
379
|
address = getChildAddress({ log, factory });
|
|
@@ -462,6 +441,69 @@ export const createRealtimeSync = (
|
|
|
462
441
|
return isMatched;
|
|
463
442
|
});
|
|
464
443
|
|
|
444
|
+
if (isAsyncExecution === false && block !== undefined) {
|
|
445
|
+
// Note: Exact `logsBloom` validations were considered too strict to add to `validateLogsAndBlock`.
|
|
446
|
+
const logsBloom = hexToBytes(block.logsBloom);
|
|
447
|
+
const checkedInputs = new Set<Hex>();
|
|
448
|
+
let isInvalidLogsBloom = false;
|
|
449
|
+
for (const log of [...factoryLogs, ...logs]) {
|
|
450
|
+
if (
|
|
451
|
+
checkedInputs.has(log.address) === false &&
|
|
452
|
+
isInBloom(logsBloom, log.address) === false
|
|
453
|
+
) {
|
|
454
|
+
isInvalidLogsBloom = true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (
|
|
458
|
+
log.topics[0] &&
|
|
459
|
+
checkedInputs.has(log.topics[0]) === false &&
|
|
460
|
+
isInBloom(logsBloom, log.topics[0]) === false
|
|
461
|
+
) {
|
|
462
|
+
isInvalidLogsBloom = true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (
|
|
466
|
+
log.topics[1] &&
|
|
467
|
+
checkedInputs.has(log.topics[1]) === false &&
|
|
468
|
+
isInBloom(logsBloom, log.topics[1]) === false
|
|
469
|
+
) {
|
|
470
|
+
isInvalidLogsBloom = true;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (
|
|
474
|
+
log.topics[2] &&
|
|
475
|
+
checkedInputs.has(log.topics[2]) === false &&
|
|
476
|
+
isInBloom(logsBloom, log.topics[2]) === false
|
|
477
|
+
) {
|
|
478
|
+
isInvalidLogsBloom = true;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (
|
|
482
|
+
log.topics[3] &&
|
|
483
|
+
checkedInputs.has(log.topics[3]) === false &&
|
|
484
|
+
isInBloom(logsBloom, log.topics[3]) === false
|
|
485
|
+
) {
|
|
486
|
+
isInvalidLogsBloom = true;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (isInvalidLogsBloom) {
|
|
490
|
+
args.common.logger.warn({
|
|
491
|
+
msg: "Detected inconsistent RPC responses. Log not found in block.logsBloom.",
|
|
492
|
+
action: "fetch_block_data",
|
|
493
|
+
chain: args.chain.name,
|
|
494
|
+
chain_id: args.chain.id,
|
|
495
|
+
number: hexToNumber(block.number),
|
|
496
|
+
hash: block.hash,
|
|
497
|
+
logIndex: hexToNumber(log.logIndex),
|
|
498
|
+
});
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
checkedInputs.add(log.address);
|
|
503
|
+
for (const topic of log.topics) checkedInputs.add(topic);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
465
507
|
// Initial weak trace filtering before full filtering with factory addresses in handleBlock
|
|
466
508
|
traces = traces.filter((trace) => {
|
|
467
509
|
let isMatched = false;
|
package/src/utils/copy.ts
CHANGED
|
@@ -5,75 +5,94 @@ import util from "node:util";
|
|
|
5
5
|
*/
|
|
6
6
|
export const COPY_ON_WRITE = Symbol.for("ponder:copyOnWrite");
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Target of a copy-on-write proxy.
|
|
10
|
+
*
|
|
11
|
+
* Note: The handler is shared by all proxies, so the state of each proxy
|
|
12
|
+
* is stored in its target.
|
|
13
|
+
*/
|
|
14
|
+
class CopyOnWriteTarget {
|
|
15
|
+
obj: object;
|
|
16
|
+
copiedObject: object | undefined;
|
|
17
|
+
|
|
18
|
+
constructor(obj: object) {
|
|
19
|
+
this.obj = obj;
|
|
20
|
+
this.copiedObject = undefined;
|
|
21
|
+
}
|
|
9
22
|
|
|
10
|
-
|
|
11
|
-
|
|
23
|
+
// Note: `this` is the proxy when called by `util.inspect`.
|
|
24
|
+
[util.inspect.custom]() {
|
|
25
|
+
// @ts-expect-error
|
|
26
|
+
return this[COPY_ON_WRITE] ?? this.copiedObject ?? this.obj;
|
|
27
|
+
}
|
|
12
28
|
}
|
|
13
29
|
|
|
30
|
+
const getCopiedObject = (target: CopyOnWriteTarget) => {
|
|
31
|
+
if (target.copiedObject === undefined) {
|
|
32
|
+
target.copiedObject = structuredClone(target.obj);
|
|
33
|
+
}
|
|
34
|
+
return target.copiedObject;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const copyOnWriteHandler: ProxyHandler<CopyOnWriteTarget> = {
|
|
38
|
+
get(target, prop, receiver) {
|
|
39
|
+
if (prop === COPY_ON_WRITE) {
|
|
40
|
+
return target.copiedObject ?? target.obj;
|
|
41
|
+
}
|
|
42
|
+
let result = Reflect.get(target.copiedObject ?? target.obj, prop, receiver);
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
typeof result === "object" &&
|
|
46
|
+
result !== null &&
|
|
47
|
+
target.copiedObject === undefined
|
|
48
|
+
) {
|
|
49
|
+
result = Reflect.get(getCopiedObject(target), prop, receiver);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return result;
|
|
53
|
+
},
|
|
54
|
+
set(target, prop, newValue, receiver) {
|
|
55
|
+
return Reflect.set(getCopiedObject(target), prop, newValue, receiver);
|
|
56
|
+
},
|
|
57
|
+
deleteProperty(target, prop) {
|
|
58
|
+
return Reflect.deleteProperty(getCopiedObject(target), prop);
|
|
59
|
+
},
|
|
60
|
+
defineProperty(target, prop, descriptor) {
|
|
61
|
+
return Reflect.defineProperty(getCopiedObject(target), prop, descriptor);
|
|
62
|
+
},
|
|
63
|
+
ownKeys(target) {
|
|
64
|
+
return Reflect.ownKeys(target.copiedObject ?? target.obj);
|
|
65
|
+
},
|
|
66
|
+
has(target, prop) {
|
|
67
|
+
return Reflect.has(target.copiedObject ?? target.obj, prop);
|
|
68
|
+
},
|
|
69
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
70
|
+
return Reflect.getOwnPropertyDescriptor(
|
|
71
|
+
target.copiedObject ?? target.obj,
|
|
72
|
+
prop,
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
getPrototypeOf(target) {
|
|
76
|
+
return Reflect.getPrototypeOf(target.copiedObject ?? target.obj);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
14
80
|
/**
|
|
15
|
-
* Create a copy-on-write proxy for
|
|
81
|
+
* Create a copy-on-write proxy for a plain object.
|
|
82
|
+
*
|
|
83
|
+
* @dev Arrays and other exotic objects are not supported. The proxy target
|
|
84
|
+
* is a `CopyOnWriteTarget`, not `obj`, so the proxy invariants break for
|
|
85
|
+
* non-configurable properties such as `Array.length`:
|
|
86
|
+
* `Object.keys()` and spread throw a `TypeError`, and `Array.isArray()`
|
|
87
|
+
* returns `false`.
|
|
16
88
|
*/
|
|
17
|
-
export const copyOnWrite = <T extends
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const proxy = new Proxy<T>(obj, {
|
|
26
|
-
get(target, prop, receiver) {
|
|
27
|
-
if (prop === COPY_ON_WRITE) {
|
|
28
|
-
return copiedObject ?? target;
|
|
29
|
-
}
|
|
30
|
-
let result = Reflect.get(copiedObject ?? target, prop, receiver);
|
|
31
|
-
|
|
32
|
-
if (
|
|
33
|
-
typeof result === "object" &&
|
|
34
|
-
result !== null &&
|
|
35
|
-
copiedObject === undefined
|
|
36
|
-
) {
|
|
37
|
-
copiedObject = structuredClone(target);
|
|
38
|
-
copyOnWriteCopies.set(proxy, copiedObject);
|
|
39
|
-
result = Reflect.get(copiedObject, prop, receiver);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return result;
|
|
43
|
-
},
|
|
44
|
-
set(target, prop, newValue, receiver) {
|
|
45
|
-
if (copiedObject === undefined) {
|
|
46
|
-
copiedObject = structuredClone(target);
|
|
47
|
-
copyOnWriteCopies.set(proxy, copiedObject);
|
|
48
|
-
}
|
|
49
|
-
return Reflect.set(copiedObject, prop, newValue, receiver);
|
|
50
|
-
},
|
|
51
|
-
deleteProperty(target, prop) {
|
|
52
|
-
if (copiedObject === undefined) {
|
|
53
|
-
copiedObject = structuredClone(target);
|
|
54
|
-
copyOnWriteCopies.set(proxy, copiedObject);
|
|
55
|
-
}
|
|
56
|
-
return Reflect.deleteProperty(copiedObject, prop);
|
|
57
|
-
},
|
|
58
|
-
defineProperty(target, prop, descriptor) {
|
|
59
|
-
if (copiedObject === undefined) {
|
|
60
|
-
copiedObject = structuredClone(target);
|
|
61
|
-
copyOnWriteCopies.set(proxy, copiedObject);
|
|
62
|
-
}
|
|
63
|
-
return Reflect.defineProperty(copiedObject, prop, descriptor);
|
|
64
|
-
},
|
|
65
|
-
ownKeys(target) {
|
|
66
|
-
return Reflect.ownKeys(copiedObject ?? target);
|
|
67
|
-
},
|
|
68
|
-
has(target, prop) {
|
|
69
|
-
return Reflect.has(copiedObject ?? target, prop);
|
|
70
|
-
},
|
|
71
|
-
getOwnPropertyDescriptor(target, prop) {
|
|
72
|
-
return Reflect.getOwnPropertyDescriptor(copiedObject ?? target, prop);
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return proxy;
|
|
89
|
+
export const copyOnWrite = <T extends { [key: string]: unknown }>(
|
|
90
|
+
obj: T,
|
|
91
|
+
): T => {
|
|
92
|
+
return new Proxy(
|
|
93
|
+
new CopyOnWriteTarget(obj),
|
|
94
|
+
copyOnWriteHandler,
|
|
95
|
+
) as unknown as T;
|
|
77
96
|
};
|
|
78
97
|
|
|
79
98
|
/**
|