Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Zircuit fraud transactions and zk overflow detection #14629

Merged
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
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 @@ -22,6 +22,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 @@ -38,7 +39,7 @@ func (c ChainType) IsL2() bool {

func (c ChainType) IsValid() bool {
switch c {
case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync:
case "", ChainArbitrum, ChainAstar, ChainCelo, ChainGnosis, ChainHedera, ChainKroma, ChainMantle, ChainMetis, ChainOptimismBedrock, ChainScroll, ChainWeMix, ChainXLayer, ChainZkEvm, ChainZkSync, ChainZircuit:
return true
}
return false
Expand Down Expand Up @@ -74,6 +75,8 @@ func FromSlug(slug string) ChainType {
return ChainZkEvm
case "zksync":
return ChainZkSync
case "zircuit":
return ChainZircuit
default:
return ChainType(slug)
}
Expand Down Expand Up @@ -140,4 +143,5 @@ var ErrInvalid = fmt.Errorf("must be one of %s or omitted", strings.Join([]strin
string(ChainXLayer),
string(ChainZkEvm),
string(ChainZkSync),
string(ChainZircuit),
}, ", "))
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
ChainID = '48900'
ChainType = 'optimismBedrock'
ChainType = 'zircuit'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also add that for the Sepolia defaults?

FinalityTagEnabled = true
FinalityDepth = 1000
LinkContractAddress = '0x5D6d033B4FbD2190D99D930719fAbAcB64d2439a'
Expand Down
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 @@ -54,7 +54,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 @@ -95,7 +95,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
86 changes: 85 additions & 1 deletion 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 @@ -270,6 +272,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 +342,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 +474,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
102 changes: 102 additions & 0 deletions core/chains/evm/txmgr/stuck_tx_detector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,108 @@ func TestStuckTxDetector_DetectStuckTransactionsHeuristic(t *testing.T) {
})
}

func TestStuckTxDetector_DetectStuckTransactionsZircuit(t *testing.T) {
t.Parallel()

db := pgtest.NewSqlxDB(t)
txStore := cltest.NewTestTxStore(t, db)
ethKeyStore := cltest.NewKeyStore(t, db).Eth()
ctx := tests.Context(t)

lggr := logger.Test(t)
feeEstimator := gasmocks.NewEvmFeeEstimator(t)
// Return 10 gwei as market gas price
marketGasPrice := tenGwei
fee := gas.EvmFee{Legacy: marketGasPrice}
feeEstimator.On("GetFee", mock.Anything, []byte{}, uint64(0), mock.Anything, mock.Anything, mock.Anything).Return(fee, uint64(0), nil)
ethClient := testutils.NewEthClientMockWithDefaultChain(t)
autoPurgeThreshold := uint32(5)
autoPurgeMinAttempts := uint32(3)
autoPurgeCfg := testAutoPurgeConfig{
enabled: true, // Enable auto-purge feature for testing
threshold: &autoPurgeThreshold,
minAttempts: &autoPurgeMinAttempts,
}
blockNum := int64(100)
stuckTxDetector := txmgr.NewStuckTxDetector(lggr, testutils.FixtureChainID, chaintype.ChainZircuit, assets.NewWei(assets.NewEth(100).ToInt()), autoPurgeCfg, feeEstimator, txStore, ethClient)

t.Run("returns empty list if no fraud or stuck transactions identified", func(t *testing.T) {
_, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore)
tx := mustInsertUnconfirmedTxWithBroadcastAttempts(t, txStore, 0, fromAddress, 1, blockNum, tenGwei)
attempts := tx.TxAttempts[0]
// Request still returns transaction by hash, transaction not discarded by network and not considered stuck
ethClient.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return len(b) == 1 && cltest.BatchElemMatchesParams(b[0], attempts.Hash, "zirc_isQuarantined")
})).Return(nil).Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
resp, err := json.Marshal(struct {
IsQuarantined bool `json:"isQuarantined"`
}{IsQuarantined: false})
require.NoError(t, err)
elems[0].Error = json.Unmarshal(resp, elems[0].Result)
}).Once()

txs, err := stuckTxDetector.DetectStuckTransactions(ctx, []common.Address{fromAddress}, blockNum)
require.NoError(t, err)
require.Len(t, txs, 0)
})

t.Run("returns fraud transactions identified", func(t *testing.T) {
_, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore)
tx := mustInsertUnconfirmedTxWithBroadcastAttempts(t, txStore, 0, fromAddress, 1, blockNum, tenGwei)
attempts := tx.TxAttempts[0]
// Request still returns transaction by hash, transaction not discarded by network and not considered stuck
ethClient.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return len(b) == 1 && cltest.BatchElemMatchesParams(b[0], attempts.Hash, "zirc_isQuarantined")
})).Return(nil).Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
resp, err := json.Marshal(struct {
IsQuarantined bool `json:"isQuarantined"`
}{IsQuarantined: true})
require.NoError(t, err)
elems[0].Error = json.Unmarshal(resp, elems[0].Result)
}).Once()

txs, err := stuckTxDetector.DetectStuckTransactions(ctx, []common.Address{fromAddress}, blockNum)
require.NoError(t, err)
require.Len(t, txs, 1)
})

t.Run("returns the transaction only once if it's identified as both fraud and stuck", func(t *testing.T) {
_, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore)
tx := mustInsertUnconfirmedTxWithBroadcastAttempts(t, txStore, 0, fromAddress, autoPurgeMinAttempts, blockNum-int64(autoPurgeThreshold)+int64(autoPurgeMinAttempts-1), marketGasPrice.Add(oneGwei))
attempts := tx.TxAttempts[0]

ethClient.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return len(b) == 1 && cltest.BatchElemMatchesParams(b[0], attempts.Hash, "zirc_isQuarantined")
})).Return(nil).Run(func(args mock.Arguments) {
elems := args.Get(1).([]rpc.BatchElem)
resp, err := json.Marshal(struct {
IsQuarantined bool `json:"isQuarantined"`
}{IsQuarantined: true})
require.NoError(t, err)
elems[0].Error = json.Unmarshal(resp, elems[0].Result)
}).Once()

txs, err := stuckTxDetector.DetectStuckTransactions(ctx, []common.Address{fromAddress}, blockNum)
require.NoError(t, err)
require.Len(t, txs, 1)
})
amit-momin marked this conversation as resolved.
Show resolved Hide resolved
t.Run("returns the stuck tx even if failed to detect fraud tx", func(t *testing.T) {
_, fromAddress := cltest.MustInsertRandomKey(t, ethKeyStore)
tx := mustInsertUnconfirmedTxWithBroadcastAttempts(t, txStore, 0, fromAddress, autoPurgeMinAttempts, blockNum-int64(autoPurgeThreshold)+int64(autoPurgeMinAttempts-1), marketGasPrice.Add(oneGwei))
attempts := tx.TxAttempts[0]

ethClient.On("BatchCallContext", mock.Anything, mock.MatchedBy(func(b []rpc.BatchElem) bool {
return len(b) == 1 && cltest.BatchElemMatchesParams(b[0], attempts.Hash, "zirc_isQuarantined")
})).Return(fmt.Errorf("failed to fetch rpc"))

txs, err := stuckTxDetector.DetectStuckTransactions(ctx, []common.Address{fromAddress}, blockNum)
require.NoError(t, err)
require.Len(t, txs, 1)
})
}

func TestStuckTxDetector_DetectStuckTransactionsZkEVM(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 2 additions & 2 deletions core/services/chainlink/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1364,7 +1364,7 @@ func TestConfig_Validate(t *testing.T) {
- 1: 10 errors:
- ChainType: invalid value (Foo): must not be set with this chain id
- Nodes: missing: must have at least one node
- ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted
- ChainType: invalid value (Foo): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync, zircuit or omitted
- HeadTracker.HistoryDepth: invalid value (30): must be greater than or equal to FinalizedBlockOffset
- GasEstimator.BumpThreshold: invalid value (0): cannot be 0 if auto-purge feature is enabled for Foo
- Transactions.AutoPurge.Threshold: missing: needs to be set if auto-purge feature is enabled for Foo
Expand All @@ -1377,7 +1377,7 @@ func TestConfig_Validate(t *testing.T) {
- 2: 5 errors:
- ChainType: invalid value (Arbitrum): only "optimismBedrock" can be used with this chain id
- Nodes: missing: must have at least one node
- ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync or omitted
- ChainType: invalid value (Arbitrum): must be one of arbitrum, astar, celo, gnosis, hedera, kroma, mantle, metis, optimismBedrock, scroll, wemix, xlayer, zkevm, zksync, zircuit or omitted
- FinalityDepth: invalid value (0): must be greater than or equal to 1
- MinIncomingConfirmations: invalid value (0): must be greater than or equal to 1
- 3: 3 errors:
Expand Down
2 changes: 1 addition & 1 deletion core/services/ocr/contract_tracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ func (t *OCRContractTracker) LatestBlockHeight(ctx context.Context) (blockheight
// care about the block height; we have no way of getting the L1 block
// height anyway
return 0, nil
case "", chaintype.ChainArbitrum, chaintype.ChainAstar, chaintype.ChainCelo, chaintype.ChainGnosis, chaintype.ChainHedera, chaintype.ChainKroma, chaintype.ChainOptimismBedrock, chaintype.ChainScroll, chaintype.ChainWeMix, chaintype.ChainXLayer, chaintype.ChainZkEvm, chaintype.ChainZkSync:
case "", chaintype.ChainArbitrum, chaintype.ChainAstar, chaintype.ChainCelo, chaintype.ChainGnosis, chaintype.ChainHedera, chaintype.ChainKroma, chaintype.ChainOptimismBedrock, chaintype.ChainScroll, chaintype.ChainWeMix, chaintype.ChainXLayer, chaintype.ChainZkEvm, chaintype.ChainZkSync, chaintype.ChainZircuit:
// continue
}
latestBlockHeight := t.getLatestBlockHeight()
Expand Down
2 changes: 1 addition & 1 deletion core/services/ocrcommon/block_translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func NewBlockTranslator(cfg Config, client evmclient.Client, lggr logger.Logger)
switch cfg.ChainType() {
case chaintype.ChainArbitrum:
return NewArbitrumBlockTranslator(client, lggr)
case "", chaintype.ChainCelo, chaintype.ChainGnosis, chaintype.ChainKroma, chaintype.ChainMetis, chaintype.ChainOptimismBedrock, chaintype.ChainScroll, chaintype.ChainWeMix, chaintype.ChainXLayer, chaintype.ChainZkEvm, chaintype.ChainZkSync:
case "", chaintype.ChainCelo, chaintype.ChainGnosis, chaintype.ChainKroma, chaintype.ChainMetis, chaintype.ChainOptimismBedrock, chaintype.ChainScroll, chaintype.ChainWeMix, chaintype.ChainXLayer, chaintype.ChainZkEvm, chaintype.ChainZkSync, chaintype.ChainZircuit:
fallthrough
default:
return &l1BlockTranslator{}
Expand Down
4 changes: 2 additions & 2 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6752,7 +6752,7 @@ GasLimitDefault = 400000
AutoCreateKey = true
BlockBackfillDepth = 10
BlockBackfillSkip = false
ChainType = 'optimismBedrock'
ChainType = 'zircuit'
FinalityDepth = 1000
FinalityTagEnabled = true
LinkContractAddress = '0xDEE94506570cA186BC1e3516fCf4fd719C312cCD'
Expand Down Expand Up @@ -6859,7 +6859,7 @@ GasLimitDefault = 400000
AutoCreateKey = true
BlockBackfillDepth = 10
BlockBackfillSkip = false
ChainType = 'optimismBedrock'
ChainType = 'zircuit'
FinalityDepth = 1000
FinalityTagEnabled = true
LinkContractAddress = '0x5D6d033B4FbD2190D99D930719fAbAcB64d2439a'
Expand Down
Loading