@@ -5,19 +5,20 @@ import {
55 getWalletRPC ,
66} from "../utils/ethers" ;
77import { JsonRpcProvider } from "@ethersproject/providers" ;
8- import { getL2Network } from "@arbitrum/sdk" ;
8+ import { getArbitrumNetwork } from "@arbitrum/sdk" ;
99import { NODE_INTERFACE_ADDRESS } from "@arbitrum/sdk/dist/lib/dataEntities/constants" ;
1010import { NodeInterface__factory } from "@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory" ;
1111import { SequencerInbox__factory } from "@arbitrum/sdk/dist/lib/abi/factories/SequencerInbox__factory" ;
1212import { BigNumber , ContractTransaction , constants } from "ethers" ;
1313import { Block , Log , TransactionReceipt , BlockWithTransactions } from "@ethersproject/abstract-provider" ;
1414import { SequencerInbox } from "@arbitrum/sdk/dist/lib/abi/SequencerInbox" ;
15+ import { NodeInterface } from "@arbitrum/sdk/dist/lib/abi/NodeInterface" ;
1516
1617require ( "dotenv" ) . config ( ) ;
1718
18- interface ChallengeTxn {
19- timeSent : string ;
20- timeRamp : number ;
19+ interface ChallengeProgess {
20+ challengeTnxHash : string ;
21+ sentSnapshotTnxHash ?: string ;
2122}
2223
2324// https://github.com/prysmaticlabs/prysm/blob/493905ee9e33a64293b66823e69704f012b39627/config/params/mainnet_config.go#L103
@@ -52,7 +53,7 @@ const watch = async () => {
5253 ) ) as BigNumber ;
5354
5455 // get Arb sequencer params
55- const l2Network = await getL2Network ( providerArb ) ;
56+ const l2Network = await getArbitrumNetwork ( providerArb ) ;
5657 const sequencer = SequencerInbox__factory . connect ( l2Network . ethBridge . sequencerInbox , providerEth ) ;
5758 const maxDelaySeconds = (
5859 ( await retryOperation ( ( ) => sequencer . maxTimeVariation ( ) , 1000 , 10 ) ) [ 1 ] as BigNumber
@@ -115,7 +116,8 @@ const watch = async () => {
115116 . fill ( veaEpochOutboxWacthLowerBound )
116117 . map ( ( el , i ) => el + i ) ;
117118 // epoch => (minChallengePeriodDeadline, maxPriorityFeePerGas, maxFeePerGas)
118- const challengeTxnHashes = new Map < number , string > ( ) ;
119+
120+ const challenges = new Map < number , ChallengeProgess > ( ) ;
119121
120122 console . log (
121123 "cold start: checking past claim history from epoch " +
@@ -346,11 +348,20 @@ const watch = async () => {
346348 }
347349 } else {
348350 console . log ( "claim " + veaEpochOutboxCheck + " is already challenged" ) ;
351+ console . log ( "challenge is finalized" ) ;
349352 if ( logChallenges [ 0 ] . blockNumber < blockFinalizedGnosis . number ) {
350- veaEpochOutboxCheckClaimsRangeArray . splice ( index , 1 ) ;
351- index -- ;
352- // the challenge is finalized, no further action needed
353- console . log ( "challenge is finalized" ) ;
353+ if ( logChallenges [ 0 ] . topics [ 1 ] === watcherAddress ) {
354+ console . log ( "challenge by bot detected, calling sendSnaphot" ) ;
355+ const txnReceipt = ( await retryOperation (
356+ ( ) => providerArb . getTransactionReceipt ( challenges . get ( index ) ) ,
357+ 10 ,
358+ 1000
359+ ) ) as TransactionReceipt ;
360+ if ( ! txnReceipt ) {
361+ console . log ( "challenge txn " + challengeTxnHashes . get ( index ) + " not mined yet" ) ;
362+ continue ;
363+ }
364+ }
354365 continue ;
355366 } else {
356367 console . log (
@@ -360,14 +371,15 @@ const watch = async () => {
360371 continue ;
361372 }
362373
363- if ( challengeTxnHashes . has ( index ) ) {
374+ if ( challenges . has ( index ) ) {
375+ const challengeProgess = challenges . get ( index ) ;
364376 const txnReceipt = ( await retryOperation (
365- ( ) => providerGnosis . getTransactionReceipt ( challengeTxnHashes . get ( index ) ) ,
377+ ( ) => providerGnosis . getTransactionReceipt ( challengeProgess . challengeTnxHash ) ,
366378 10 ,
367379 1000
368380 ) ) as TransactionReceipt ;
369381 if ( ! txnReceipt ) {
370- console . log ( "challenge txn " + challengeTxnHashes . get ( index ) + " not mined yet" ) ;
382+ console . log ( "challenge txn " + challengeProgess . challengeTnxHash + " not mined yet" ) ;
371383 continue ;
372384 }
373385 const blockNumber = txnReceipt . blockNumber ;
@@ -378,7 +390,6 @@ const watch = async () => {
378390 ) ) as Block ;
379391 if ( challengeBlock . number < blockFinalizedGnosis . number ) {
380392 veaEpochOutboxCheckClaimsRangeArray . splice ( index , 1 ) ;
381- challengeTxnHashes . get ( index ) ;
382393 index -- ;
383394 // the challenge is finalized, no further action needed
384395 console . log ( "challenge is finalized" ) ;
@@ -464,13 +475,146 @@ const watch = async () => {
464475 10
465476 ) ) as ContractTransaction ;
466477
467- challengeTxnHashes . set ( index , txnChallenge . hash ) ;
478+ challenges . set ( index , { challengeTnxHash : txnChallenge . hash } ) ;
468479 console . log (
469480 "challenging claim for epoch " + veaEpochOutboxCheck + " with txn hash " + txnChallenge . hash
470481 ) ;
471482 }
472483 }
473484 }
485+ if ( challenges . has ( index ) ) {
486+ const challengeProgress = challenges . get ( index ) ;
487+ const txnReceipt = ( await retryOperation (
488+ ( ) => providerGnosis . getTransactionReceipt ( challenges . get ( index ) . challengeTnxHash ) ,
489+ 10 ,
490+ 1000
491+ ) ) as TransactionReceipt ;
492+ if ( ! txnReceipt ) {
493+ console . log ( "challenge txn " + challenges . get ( index ) . challengeTnxHash + " not mined yet" ) ;
494+ continue ;
495+ }
496+ const blockNumber = txnReceipt . blockNumber ;
497+ const challengeBlock = ( await retryOperation (
498+ ( ) => providerGnosis . getBlock ( blockNumber ) ,
499+ 1000 ,
500+ 10
501+ ) ) as Block ;
502+ if ( challengeBlock . number < blockFinalizedGnosis . number ) {
503+ if ( ! challengeProgress . sentSnapshotTnxHash ) {
504+ console . log ( "Sending snapshot for challenged claim in epoch " + veaEpochOutboxCheck ) ;
505+ try {
506+ const sendSnapshotTx = await veaInbox . sendSnapshot (
507+ veaEpochOutboxCheck ,
508+ 30000000 , // gas limit, you might want to adjust this
509+ {
510+ stateRoot : claimSnapshot ,
511+ claimer : claim . claimer ,
512+ timestampClaimed : claim . timestampClaimed ,
513+ timestampVerification : claim . timestampVerification ,
514+ blocknumberVerification : claim . blocknumberVerification ,
515+ honest : claim . honest ,
516+ challenger : watcherAddress ,
517+ } ,
518+ { gasLimit : 30000000 }
519+ ) ;
520+ console . log ( "Sent snapshot with transaction hash: " + sendSnapshotTx . hash ) ;
521+ challenges . set ( index , {
522+ ...challengeProgress ,
523+ sentSnapshotTnxHash : sendSnapshotTx . hash ,
524+ } ) ;
525+ } catch ( error ) {
526+ console . error ( "Failed to send snapshot: " , error ) ;
527+ }
528+ } else {
529+ console . log ( "Snapshot already sent for challenge in epoch " + veaEpochOutboxCheck ) ;
530+ }
531+ }
532+ } else {
533+ let gasEstimate : BigNumber ;
534+ try {
535+ gasEstimate = ( await retryOperation (
536+ ( ) => veaOutbox . estimateGas . challenge ( veaEpochOutboxCheck , claim ) ,
537+ 1000 ,
538+ 10
539+ ) ) as BigNumber ;
540+ } catch ( e ) {
541+ console . log ( e ) ;
542+ console . log ( "Challenge failed to estimate gas, skipping." ) ;
543+ const logChallenges = ( await retryOperation (
544+ ( ) =>
545+ providerGnosis . getLogs ( {
546+ address : veaOutboxAddress ,
547+ topics : veaOutbox . filters . Challenged ( veaEpochOutboxCheck , null ) . topics ,
548+ fromBlock : blockNumberOutboxLowerBound ,
549+ toBlock : blockTagGnosis ,
550+ } ) ,
551+ 1000 ,
552+ 10
553+ ) ) as Log [ ] ;
554+
555+ // if already challenged, no action needed
556+
557+ // if not challenged, keep checking all claim struct variables
558+ if ( logChallenges . length == 0 ) {
559+ }
560+ }
561+ // deposit / 2 is the profit for challengers
562+ // the initial challenge txn is roughly 1/3 of the cost of completing the challenge process.
563+ const maxFeePerGasProfitable = deposit . div ( gasEstimate . mul ( 3 * 2 ) ) ;
564+
565+ // there's practically very little MEV on gnosis
566+ // priority fee should just be a small amount to get the txn included in a block
567+
568+ let maxPriorityFeePerGas = BigNumber . from ( "3000000000" ) ; // 3 gwei
569+
570+ // if claim is in min challenge period, we can use a higher priority fee
571+ // this ensures the txn is always competitive during the censorship test (min challenge period)
572+ if ( claim . timestampClaimed < timeLocal - sequencerDelayLimit - epochPeriod ) {
573+ try {
574+ const blockPendingGnosis = ( await retryOperation (
575+ ( ) => providerGnosis . getBlockWithTransactions ( "pending" ) ,
576+ 1000 ,
577+ 10
578+ ) ) as BlockWithTransactions ;
579+ // can't access actual gas used from pending block, consider all txns equal weight
580+ let maxPriorityFeePerGasAvg = BigNumber . from ( "0" ) ;
581+ for ( const txn of blockPendingGnosis . transactions ) {
582+ maxPriorityFeePerGasAvg = maxPriorityFeePerGasAvg . add ( txn . maxPriorityFeePerGas ) ;
583+ }
584+ maxPriorityFeePerGasAvg = maxPriorityFeePerGasAvg . div ( blockPendingGnosis . transactions . length ) ;
585+ if ( maxPriorityFeePerGas . lt ( maxPriorityFeePerGasAvg ) ) {
586+ maxPriorityFeePerGas = maxPriorityFeePerGasAvg ;
587+ }
588+ } catch ( e ) { }
589+
590+ // there's almost no MEV on gnosis
591+ // will update this default value if there is more MEV on gnosis in the future
592+ if ( maxPriorityFeePerGas . lt ( BigNumber . from ( "100000000000" ) ) ) {
593+ maxPriorityFeePerGas = BigNumber . from ( "100000000000" ) ; // 100 gwei
594+ }
595+
596+ if ( maxPriorityFeePerGas . gt ( maxFeePerGasProfitable ) ) {
597+ maxPriorityFeePerGas = maxFeePerGasProfitable ;
598+ }
599+ }
600+
601+ if ( ! inactive ) {
602+ const txnChallenge = ( await retryOperation (
603+ ( ) =>
604+ veaOutbox . challenge ( veaEpochOutboxCheck , claim , {
605+ maxFeePerGas : maxFeePerGasProfitable ,
606+ maxPriorityFeePerGas : maxPriorityFeePerGas ,
607+ } ) ,
608+ 1000 ,
609+ 10
610+ ) ) as ContractTransaction ;
611+
612+ challenges . set ( index , { challengeTnxHash : txnChallenge . hash } ) ;
613+ console . log (
614+ "challenging claim for epoch " + veaEpochOutboxCheck + " with txn hash " + txnChallenge . hash
615+ ) ;
616+ }
617+ }
474618 }
475619 } else {
476620 console . log ( "claim hash matches snapshot for epoch " + veaEpochOutboxCheck ) ;
@@ -522,17 +666,19 @@ const getBlocksAndCheckFinality = async (
522666 const blockFinalizedArb = ( await retryOperation ( ( ) => ArbProvider . getBlock ( "finalized" ) , 1000 , 10 ) ) as Block ;
523667 const blockFinalizedEth = ( await retryOperation ( ( ) => EthProvider . getBlock ( "finalized" ) , 1000 , 10 ) ) as Block ;
524668 const blockFinalizedGnosis = ( await retryOperation ( ( ) => GnosisProvider . getBlock ( "finalized" ) , 1000 , 10 ) ) as Block ;
525-
526669 const finalityBuffer = 300 ; // 5 minutes, allows for network delays
527670 const maxFinalityTimeSecondsEth = ( slotsPerEpochEth * 3 - 1 ) * secondsPerSlotEth ; // finalization after 2 justified epochs
528671 const maxFinalityTimeSecondsGnosis = ( slotsPerEpochGnosis * 3 - 1 ) * secondsPerSlotGnosis ; // finalization after 2 justified epochs
529672
530673 let finalityIssueFlagArb = false ;
531674 let finalityIssueFlagEth = false ;
532675 let finalityIssueFlagGnosis = false ;
533-
534676 // check latest arb block to see if there are any sequencer issues
535677 let blockLatestArb = ( await retryOperation ( ( ) => ArbProvider . getBlock ( "latest" ) , 1000 , 10 ) ) as Block ;
678+ let blockoldArb = ( await retryOperation ( ( ) => ArbProvider . getBlock ( blockLatestArb . number - 100 ) , 1000 , 10 ) ) as Block ;
679+ const arbAverageBlockTime = ( blockLatestArb . timestamp - blockoldArb . timestamp ) / 100 ;
680+ const maxDelayInSeconds = 7 * 24 * 60 * 60 ; // 7 days in seconds
681+ const fromBlockArbFinalized = blockFinalizedArb . number - Math . ceil ( maxDelayInSeconds / arbAverageBlockTime ) ;
536682
537683 // to performantly query the sequencerInbox's SequencerBatchDelivered event on Eth, we limit the block range
538684 // we use the heuristic that. delta blocknumber <= delta timestamp / secondsPerSlot
@@ -553,6 +699,7 @@ const getBlocksAndCheckFinality = async (
553699 sequencer ,
554700 blockFinalizedArb ,
555701 fromBlockEthFinalized ,
702+ fromBlockArbFinalized ,
556703 false
557704 ) ;
558705
@@ -570,6 +717,7 @@ const getBlocksAndCheckFinality = async (
570717 sequencer ,
571718 blockLatestArb ,
572719 fromBlockEthFinalized ,
720+ fromBlockArbFinalized ,
573721 true
574722 ) ;
575723
@@ -635,10 +783,10 @@ const ArbBlockToL1Block = async (
635783 sequencer : SequencerInbox ,
636784 L2Block : Block ,
637785 fromBlockEth : number ,
786+ fromArbBlock : number ,
638787 fallbackLatest : boolean
639788) : Promise < [ Block , number ] | undefined > => {
640789 const nodeInterface = NodeInterface__factory . connect ( NODE_INTERFACE_ADDRESS , L2Provider ) ;
641-
642790 let latestL2batchOnEth : number ;
643791 let latestL2BlockNumberOnEth : number ;
644792 let result = ( await nodeInterface . functions
@@ -651,16 +799,20 @@ const ArbBlockToL1Block = async (
651799 const errMsg = JSON . parse ( JSON . parse ( JSON . stringify ( e ) ) . error . body ) . error . message ;
652800
653801 if ( fallbackLatest ) {
654- latestL2batchOnEth = parseInt ( errMsg . split ( " published in batch " ) [ 1 ] ) ;
655- latestL2BlockNumberOnEth = parseInt ( errMsg . split ( " is after latest on-chain block " ) [ 1 ] ) ;
656- if ( Number . isNaN ( latestL2batchOnEth ) ) {
657- console . error ( errMsg ) ;
658- }
659802 }
660803 } ) ) as [ BigNumber ] & { batch : BigNumber } ;
661804
662- if ( ! result && ! fallbackLatest ) return undefined ;
663-
805+ if ( ! result ) {
806+ if ( ! fallbackLatest ) {
807+ return undefined ;
808+ } else {
809+ [ latestL2batchOnEth , latestL2BlockNumberOnEth ] = await findLatestL2BatchAndBlock (
810+ nodeInterface ,
811+ fromArbBlock ,
812+ L2Block . number
813+ ) ;
814+ }
815+ }
664816 const batch = result ?. batch ?. toNumber ( ) ?? latestL2batchOnEth ;
665817 const L2BlockNumberFallback = latestL2BlockNumberOnEth ?? L2Block . number ;
666818 /**
@@ -682,6 +834,29 @@ const ArbBlockToL1Block = async (
682834 return [ L1Block , L2BlockNumberFallback ] ;
683835} ;
684836
837+ const findLatestL2BatchAndBlock = async (
838+ nodeInterface : NodeInterface ,
839+ fromArbBlock : number ,
840+ latestBlockNumber : number
841+ ) : Promise < [ number , number ] > => {
842+ let low = fromArbBlock ;
843+ let high = latestBlockNumber ;
844+
845+ while ( low <= high ) {
846+ const mid = Math . floor ( ( low + high ) / 2 ) ;
847+ try {
848+ ( await nodeInterface . functions . findBatchContainingBlock ( mid , { blockTag : "latest" } ) ) as any ;
849+ low = mid + 1 ;
850+ } catch ( e ) {
851+ high = mid - 1 ;
852+ }
853+ }
854+ if ( high < low ) return [ undefined , undefined ] ;
855+ // high is now the latest L2 block number that has a corresponding batch on L1
856+ const result = ( await nodeInterface . functions . findBatchContainingBlock ( high , { blockTag : "latest" } ) ) as any ;
857+ return [ result . batch . toNumber ( ) , high ] ;
858+ } ;
859+
685860( async ( ) => {
686861 await watch ( ) ;
687862} ) ( ) ;
0 commit comments