diff --git a/yarn-project/ethereum/src/l1_tx_utils.test.ts b/yarn-project/ethereum/src/l1_tx_utils.test.ts index 91d4c87e9a7..1927563052a 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.test.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.test.ts @@ -299,4 +299,22 @@ describe('GasUtils', () => { const expectedEstimate = baseEstimate + (baseEstimate * 20n) / 100n; expect(bufferedEstimate).toBe(expectedEstimate); }); + + it('stops trying after timeout', async () => { + await cheatCodes.setAutomine(false); + await cheatCodes.setIntervalMining(0); + + const now = Date.now(); + await expect( + gasUtils.sendAndMonitorTransaction( + { + to: '0x1234567890123456789012345678901234567890', + data: '0x', + value: 0n, + }, + { txTimeoutAt: new Date(now + 1000) }, + ), + ).rejects.toThrow(/timed out/); + expect(Date.now() - now).toBeGreaterThanOrEqual(990); + }, 60_000); }); diff --git a/yarn-project/ethereum/src/l1_tx_utils.ts b/yarn-project/ethereum/src/l1_tx_utils.ts index 607f06b9567..b88c4ce0deb 100644 --- a/yarn-project/ethereum/src/l1_tx_utils.ts +++ b/yarn-project/ethereum/src/l1_tx_utils.ts @@ -174,7 +174,7 @@ export class L1TxUtils { */ public async sendTransaction( request: L1TxRequest, - _gasConfig?: Partial & { fixedGas?: bigint }, + _gasConfig?: Partial & { fixedGas?: bigint; txTimeoutAt?: Date }, _blobInputs?: L1BlobInputs, ): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> { const gasConfig = { ...this.config, ..._gasConfig }; @@ -189,6 +189,10 @@ export class L1TxUtils { const gasPrice = await this.getGasPrice(gasConfig); + if (gasConfig.txTimeoutAt && Date.now() > gasConfig.txTimeoutAt.getTime()) { + throw new Error('Transaction timed out before sending'); + } + const blobInputs = _blobInputs || {}; const txHash = await this.walletClient.sendTransaction({ ...request, @@ -218,7 +222,7 @@ export class L1TxUtils { request: L1TxRequest, initialTxHash: Hex, params: { gasLimit: bigint }, - _gasConfig?: Partial, + _gasConfig?: Partial & { txTimeoutAt?: Date }, _blobInputs?: L1BlobInputs, ): Promise { const gasConfig = { ...this.config, ..._gasConfig }; @@ -246,7 +250,12 @@ export class L1TxUtils { let attempts = 0; let lastAttemptSent = Date.now(); const initialTxTime = lastAttemptSent; + let txTimedOut = false; + const isTimedOut = () => + (gasConfig.txTimeoutAt && Date.now() > gasConfig.txTimeoutAt.getTime()) || + (gasConfig.txTimeoutMs !== undefined && Date.now() - initialTxTime > gasConfig.txTimeoutMs) || + false; while (!txTimedOut) { try { @@ -284,11 +293,9 @@ export class L1TxUtils { this.logger?.debug(`L1 transaction ${currentTxHash} pending. Time passed: ${timePassed}ms.`); // Check timeout before continuing - if (gasConfig.txTimeoutMs) { - txTimedOut = Date.now() - initialTxTime > gasConfig.txTimeoutMs; - if (txTimedOut) { - break; - } + txTimedOut = isTimedOut(); + if (txTimedOut) { + break; } await sleep(gasConfig.checkIntervalMs!); @@ -331,9 +338,7 @@ export class L1TxUtils { await sleep(gasConfig.checkIntervalMs!); } // Check if tx has timed out. - if (gasConfig.txTimeoutMs) { - txTimedOut = Date.now() - initialTxTime > gasConfig.txTimeoutMs!; - } + txTimedOut = isTimedOut(); } throw new Error(`L1 transaction ${currentTxHash} timed out`); } @@ -346,7 +351,7 @@ export class L1TxUtils { */ public async sendAndMonitorTransaction( request: L1TxRequest, - gasConfig?: Partial & { fixedGas?: bigint }, + gasConfig?: Partial & { fixedGas?: bigint; txTimeoutAt?: Date }, blobInputs?: L1BlobInputs, ): Promise { const { txHash, gasLimit } = await this.sendTransaction(request, gasConfig, blobInputs); diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index e1c8d631f4e..9e19e25e40f 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -557,6 +557,7 @@ export class L1Publisher { attestations?: Signature[], txHashes?: TxHash[], proofQuote?: EpochProofQuote, + opts: { txTimeoutAt?: Date } = {}, ): Promise { const ctx = { blockNumber: block.number, @@ -598,8 +599,8 @@ export class L1Publisher { this.log.debug(`Submitting propose transaction`); const result = proofQuote - ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote) - : await this.sendProposeTx(proposeTxArgs); + ? await this.sendProposeAndClaimTx(proposeTxArgs, proofQuote, opts) + : await this.sendProposeTx(proposeTxArgs, opts); if (!result?.receipt) { this.log.info(`Failed to publish block ${block.number} to L1`, ctx); @@ -1016,6 +1017,7 @@ export class L1Publisher { private async sendProposeTx( encodedData: L1ProcessArgs, + opts: { txTimeoutAt?: Date } = {}, ): Promise<{ receipt: TransactionReceipt | undefined; args: any; functionName: string; data: Hex } | undefined> { if (this.interrupted) { return undefined; @@ -1035,6 +1037,7 @@ export class L1Publisher { }, { fixedGas: gas, + ...opts, }, { blobs: encodedData.blobs.map(b => b.dataWithZeros), @@ -1057,6 +1060,7 @@ export class L1Publisher { private async sendProposeAndClaimTx( encodedData: L1ProcessArgs, quote: EpochProofQuote, + opts: { txTimeoutAt?: Date } = {}, ): Promise<{ receipt: TransactionReceipt | undefined; args: any; functionName: string; data: Hex } | undefined> { if (this.interrupted) { return undefined; @@ -1074,7 +1078,10 @@ export class L1Publisher { to: this.rollupContract.address, data, }, - { fixedGas: gas }, + { + fixedGas: gas, + ...opts, + }, { blobs: encodedData.blobs.map(b => b.dataWithZeros), kzg, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts index c012fbefb2e..7a9c96e898e 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.test.ts @@ -130,6 +130,13 @@ describe('sequencer', () => { return tx; }; + const expectPublisherProposeL2Block = (txHashes: TxHash[], proofQuote?: EpochProofQuote) => { + expect(publisher.proposeL2Block).toHaveBeenCalledTimes(1); + expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), txHashes, proofQuote, { + txTimeoutAt: expect.any(Date), + }); + }; + beforeEach(() => { lastBlockNumber = 0; newBlockNumber = lastBlockNumber + 1; @@ -257,8 +264,7 @@ describe('sequencer', () => { Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.proposeL2Block).toHaveBeenCalledTimes(1); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined); + expectPublisherProposeL2Block([txHash]); }); it.each([ @@ -317,7 +323,7 @@ describe('sequencer', () => { globalVariables, Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined); + expectPublisherProposeL2Block([txHash]); }); it('builds a block out of several txs rejecting invalid txs', async () => { @@ -340,7 +346,7 @@ describe('sequencer', () => { globalVariables, Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), validTxHashes, undefined); + expectPublisherProposeL2Block(validTxHashes); expect(p2p.deleteTxs).toHaveBeenCalledWith([invalidTx.getTxHash()]); }); @@ -370,13 +376,8 @@ describe('sequencer', () => { globalVariables, times(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, Fr.zero), ); - expect(publisher.proposeL2Block).toHaveBeenCalledTimes(1); - expect(publisher.proposeL2Block).toHaveBeenCalledWith( - block, - getSignatures(), - neededTxs.map(tx => tx.getTxHash()), - undefined, - ); + + expectPublisherProposeL2Block(neededTxs.map(tx => tx.getTxHash())); }); it('builds a block that contains zero real transactions once flushed', async () => { @@ -409,8 +410,7 @@ describe('sequencer', () => { times(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP, Fr.zero), ); expect(blockBuilder.addTxs).toHaveBeenCalledWith([]); - expect(publisher.proposeL2Block).toHaveBeenCalledTimes(1); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [], undefined); + expectPublisherProposeL2Block([]); }); it('builds a block that contains less than the minimum number of transactions once flushed', async () => { @@ -443,9 +443,8 @@ describe('sequencer', () => { globalVariables, Array(NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP).fill(new Fr(0n)), ); - expect(publisher.proposeL2Block).toHaveBeenCalledTimes(1); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), postFlushTxHashes, undefined); + expectPublisherProposeL2Block(postFlushTxHashes); }); it('aborts building a block if the chain moves underneath it', async () => { @@ -533,7 +532,7 @@ describe('sequencer', () => { publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n)); await sequencer.doRealWork(); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], proofQuote); + expectPublisherProposeL2Block([txHash], proofQuote); }); it('submits a valid proof quote even without a block', async () => { @@ -568,7 +567,7 @@ describe('sequencer', () => { publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(undefined)); await sequencer.doRealWork(); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined); + expectPublisherProposeL2Block([txHash]); }); it('does not submit a quote with an expired slot number', async () => { @@ -585,7 +584,7 @@ describe('sequencer', () => { publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n)); await sequencer.doRealWork(); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined); + expectPublisherProposeL2Block([txHash]); }); it('does not submit a valid quote if unable to claim epoch', async () => { @@ -600,7 +599,7 @@ describe('sequencer', () => { publisher.getClaimableEpoch.mockResolvedValue(undefined); await sequencer.doRealWork(); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined); + expectPublisherProposeL2Block([txHash]); }); it('does not submit an invalid quote', async () => { @@ -619,7 +618,7 @@ describe('sequencer', () => { publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n)); await sequencer.doRealWork(); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], undefined); + expectPublisherProposeL2Block([txHash]); }); it('selects the lowest cost valid quote', async () => { @@ -652,7 +651,7 @@ describe('sequencer', () => { publisher.getClaimableEpoch.mockImplementation(() => Promise.resolve(currentEpoch - 1n)); await sequencer.doRealWork(); - expect(publisher.proposeL2Block).toHaveBeenCalledWith(block, getSignatures(), [txHash], validQuotes[0]); + expectPublisherProposeL2Block([txHash], validQuotes[0]); }); }); }); diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index a953e51e110..b855688b152 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -772,7 +772,13 @@ export class Sequencer { // Publishes new block to the network and awaits the tx to be mined this.setState(SequencerState.PUBLISHING_BLOCK, block.header.globalVariables.slotNumber.toBigInt()); - const publishedL2Block = await this.publisher.proposeL2Block(block, attestations, txHashes, proofQuote); + // Time out tx at the end of the slot + const slot = block.header.globalVariables.slotNumber.toNumber(); + const txTimeoutAt = new Date((this.getSlotStartTimestamp(slot) + this.aztecSlotDuration) * 1000); + + const publishedL2Block = await this.publisher.proposeL2Block(block, attestations, txHashes, proofQuote, { + txTimeoutAt, + }); if (!publishedL2Block) { throw new Error(`Failed to publish block ${block.number}`); }