Skip to content

Commit

Permalink
fix: Sequencer times out L1 tx at end of L2 slot (#11112)
Browse files Browse the repository at this point in the history
Sets the L1 tx utils timeout to the end of the L2 slot.
  • Loading branch information
spalladino authored Jan 9, 2025
1 parent 393e843 commit 1b88a34
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 36 deletions.
18 changes: 18 additions & 0 deletions yarn-project/ethereum/src/l1_tx_utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
27 changes: 16 additions & 11 deletions yarn-project/ethereum/src/l1_tx_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ export class L1TxUtils {
*/
public async sendTransaction(
request: L1TxRequest,
_gasConfig?: Partial<L1TxUtilsConfig> & { fixedGas?: bigint },
_gasConfig?: Partial<L1TxUtilsConfig> & { fixedGas?: bigint; txTimeoutAt?: Date },
_blobInputs?: L1BlobInputs,
): Promise<{ txHash: Hex; gasLimit: bigint; gasPrice: GasPrice }> {
const gasConfig = { ...this.config, ..._gasConfig };
Expand All @@ -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,
Expand Down Expand Up @@ -218,7 +222,7 @@ export class L1TxUtils {
request: L1TxRequest,
initialTxHash: Hex,
params: { gasLimit: bigint },
_gasConfig?: Partial<L1TxUtilsConfig>,
_gasConfig?: Partial<L1TxUtilsConfig> & { txTimeoutAt?: Date },
_blobInputs?: L1BlobInputs,
): Promise<TransactionReceipt> {
const gasConfig = { ...this.config, ..._gasConfig };
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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!);
Expand Down Expand Up @@ -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`);
}
Expand All @@ -346,7 +351,7 @@ export class L1TxUtils {
*/
public async sendAndMonitorTransaction(
request: L1TxRequest,
gasConfig?: Partial<L1TxUtilsConfig> & { fixedGas?: bigint },
gasConfig?: Partial<L1TxUtilsConfig> & { fixedGas?: bigint; txTimeoutAt?: Date },
blobInputs?: L1BlobInputs,
): Promise<TransactionReceipt> {
const { txHash, gasLimit } = await this.sendTransaction(request, gasConfig, blobInputs);
Expand Down
13 changes: 10 additions & 3 deletions yarn-project/sequencer-client/src/publisher/l1-publisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ export class L1Publisher {
attestations?: Signature[],
txHashes?: TxHash[],
proofQuote?: EpochProofQuote,
opts: { txTimeoutAt?: Date } = {},
): Promise<boolean> {
const ctx = {
blockNumber: block.number,
Expand Down Expand Up @@ -602,8 +603,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);
Expand Down Expand Up @@ -1020,6 +1021,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;
Expand All @@ -1039,6 +1041,7 @@ export class L1Publisher {
},
{
fixedGas: gas,
...opts,
},
{
blobs: encodedData.blobs.map(b => b.dataWithZeros),
Expand All @@ -1061,6 +1064,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;
Expand All @@ -1078,7 +1082,10 @@ export class L1Publisher {
to: this.rollupContract.address,
data,
},
{ fixedGas: gas },
{
fixedGas: gas,
...opts,
},
{
blobs: encodedData.blobs.map(b => b.dataWithZeros),
kzg,
Expand Down
41 changes: 20 additions & 21 deletions yarn-project/sequencer-client/src/sequencer/sequencer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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([
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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()]);
});

Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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]);
});
});
});
Expand Down
8 changes: 7 additions & 1 deletion yarn-project/sequencer-client/src/sequencer/sequencer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
Expand Down

0 comments on commit 1b88a34

Please sign in to comment.