Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions yarn-project/ethereum/src/test/tx_delayer.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Blob } from '@aztec/blob-lib';
import { type Logger, createLogger } from '@aztec/foundation/log';
import { retryUntil } from '@aztec/foundation/retry';
import { TestERC20Abi, TestERC20Bytecode } from '@aztec/l1-artifacts';

import type { Anvil } from '@viem/anvil';
import { type PrivateKeyAccount, createWalletClient, fallback, getContract, http, publicActions } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { foundry } from 'viem/chains';

import { EthCheatCodes } from '../eth_cheat_codes.js';
import type { ExtendedViemWalletClient } from '../types.js';
import { startAnvil } from './start_anvil.js';
import { type Delayer, withDelayer } from './tx_delayer.js';
Expand All @@ -17,11 +20,13 @@ describe('tx_delayer', () => {
let account: PrivateKeyAccount;
let client: ExtendedViemWalletClient;
let delayer: Delayer;
let cheatCodes: EthCheatCodes;

const ETHEREUM_SLOT_DURATION = 2;

beforeAll(async () => {
({ anvil, rpcUrl } = await startAnvil({ l1BlockTime: ETHEREUM_SLOT_DURATION }));
cheatCodes = new EthCheatCodes([rpcUrl]);
logger = createLogger('ethereum:test:tx_delayer');
});

Expand Down Expand Up @@ -96,6 +101,61 @@ describe('tx_delayer', () => {
expect(delayedTxReceipt.blockNumber).toEqual(blockNumber + 3n);
}, 20000);

it('cancels a tx and sends it later manually', async () => {
const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
delayer.cancelNextTx();
logger.info(`Cancelling next tx`);

const delayedTxHash = await client.sendTransaction({ to: account.address });
await expect(client.getTransactionReceipt({ hash: delayedTxHash })).rejects.toThrow(receiptNotFound);

logger.info(`Delayed tx sent. Waiting for one block to pass.`);
await retryUntil(() => client.getBlockNumber({ cacheTime: 0 }).then(b => b === blockNumber + 1n), 'block', 20, 0.1);
await expect(client.getTransactionReceipt({ hash: delayedTxHash })).rejects.toThrow(receiptNotFound);

logger.info(`Manually resending tx.`);
const [tx] = delayer.getCancelledTxs();
const txHash = await client.sendRawTransaction({ serializedTransaction: tx });
expect(txHash).toEqual(delayedTxHash);
await client.waitForTransactionReceipt({ hash: delayedTxHash });
}, 20000);

it('cancels a tx with blobs and sends it later manually', async () => {
const blockNumber = await client.getBlockNumber({ cacheTime: 0 });
const blobs = [new Uint8Array(131072).fill(1)];
const kzg = Blob.getViemKzgInstance();
const maxFeePerBlobGas = BigInt(1e10);
const nonce = await client.getTransactionCount({ address: account.address });
const txRequest = { to: account.address, blobs, kzg, maxFeePerBlobGas, nonce };

// We first disable mining and check the txHash as returned by anvil
logger.info(`Sending initial tx not to be mined`);
await cheatCodes.setIntervalMining(0);
const expectedTxHash = await client.sendTransaction(txRequest);
await cheatCodes.dropTransaction(expectedTxHash);
await cheatCodes.setIntervalMining(ETHEREUM_SLOT_DURATION);
await expect(client.getTransactionReceipt({ hash: expectedTxHash })).rejects.toThrow(receiptNotFound);

// And then try the delayer flow, checking we produced the correct txHash
logger.info(`Cancelling next tx`);
delayer.cancelNextTx();

const delayedTxHash = await client.sendTransaction(txRequest);
expect(delayedTxHash).toEqual(expectedTxHash);
await expect(client.getTransactionReceipt({ hash: delayedTxHash })).rejects.toThrow(receiptNotFound);

logger.info(`Delayed tx sent. Waiting for one block to pass.`);
await retryUntil(() => client.getBlockNumber({ cacheTime: 0 }).then(b => b === blockNumber + 1n), 'block', 20, 0.1);
await expect(client.getTransactionReceipt({ hash: delayedTxHash })).rejects.toThrow(receiptNotFound);

logger.info(`Manually resending tx`);
const [tx] = delayer.getCancelledTxs();
const txHash = await client.sendRawTransaction({ serializedTransaction: tx });
expect(txHash).toEqual(delayedTxHash);
const receipt = await client.waitForTransactionReceipt({ hash: delayedTxHash });
expect(receipt.blobGasUsed).toBeGreaterThan(0n);
}, 20000);

afterAll(async () => {
await anvil.stop().catch(err => createLogger('cleanup').error(err));
});
Expand Down
20 changes: 19 additions & 1 deletion yarn-project/ethereum/src/test/tx_delayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import {
type Client,
type Hex,
type PublicClient,
type TransactionSerializableEIP4844,
keccak256,
parseTransaction,
publicActions,
serializeTransaction,
walletActions,
} from 'viem';

Expand Down Expand Up @@ -130,7 +132,7 @@ export function withDelayer<T extends ViemClient>(

// Compute the tx hash manually so we emulate sendRawTransaction response
const { serializedTransaction } = args[0];
const txHash = keccak256(serializedTransaction);
const txHash = computeTxHash(serializedTransaction);

// Cancel tx outright if instructed
if ('indefinitely' in waitUntil && waitUntil.indefinitely) {
Expand Down Expand Up @@ -188,3 +190,19 @@ export function withDelayer<T extends ViemClient>(

return { client: extended, delayer };
}

/**
* Compute the tx hash given the serialized tx. Note that if this is a blob tx, we need to
* exclude the blobs, commitments, and proofs from the hash.
*/
function computeTxHash(serializedTransaction: Hex) {
if (serializedTransaction.startsWith('0x03')) {
const parsed = parseTransaction(serializedTransaction);
if (parsed.blobs || parsed.sidecars) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { blobs, sidecars, ...rest } = parsed;
return keccak256(serializeTransaction({ type: 'eip4844', ...rest } as TransactionSerializableEIP4844));
}
}
return keccak256(serializedTransaction);
}