Skip to content

Commit

Permalink
[Cherry Pick] Zircuit ZK Overflow detection (#1594)
Browse files Browse the repository at this point in the history
Support Zircuit fraud transactions detection and zk overflow detection,
need dedup and unit test

---------

Co-authored-by: Joe Huang <[email protected]>
Co-authored-by: stackman27 <[email protected]>
  • Loading branch information
3 people committed Jan 9, 2025
1 parent 5611118 commit 1476004
Show file tree
Hide file tree
Showing 15 changed files with 277 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/chilled-plants-clap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": patch
---

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
5 changes: 5 additions & 0 deletions .changeset/orange-humans-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"chainlink": minor
---

Support Zircuit fraud transactions detection and zk overflow detection #added
18 changes: 18 additions & 0 deletions ccip/config/evm/Sei_Mainnet.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ChainID = '1329'
ChainType = 'sei'
# finality_depth: instant
FinalityDepth = 10
# block_time: ~0.4s, adding 1 second buffer
LogPollInterval = '2s'
# finality_depth * block_time / 60 secs = ~0.8 min (finality time)
NoNewFinalizedHeadsThreshold = '5m'
# "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)"
RPCBlockQueryDelay = 5

[GasEstimator]
EIP1559DynamicFees = false
Mode = 'BlockHistory'
PriceMax = '3000 gwei' # recommended by ds&a

[GasEstimator.BlockHistory]
BlockHistorySize = 200
6 changes: 5 additions & 1 deletion core/chains/evm/config/chaintype/chaintype.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
ChainXLayer ChainType = "xlayer"
ChainZkEvm ChainType = "zkevm"
ChainZkSync ChainType = "zksync"
ChainZircuit ChainType = "zircuit"
)

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

func (c ChainType) IsValid() bool {
switch c {
case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainSei, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync:
case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainSei, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync, ChainZircuit:
return true
}
return false
Expand Down Expand Up @@ -77,6 +78,8 @@ func FromSlug(slug string) ChainType {
return ChainZkEvm
case "zksync":
return ChainZkSync
case "zircuit":
return ChainZircuit
default:
return ChainType(slug)
}
Expand Down Expand Up @@ -144,4 +147,5 @@ var ErrInvalid = fmt.Errorf("must be one of %s or omitted", strings.Join([]strin
string(ChainXLayer),
string(ChainZkEvm),
string(ChainZkSync),
string(ChainZircuit),
}, ", "))
2 changes: 1 addition & 1 deletion core/chains/evm/config/toml/defaults/Zircuit_Mainnet.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ChainID = '48900'
ChainType = 'optimismBedrock'
ChainType = 'zircuit'
FinalityTagEnabled = true
FinalityDepth = 1000
LinkContractAddress = '0x5D6d033B4FbD2190D99D930719fAbAcB64d2439a'
Expand Down
2 changes: 1 addition & 1 deletion core/chains/evm/config/toml/defaults/Zircuit_Sepolia.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ChainID = '48899'
ChainType = 'optimismBedrock'
ChainType = 'zircuit'
FinalityTagEnabled = true
FinalityDepth = 1000
LinkContractAddress = '0xDEE94506570cA186BC1e3516fCf4fd719C312cCD'
Expand Down
2 changes: 1 addition & 1 deletion core/chains/evm/gas/chain_specific.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ func chainSpecificIsUsable(tx evmtypes.Transaction, baseFee *assets.Wei, chainTy
return false
}
}
if chainType == chaintype.ChainOptimismBedrock || chainType == chaintype.ChainKroma || chainType == chaintype.ChainScroll {
if chainType == chaintype.ChainOptimismBedrock || chainType == chaintype.ChainKroma || chainType == chaintype.ChainScroll || chainType == chaintype.ChainZircuit {
// This is a special deposit transaction type introduced in Bedrock upgrade.
// This is a system transaction that it will occur at least one time per block.
// We should discard this type before even processing it to avoid flooding the
Expand Down
2 changes: 1 addition & 1 deletion core/chains/evm/gas/rollups/l1_oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chai
var l1Oracle L1Oracle
var err error
switch chainType {
case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainMantle:
case chaintype.ChainOptimismBedrock, chaintype.ChainKroma, chaintype.ChainScroll, chaintype.ChainMantle, chaintype.ChainZircuit:
l1Oracle, err = NewOpStackL1GasOracle(lggr, ethClient, chainType)
case chaintype.ChainArbitrum:
l1Oracle, err = NewArbitrumL1GasOracle(lggr, ethClient)
Expand Down
2 changes: 1 addition & 1 deletion core/chains/evm/gas/rollups/op_l1_oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ const (
func NewOpStackL1GasOracle(lggr logger.Logger, ethClient l1OracleClient, chainType chaintype.ChainType) (*optimismL1Oracle, error) {
var precompileAddress string
switch chainType {
case chaintype.ChainOptimismBedrock, chaintype.ChainMantle:
case chaintype.ChainOptimismBedrock, chaintype.ChainMantle, chaintype.ChainZircuit:
precompileAddress = OPGasOracleAddress
case chaintype.ChainKroma:
precompileAddress = KromaGasOracleAddress
Expand Down
110 changes: 105 additions & 5 deletions core/chains/evm/txmgr/stuck_tx_detector.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ func (d *stuckTxDetector) DetectStuckTransactions(ctx context.Context, enabledAd
return d.detectStuckTransactionsScroll(ctx, txs)
case chaintype.ChainZkEvm, chaintype.ChainXLayer:
return d.detectStuckTransactionsZkEVM(ctx, txs)
case chaintype.ChainZircuit:
return d.detectStuckTransactionsZircuit(ctx, txs, blockNum)
default:
return d.detectStuckTransactionsHeuristic(ctx, txs, blockNum)
}
Expand Down Expand Up @@ -215,10 +217,25 @@ func (d *stuckTxDetector) detectStuckTransactionsHeuristic(ctx context.Context,
}
// Tx attempts are loaded from newest to oldest
oldestBroadcastAttempt, newestBroadcastAttempt, broadcastedAttemptsCount := findBroadcastedAttempts(tx)
d.lggr.Debugf("found %d broadcasted attempts for tx id %d in stuck transaction heuristic", broadcastedAttemptsCount, tx.ID)

// attempt shouldn't be nil as we validated in FindUnconfirmedTxWithLowestNonce, but added anyway for a "belts and braces" approach
if oldestBroadcastAttempt == nil || newestBroadcastAttempt == nil {
d.lggr.Debugw("failed to find broadcast attempt for tx in stuck transaction heuristic", "tx", tx)
continue
}

// sanity check
if oldestBroadcastAttempt.BroadcastBeforeBlockNum == nil {
d.lggr.Debugw("BroadcastBeforeBlockNum was not set for broadcast attempt in stuck transaction heuristic", "attempt", oldestBroadcastAttempt)
continue
}

// 2. Check if Threshold amount of blocks have passed since the oldest attempt's broadcast block num
if *oldestBroadcastAttempt.BroadcastBeforeBlockNum > blockNum-int64(*d.cfg.Threshold()) {
continue
}

// 3. Check if the transaction has at least MinAttempts amount of broadcasted attempts
if broadcastedAttemptsCount < *d.cfg.MinAttempts() {
continue
Expand All @@ -244,17 +261,18 @@ func compareGasFees(attemptGas gas.EvmFee, marketGas gas.EvmFee) int {
}

// Assumes tx attempts are loaded newest to oldest
func findBroadcastedAttempts(tx Tx) (oldestAttempt TxAttempt, newestAttempt TxAttempt, broadcastedCount uint32) {
func findBroadcastedAttempts(tx Tx) (oldestAttempt *TxAttempt, newestAttempt *TxAttempt, broadcastedCount uint32) {
foundNewest := false
for _, attempt := range tx.TxAttempts {
for i := range tx.TxAttempts {
attempt := tx.TxAttempts[i]
if attempt.State != types.TxAttemptBroadcast {
continue
}
if !foundNewest {
newestAttempt = attempt
newestAttempt = &attempt
foundNewest = true
}
oldestAttempt = attempt
oldestAttempt = &attempt
broadcastedCount++
}
return
Expand All @@ -270,6 +288,10 @@ type scrollResponse struct {
Data map[string]int `json:"data"`
}

type zircuitResponse struct {
IsQuarantined bool `json:"isQuarantined"`
}

// Uses the custom Scroll skipped endpoint to determine an overflow transaction
func (d *stuckTxDetector) detectStuckTransactionsScroll(ctx context.Context, txs []Tx) ([]Tx, error) {
if d.cfg.DetectionApiUrl() == nil {
Expand Down Expand Up @@ -336,6 +358,84 @@ func (d *stuckTxDetector) detectStuckTransactionsScroll(ctx context.Context, txs
return stuckTx, nil
}

// return fraud and overflow transactions
func (d *stuckTxDetector) detectStuckTransactionsZircuit(ctx context.Context, txs []Tx, blockNum int64) ([]Tx, error) {
var err error
var fraudTxs, stuckTxs []Tx
fraudTxs, err = d.detectFraudTransactionsZircuit(ctx, txs)
if err != nil {
d.lggr.Errorf("Failed to detect zircuit fraud transactions: %v", err)
}

stuckTxs, err = d.detectStuckTransactionsHeuristic(ctx, txs, blockNum)
if err != nil {
return txs, err
}

// prevent duplicate transactions from the fraudTxs and stuckTxs with a map
uniqueTxs := make(map[int64]Tx)
for _, tx := range fraudTxs {
uniqueTxs[tx.ID] = tx
}

for _, tx := range stuckTxs {
uniqueTxs[tx.ID] = tx
}

var combinedStuckTxs []Tx
for _, tx := range uniqueTxs {
combinedStuckTxs = append(combinedStuckTxs, tx)
}

return combinedStuckTxs, nil
}

// Uses zirc_isQuarantined to check whether the transactions are considered as malicious by the sequencer and
// preventing their inclusion into a block
func (d *stuckTxDetector) detectFraudTransactionsZircuit(ctx context.Context, txs []Tx) ([]Tx, error) {
txReqs := make([]rpc.BatchElem, len(txs))
txHashMap := make(map[common.Hash]Tx)
txRes := make([]*zircuitResponse, len(txs))

// Build batch request elems to perform
for i, tx := range txs {
latestAttemptHash := tx.TxAttempts[0].Hash
var result zircuitResponse
txReqs[i] = rpc.BatchElem{
Method: "zirc_isQuarantined",
Args: []interface{}{
latestAttemptHash,
},
Result: &result,
}
txHashMap[latestAttemptHash] = tx
txRes[i] = &result
}

// Send batch request
err := d.chainClient.BatchCallContext(ctx, txReqs)
if err != nil {
return nil, fmt.Errorf("failed to check Quarantine transactions in batch: %w", err)
}

// If the result is not nil, the fraud transaction is flagged as quarantined
var fraudTxs []Tx
for i, req := range txReqs {
txHash := req.Args[0].(common.Hash)
if req.Error != nil {
d.lggr.Errorf("failed to check fraud transaction by hash (%s): %v", txHash.String(), req.Error)
continue
}

result := txRes[i]
if result != nil && result.IsQuarantined {
tx := txHashMap[txHash]
fraudTxs = append(fraudTxs, tx)
}
}
return fraudTxs, nil
}

// Uses eth_getTransactionByHash to detect that a transaction has been discarded due to overflow
// Currently only used by zkEVM but if other chains follow the same behavior in the future
func (d *stuckTxDetector) detectStuckTransactionsZkEVM(ctx context.Context, txs []Tx) ([]Tx, error) {
Expand Down Expand Up @@ -390,7 +490,7 @@ func (d *stuckTxDetector) detectStuckTransactionsZkEVM(ctx context.Context, txs
for i, req := range txReqs {
txHash := req.Args[0].(common.Hash)
if req.Error != nil {
d.lggr.Debugf("failed to get transaction by hash (%s): %v", txHash.String(), req.Error)
d.lggr.Errorf("failed to get transaction by hash (%s): %v", txHash.String(), req.Error)
continue
}
result := *txRes[i]
Expand Down
Loading

0 comments on commit 1476004

Please sign in to comment.