@@ -44,6 +44,17 @@ var defaultTraceFields = []string{
4444 "reward_type" , "refund_address" ,
4545}
4646
47+ type blockTxAggregate struct {
48+ BlockNumber * big.Int `ch:"block_number"`
49+ TxCount uint64 `ch:"tx_count"`
50+ }
51+
52+ type blockLogAggregate struct {
53+ BlockNumber * big.Int `ch:"block_number"`
54+ LogCount uint64 `ch:"log_count"`
55+ MaxLogIndex uint64 `ch:"max_log_index"`
56+ }
57+
4758// only use this for backfill or getting old data.
4859var ClickhouseConnV1 clickhouse.Conn
4960
@@ -253,6 +264,162 @@ func GetBlockDataFromClickHouseV2(chainId uint64, startBlockNumber uint64, endBl
253264 return blockData , nil
254265}
255266
267+ // GetTransactionMismatchRangeFromClickHouseV2 checks, for blocks in the given range,
268+ // where the stored transaction_count in the blocks table does not match the number
269+ // of transactions in the transactions table. It returns the minimum and maximum
270+ // block numbers that have a mismatch, or (-1, -1) if all blocks are consistent.
271+ func GetTransactionMismatchRangeFromClickHouseV2 (chainId uint64 , startBlockNumber uint64 , endBlockNumber uint64 ) (int64 , int64 , error ) {
272+ if endBlockNumber < startBlockNumber {
273+ return - 1 , - 1 , nil
274+ }
275+
276+ blocksRaw , err := getBlocksFromV2 (chainId , startBlockNumber , endBlockNumber )
277+ if err != nil {
278+ return - 1 , - 1 , fmt .Errorf ("GetTransactionMismatchRangeFromClickHouseV2: failed to load blocks: %w" , err )
279+ }
280+
281+ // Aggregate transaction counts per block from the transactions table.
282+ query := fmt .Sprintf (
283+ "SELECT block_number, count() AS tx_count FROM %s.transactions FINAL WHERE chain_id = %d AND block_number BETWEEN %d AND %d GROUP BY block_number ORDER BY block_number" ,
284+ config .Cfg .CommitterClickhouseDatabase ,
285+ chainId ,
286+ startBlockNumber ,
287+ endBlockNumber ,
288+ )
289+
290+ txAggRows , err := execQueryV2 [blockTxAggregate ](query )
291+ if err != nil {
292+ return - 1 , - 1 , fmt .Errorf ("GetTransactionMismatchRangeFromClickHouseV2: failed to load tx aggregates: %w" , err )
293+ }
294+
295+ txCounts := make (map [uint64 ]uint64 , len (txAggRows ))
296+ for _ , row := range txAggRows {
297+ if row .BlockNumber == nil {
298+ continue
299+ }
300+ txCounts [row .BlockNumber .Uint64 ()] = row .TxCount
301+ }
302+
303+ var mismatchStart int64 = - 1
304+ var mismatchEnd int64 = - 1
305+
306+ for _ , block := range blocksRaw {
307+ if block .ChainId == nil || block .ChainId .Uint64 () == 0 || block .Number == nil {
308+ continue
309+ }
310+
311+ bn := block .Number .Uint64 ()
312+ expectedTxCount := block .TransactionCount
313+ actualTxCount , hasTx := txCounts [bn ]
314+
315+ mismatch := false
316+ if expectedTxCount == 0 {
317+ // Header says no transactions; ensure there are none in the table.
318+ if hasTx && actualTxCount > 0 {
319+ mismatch = true
320+ }
321+ } else {
322+ // Header says there should be transactions.
323+ if ! hasTx || actualTxCount != expectedTxCount {
324+ mismatch = true
325+ }
326+ }
327+
328+ if mismatch {
329+ if mismatchStart == - 1 || int64 (bn ) < mismatchStart {
330+ mismatchStart = int64 (bn )
331+ }
332+ if mismatchEnd == - 1 || int64 (bn ) > mismatchEnd {
333+ mismatchEnd = int64 (bn )
334+ }
335+ }
336+ }
337+
338+ return mismatchStart , mismatchEnd , nil
339+ }
340+
341+ // GetLogsMismatchRangeFromClickHouseV2 checks, for blocks in the given range,
342+ // where logs in the logs table are inconsistent with the block's logs_bloom:
343+ // - logsBloom is non-empty but there are no logs for that block
344+ // - logsBloom is empty/zero but logs exist
345+ // - log indexes are not contiguous (count(*) != max(log_index)+1 when logs exist)
346+ // It returns the minimum and maximum block numbers that have a mismatch, or
347+ // (-1, -1) if all blocks are consistent.
348+ func GetLogsMismatchRangeFromClickHouseV2 (chainId uint64 , startBlockNumber uint64 , endBlockNumber uint64 ) (int64 , int64 , error ) {
349+ if endBlockNumber < startBlockNumber {
350+ return - 1 , - 1 , nil
351+ }
352+
353+ blocksRaw , err := getBlocksFromV2 (chainId , startBlockNumber , endBlockNumber )
354+ if err != nil {
355+ return - 1 , - 1 , fmt .Errorf ("GetLogsMismatchRangeFromClickHouseV2: failed to load blocks: %w" , err )
356+ }
357+
358+ // Aggregate log counts and max log_index per block from the logs table.
359+ query := fmt .Sprintf (
360+ "SELECT block_number, count() AS log_count, max(log_index) AS max_log_index FROM %s.logs FINAL WHERE chain_id = %d AND block_number BETWEEN %d AND %d GROUP BY block_number ORDER BY block_number" ,
361+ config .Cfg .CommitterClickhouseDatabase ,
362+ chainId ,
363+ startBlockNumber ,
364+ endBlockNumber ,
365+ )
366+
367+ logAggRows , err := execQueryV2 [blockLogAggregate ](query )
368+ if err != nil {
369+ return - 1 , - 1 , fmt .Errorf ("GetLogsMismatchRangeFromClickHouseV2: failed to load log aggregates: %w" , err )
370+ }
371+
372+ logAggs := make (map [uint64 ]blockLogAggregate , len (logAggRows ))
373+ for _ , row := range logAggRows {
374+ if row .BlockNumber == nil {
375+ continue
376+ }
377+ bn := row .BlockNumber .Uint64 ()
378+ logAggs [bn ] = row
379+ }
380+
381+ var mismatchStart int64 = - 1
382+ var mismatchEnd int64 = - 1
383+
384+ for _ , block := range blocksRaw {
385+ if block .ChainId == nil || block .ChainId .Uint64 () == 0 || block .Number == nil {
386+ continue
387+ }
388+
389+ bn := block .Number .Uint64 ()
390+ hasLogsBloom := block .LogsBloom != "" && block .LogsBloom != EMPTY_LOGS_BLOOM
391+ logAgg , hasLogAgg := logAggs [bn ]
392+
393+ mismatch := false
394+
395+ if hasLogsBloom {
396+ // logsBloom indicates logs should exist
397+ if ! hasLogAgg || logAgg .LogCount == 0 {
398+ mismatch = true
399+ } else if logAgg .MaxLogIndex + 1 != logAgg .LogCount {
400+ // log_index should be contiguous from 0..log_count-1
401+ mismatch = true
402+ }
403+ } else {
404+ // logsBloom is empty/zero; there should be no logs
405+ if hasLogAgg && logAgg .LogCount > 0 {
406+ mismatch = true
407+ }
408+ }
409+
410+ if mismatch {
411+ if mismatchStart == - 1 || int64 (bn ) < mismatchStart {
412+ mismatchStart = int64 (bn )
413+ }
414+ if mismatchEnd == - 1 || int64 (bn ) > mismatchEnd {
415+ mismatchEnd = int64 (bn )
416+ }
417+ }
418+ }
419+
420+ return mismatchStart , mismatchEnd , nil
421+ }
422+
256423func getBlocksFromV2 (chainId uint64 , startBlockNumber uint64 , endBlockNumber uint64 ) ([]common.Block , error ) {
257424 sb := startBlockNumber
258425 length := endBlockNumber - startBlockNumber + 1
0 commit comments