@@ -130,6 +130,8 @@ func (d *stuckTxDetector) DetectStuckTransactions(ctx context.Context, enabledAd
130130 return d .detectStuckTransactionsScroll (ctx , txs )
131131 case chaintype .ChainZkEvm , chaintype .ChainXLayer :
132132 return d .detectStuckTransactionsZkEVM (ctx , txs )
133+ case chaintype .ChainZircuit :
134+ return d .detectStuckTransactionsZircuit (ctx , txs , blockNum )
133135 default :
134136 return d .detectStuckTransactionsHeuristic (ctx , txs , blockNum )
135137 }
@@ -215,10 +217,25 @@ func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context,
215217 }
216218 // Tx attempts are loaded from newest to oldest
217219 oldestBroadcastAttempt , newestBroadcastAttempt , broadcastedAttemptsCount := findBroadcastedAttempts (tx )
220+ d .lggr .Debugf ("found %d broadcasted attempts for tx id %d in stuck transaction heuristic" , broadcastedAttemptsCount , tx .ID )
221+
222+ // attempt shouldn't be nil as we validated in FindUnconfirmedTxWithLowestNonce, but added anyway for a "belts and braces" approach
223+ if oldestBroadcastAttempt == nil || newestBroadcastAttempt == nil {
224+ d .lggr .Debugw ("failed to find broadcast attempt for tx in stuck transaction heuristic" , "tx" , tx )
225+ continue
226+ }
227+
228+ // sanity check
229+ if oldestBroadcastAttempt .BroadcastBeforeBlockNum == nil {
230+ d .lggr .Debugw ("BroadcastBeforeBlockNum was not set for broadcast attempt in stuck transaction heuristic" , "attempt" , oldestBroadcastAttempt )
231+ continue
232+ }
233+
218234 // 2. Check if Threshold amount of blocks have passed since the oldest attempt's broadcast block num
219235 if * oldestBroadcastAttempt .BroadcastBeforeBlockNum > blockNum - int64 (* d .cfg .Threshold ()) {
220236 continue
221237 }
238+
222239 // 3. Check if the transaction has at least MinAttempts amount of broadcasted attempts
223240 if broadcastedAttemptsCount < * d .cfg .MinAttempts () {
224241 continue
@@ -244,17 +261,18 @@ func compareGasFees(attemptGas gas.EvmFee, marketGas gas.EvmFee) int {
244261}
245262
246263// Assumes tx attempts are loaded newest to oldest
247- func findBroadcastedAttempts (tx Tx ) (oldestAttempt TxAttempt , newestAttempt TxAttempt , broadcastedCount uint32 ) {
264+ func findBroadcastedAttempts (tx Tx ) (oldestAttempt * TxAttempt , newestAttempt * TxAttempt , broadcastedCount uint32 ) {
248265 foundNewest := false
249- for _ , attempt := range tx .TxAttempts {
266+ for i := range tx .TxAttempts {
267+ attempt := tx .TxAttempts [i ]
250268 if attempt .State != types .TxAttemptBroadcast {
251269 continue
252270 }
253271 if ! foundNewest {
254- newestAttempt = attempt
272+ newestAttempt = & attempt
255273 foundNewest = true
256274 }
257- oldestAttempt = attempt
275+ oldestAttempt = & attempt
258276 broadcastedCount ++
259277 }
260278 return
@@ -270,6 +288,10 @@ type scrollResponse struct {
270288 Data map [string ]int `json:"data"`
271289}
272290
291+ type zircuitResponse struct {
292+ IsQuarantined bool `json:"isQuarantined"`
293+ }
294+
273295// Uses the custom Scroll skipped endpoint to determine an overflow transaction
274296func (d * stuckTxDetector ) detectStuckTransactionsScroll (ctx context.Context , txs []Tx ) ([]Tx , error ) {
275297 if d .cfg .DetectionApiUrl () == nil {
@@ -336,6 +358,84 @@ func (d *stuckTxDetector) detectStuckTransactionsScroll(ctx context.Context, txs
336358 return stuckTx , nil
337359}
338360
361+ // return fraud and overflow transactions
362+ func (d * stuckTxDetector ) detectStuckTransactionsZircuit (ctx context.Context , txs []Tx , blockNum int64 ) ([]Tx , error ) {
363+ var err error
364+ var fraudTxs , stuckTxs []Tx
365+ fraudTxs , err = d .detectFraudTransactionsZircuit (ctx , txs )
366+ if err != nil {
367+ d .lggr .Errorf ("Failed to detect zircuit fraud transactions: %v" , err )
368+ }
369+
370+ stuckTxs , err = d .detectStuckTransactionsHeuristic (ctx , txs , blockNum )
371+ if err != nil {
372+ return txs , err
373+ }
374+
375+ // prevent duplicate transactions from the fraudTxs and stuckTxs with a map
376+ uniqueTxs := make (map [int64 ]Tx )
377+ for _ , tx := range fraudTxs {
378+ uniqueTxs [tx .ID ] = tx
379+ }
380+
381+ for _ , tx := range stuckTxs {
382+ uniqueTxs [tx .ID ] = tx
383+ }
384+
385+ var combinedStuckTxs []Tx
386+ for _ , tx := range uniqueTxs {
387+ combinedStuckTxs = append (combinedStuckTxs , tx )
388+ }
389+
390+ return combinedStuckTxs , nil
391+ }
392+
393+ // Uses zirc_isQuarantined to check whether the transactions are considered as malicious by the sequencer and
394+ // preventing their inclusion into a block
395+ func (d * stuckTxDetector ) detectFraudTransactionsZircuit (ctx context.Context , txs []Tx ) ([]Tx , error ) {
396+ txReqs := make ([]rpc.BatchElem , len (txs ))
397+ txHashMap := make (map [common.Hash ]Tx )
398+ txRes := make ([]* zircuitResponse , len (txs ))
399+
400+ // Build batch request elems to perform
401+ for i , tx := range txs {
402+ latestAttemptHash := tx .TxAttempts [0 ].Hash
403+ var result zircuitResponse
404+ txReqs [i ] = rpc.BatchElem {
405+ Method : "zirc_isQuarantined" ,
406+ Args : []interface {}{
407+ latestAttemptHash ,
408+ },
409+ Result : & result ,
410+ }
411+ txHashMap [latestAttemptHash ] = tx
412+ txRes [i ] = & result
413+ }
414+
415+ // Send batch request
416+ err := d .chainClient .BatchCallContext (ctx , txReqs )
417+ if err != nil {
418+ return nil , fmt .Errorf ("failed to check Quarantine transactions in batch: %w" , err )
419+ }
420+
421+ // If the result is not nil, the fraud transaction is flagged as quarantined
422+ var fraudTxs []Tx
423+ for i , req := range txReqs {
424+ txHash := req .Args [0 ].(common.Hash )
425+ if req .Error != nil {
426+ d .lggr .Errorf ("failed to check fraud transaction by hash (%s): %v" , txHash .String (), req .Error )
427+ continue
428+ }
429+
430+ result := txRes [i ]
431+ if result != nil && result .IsQuarantined {
432+ tx := txHashMap [txHash ]
433+ fraudTxs = append (fraudTxs , tx )
434+ }
435+ }
436+ return fraudTxs , nil
437+ }
438+
339439// Uses eth_getTransactionByHash to detect that a transaction has been discarded due to overflow
340440// Currently only used by zkEVM but if other chains follow the same behavior in the future
341441func (d * stuckTxDetector ) detectStuckTransactionsZkEVM (ctx context.Context , txs []Tx ) ([]Tx , error ) {
@@ -390,7 +490,7 @@ func (d *stuckTxDetector) detectStuckTransactionsZkEVM(ctx context.Context, txs
390490 for i , req := range txReqs {
391491 txHash := req .Args [0 ].(common.Hash )
392492 if req .Error != nil {
393- d .lggr .Debugf ("failed to get transaction by hash (%s): %v" , txHash .String (), req .Error )
493+ d .lggr .Errorf ("failed to get transaction by hash (%s): %v" , txHash .String (), req .Error )
394494 continue
395495 }
396496 result := * txRes [i ]
0 commit comments