Skip to content

Commit f11393d

Browse files
committed
feat(validtor-bot): function to find latest l2 batch on l1
1 parent 73fa3ff commit f11393d

File tree

5 files changed

+210
-28
lines changed

5 files changed

+210
-28
lines changed

contracts/deployments/arbitrumSepolia/VeaInboxArbToGnosisTestnet.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"address": "0x72c7d51647cBeaca636d0E20A66ca2F682da3539",
2+
"address": "0x854374483572FFcD4d0225290346279d0718240b",
33
"abi": [
44
{
55
"inputs": [

contracts/deployments/chiado/VeaOutboxArbToGnosisTestnet.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"address": "0xa3C6608539693C13434e4E29c9aB53Dd029178BE",
2+
"address": "0x2f1788F7B74e01c4C85578748290467A5f063B0b",
33
"abi": [
44
{
55
"inputs": [

relayer-cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@
2121
"typescript": "^4.9.5",
2222
"web3": "^1.10.4",
2323
"web3-batched-send": "^1.0.3"
24+
},
25+
"devDependencies": {
26+
"ts-node": "^10.9.2"
2427
}
2528
}

validator-cli/package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"start": "npx ts-node ./src/ArbToEth/watcher.ts",
1414
"start-chiado-devnet": "npx ts-node ./src/devnet/arbToChiado/happyPath.ts",
1515
"start-sepolia-devnet": "npx ts-node ./src/devnet/arbToSepolia/happyPath.ts",
16-
"start-sepolia-testnet": "npx ts-node ./src/ArbToEth/watcherArbToEth.ts"
16+
"start-sepolia-testnet": "npx ts-node ./src/ArbToEth/watcherArbToEth.ts",
17+
"start-chaido-testnet": "npx ts-node ./src/ArbToEth/watcherArbToGnosis.ts"
1718
},
1819
"dependencies": {
1920
"@arbitrum/sdk": "4.0.1",
@@ -25,5 +26,8 @@
2526
"typescript": "^4.9.5",
2627
"web3": "^1.10.4",
2728
"web3-batched-send": "^1.0.3"
29+
},
30+
"devDependencies": {
31+
"ts-node": "^10.9.2"
2832
}
2933
}

validator-cli/src/ArbToEth/watcherArbToGnosis.ts

Lines changed: 200 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ import {
55
getWalletRPC,
66
} from "../utils/ethers";
77
import { JsonRpcProvider } from "@ethersproject/providers";
8-
import { getL2Network } from "@arbitrum/sdk";
8+
import { getArbitrumNetwork } from "@arbitrum/sdk";
99
import { NODE_INTERFACE_ADDRESS } from "@arbitrum/sdk/dist/lib/dataEntities/constants";
1010
import { NodeInterface__factory } from "@arbitrum/sdk/dist/lib/abi/factories/NodeInterface__factory";
1111
import { SequencerInbox__factory } from "@arbitrum/sdk/dist/lib/abi/factories/SequencerInbox__factory";
1212
import { BigNumber, ContractTransaction, constants } from "ethers";
1313
import { Block, Log, TransactionReceipt, BlockWithTransactions } from "@ethersproject/abstract-provider";
1414
import { SequencerInbox } from "@arbitrum/sdk/dist/lib/abi/SequencerInbox";
15+
import { NodeInterface } from "@arbitrum/sdk/dist/lib/abi/NodeInterface";
1516

1617
require("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

Comments
 (0)