Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit ae80e25

Browse files
stackman27amit-mominhuangzhen1997
authored
[Cherry Pick] Zircuit ZK Overflow detection (#1594) (#1595)
Support Zircuit fraud transactions detection and zk overflow detection, need dedup and unit test --------- ## Motivation ## Solution Co-authored-by: amit-momin <[email protected]> Co-authored-by: Joe Huang <[email protected]>
1 parent 5611118 commit ae80e25

File tree

15 files changed

+277
-17
lines changed

15 files changed

+277
-17
lines changed

.changeset/chilled-plants-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink": patch
3+
---
4+
5+
The findBroadcastedAttempts in detectStuckTransactionsHeuristic can returns uninitialized struct that potentially cause nil pointer error. Changed the return type of findBroadcastedAttempts to be pointers and added nil pointer check. #bugfix

.changeset/orange-humans-laugh.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink": minor
3+
---
4+
5+
Support Zircuit fraud transactions detection and zk overflow detection #added

ccip/config/evm/Sei_Mainnet.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
ChainID = '1329'
2+
ChainType = 'sei'
3+
# finality_depth: instant
4+
FinalityDepth = 10
5+
# block_time: ~0.4s, adding 1 second buffer
6+
LogPollInterval = '2s'
7+
# finality_depth * block_time / 60 secs = ~0.8 min (finality time)
8+
NoNewFinalizedHeadsThreshold = '5m'
9+
# "RPC node returned multiple missing blocks on query for block numbers [31592085 31592084] even though the WS subscription already sent us these blocks. It might help to increase EVM.RPCBlockQueryDelay (currently 1)"
10+
RPCBlockQueryDelay = 5
11+
12+
[GasEstimator]
13+
EIP1559DynamicFees = false
14+
Mode = 'BlockHistory'
15+
PriceMax = '3000 gwei' # recommended by ds&a
16+
17+
[GasEstimator.BlockHistory]
18+
BlockHistorySize = 200

core/chains/evm/config/chaintype/chaintype.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const (
2323
ChainXLayer ChainType = "xlayer"
2424
ChainZkEvm ChainType = "zkevm"
2525
ChainZkSync ChainType = "zksync"
26+
ChainZircuit ChainType = "zircuit"
2627
)
2728

2829
// IsL2 returns true if this chain is a Layer 2 chain. Notably:
@@ -39,7 +40,7 @@ func (c ChainType) IsL2() bool {
3940

4041
func (c ChainType) IsValid() bool {
4142
switch c {
42-
case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainSei, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync:
43+
case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainSei, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync, ChainZircuit:
4344
return true
4445
}
4546
return false
@@ -77,6 +78,8 @@ func FromSlug(slug string) ChainType {
7778
return ChainZkEvm
7879
case "zksync":
7980
return ChainZkSync
81+
case "zircuit":
82+
return ChainZircuit
8083
default:
8184
return ChainType(slug)
8285
}
@@ -144,4 +147,5 @@ var ErrInvalid = fmt.Errorf("must be one of %s or omitted", strings.Join([]strin
144147
string(ChainXLayer),
145148
string(ChainZkEvm),
146149
string(ChainZkSync),
150+
string(ChainZircuit),
147151
}, ", "))

core/chains/evm/config/toml/defaults/Zircuit_Mainnet.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
ChainID = '48900'
2-
ChainType = 'optimismBedrock'
2+
ChainType = 'zircuit'
33
FinalityTagEnabled = true
44
FinalityDepth = 1000
55
LinkContractAddress = '0x5D6d033B4FbD2190D99D930719fAbAcB64d2439a'

core/chains/evm/config/toml/defaults/Zircuit_Sepolia.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
ChainID = '48899'
2-
ChainType = 'optimismBedrock'
2+
ChainType = 'zircuit'
33
FinalityTagEnabled = true
44
FinalityDepth = 1000
55
LinkContractAddress = '0xDEE94506570cA186BC1e3516fCf4fd719C312cCD'

core/chains/evm/gas/chain_specific.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func chainSpecificIsUsable(tx evmtypes.Transaction, baseFee *assets.Wei, chainTy
1919
return false
2020
}
2121
}
22-
if chainType == chaintype.ChainOptimismBedrock || chainType == chaintype.ChainKroma || chainType == chaintype.ChainScroll {
22+
if chainType == chaintype.ChainOptimismBedrock || chainType == chaintype.ChainKroma || chainType == chaintype.ChainScroll || chainType == chaintype.ChainZircuit {
2323
// This is a special deposit transaction type introduced in Bedrock upgrade.
2424
// This is a system transaction that it will occur at least one time per block.
2525
// We should discard this type before even processing it to avoid flooding the

core/chains/evm/gas/rollups/l1_oracle.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chai
5757
var l1Oracle L1Oracle
5858
var err error
5959
switch chainType {
60-
case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainMantle:
60+
case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainMantle, chaintype.ChainZircuit:
6161
l1Oracle, err = NewOpStackL1GasOracle(lggr, ethClient, chainType)
6262
case chaintype.ChainArbitrum:
6363
l1Oracle, err = NewArbitrumL1GasOracle(lggr, ethClient)

core/chains/evm/gas/rollups/op_l1_oracle.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const (
101101
func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType) (*optimismL1Oracle, error) {
102102
var precompileAddress string
103103
switch chainType {
104-
case chaintype.ChainOptimismBedrock, chaintype.ChainMantle:
104+
case chaintype.ChainOptimismBedrock, chaintype.ChainMantle, chaintype.ChainZircuit:
105105
precompileAddress = OPGasOracleAddress
106106
case chaintype.ChainKroma:
107107
precompileAddress = KromaGasOracleAddress

core/chains/evm/txmgr/stuck_tx_detector.go

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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
274296
func (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
341441
func (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

Comments
 (0)