Skip to content

Commit d46f452

Browse files
authored
chore: fix multi-eoa flake (#16616)
2 parents 0e9b2b0 + 493d43a commit d46f452

File tree

2 files changed

+41
-121
lines changed

2 files changed

+41
-121
lines changed

.test_patterns.yml

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,11 +262,6 @@ tests:
262262
owners:
263263
- *mitch
264264

265-
- regex: "src/e2e_multi_eoa.test.ts"
266-
error_regex: "TypeError: Cannot convert undefined to a BigInt"
267-
owners:
268-
- *alex
269-
270265
# Nightly GKE tests
271266
- regex: "spartan/bootstrap.sh"
272267
owners:

yarn-project/end-to-end/src/e2e_multi_eoa.test.ts

Lines changed: 41 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
import { ContractDeployer, EthAddress, Fr, type Logger, TxStatus, type Wallet } from '@aztec/aztec.js';
22
import { EthCheatCodes } from '@aztec/aztec/testing';
3-
import type { GasPrice, L1BlobInputs, L1GasConfig, L1TxRequest, PublisherManager, TxUtilsState } from '@aztec/ethereum';
3+
import type { PublisherManager, ViemClient } from '@aztec/ethereum';
44
import type { L1TxUtilsWithBlobs } from '@aztec/ethereum/l1-tx-utils-with-blobs';
55
import { times } from '@aztec/foundation/collection';
66
import { SecretValue } from '@aztec/foundation/config';
7+
import { randomBytes } from '@aztec/foundation/crypto';
78
import { StatefulTestContractArtifact } from '@aztec/noir-test-contracts.js/StatefulTest';
89
import type { SequencerClient } from '@aztec/sequencer-client';
910
import type { TestSequencerClient } from '@aztec/sequencer-client/test';
1011
import type { AztecNodeAdmin, PXE } from '@aztec/stdlib/interfaces/client';
1112

1213
import { jest } from '@jest/globals';
1314
import 'jest-extended';
14-
import type { Hex } from 'viem';
15+
import { type Hex, type TransactionSerialized, recoverTransactionAddress } from 'viem';
1516
import { mnemonicToAccount } from 'viem/accounts';
1617

1718
import { MNEMONIC } from './fixtures/fixtures.js';
@@ -92,19 +93,6 @@ describe('e2e_multi_eoa', () => {
9293

9394
afterAll(() => teardown());
9495

95-
const disableMining = async () => {
96-
await ethCheatCodes.setAutomine(false);
97-
await ethCheatCodes.setIntervalMining(0);
98-
logger.info('Disabled Mining');
99-
};
100-
101-
// Helper to re-enable Anvil mining
102-
const enableMining = async () => {
103-
await ethCheatCodes.setAutomine(true);
104-
await ethCheatCodes.evmMine();
105-
logger.info('Enabled Mining');
106-
};
107-
10896
// This executes a test of publisher account rotation.
10997
// We try and publish a block with the expected publisher account.
11098
// We intercept the transaction and delete it from Anvil.
@@ -123,121 +111,54 @@ describe('e2e_multi_eoa', () => {
123111

124112
const l1Utils: L1TxUtilsWithBlobs[] = (publisherManager as any).publishers;
125113

126-
// Intercept the required transactions
127-
let transactionHashToDrop: Hex | undefined;
128-
let transactionHashToKeep: Hex | undefined;
129-
let cancelTransactionHashToDrop: Hex | undefined;
130-
131-
const originalSendFunctions = l1Utils.map(l1Util => l1Util.sendTransaction.bind(l1Util));
132-
const originalCancelFunctions = l1Utils.map(l1Util => l1Util.attemptTxCancellation.bind(l1Util));
133-
134-
// For the expected 'first' publisher, swap out the send function with one that gets the tx hash and drops it in anvil
135-
const sendTxThatWeWillDrop = async (
136-
request: L1TxRequest,
137-
_gasConfig?: L1GasConfig,
138-
blobInputs?: L1BlobInputs,
139-
stateChange?: TxUtilsState,
140-
) => {
141-
await disableMining();
142-
const received = await originalSendFunctions[expectedFirstSender](request, _gasConfig, blobInputs, stateChange);
143-
transactionHashToDrop = received.txHash;
144-
logger.info(`Dropping tx: ${transactionHashToDrop} from Anvil`);
145-
await ethCheatCodes.dropTransaction(transactionHashToDrop);
146-
147-
try {
148-
await ethCheatCodes.publicClient.getTransaction({
149-
hash: transactionHashToDrop!,
150-
});
151-
logger.error(`Failed to drop transaction ${transactionHashToDrop} from Anvil!!`);
152-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
153-
} catch (_) {
154-
// Should always get here
155-
}
156-
157-
await enableMining();
158-
return received;
159-
};
160-
l1Utils[expectedFirstSender].sendTransaction = jest.fn(sendTxThatWeWillDrop);
161-
162-
// Also for the expected 'first' sender, drop any cancellations that may be sent
163-
const sendCancelTxThatWeWillDrop = async (
164-
currentTxHash: Hex,
165-
nonce: number,
166-
isBlobTx: boolean,
167-
previousGasPrice?: GasPrice,
168-
attempts?: number,
169-
) => {
170-
await disableMining();
171-
const received = await originalCancelFunctions[expectedFirstSender](
172-
currentTxHash,
173-
nonce,
174-
isBlobTx,
175-
previousGasPrice,
176-
attempts,
177-
);
178-
cancelTransactionHashToDrop = received;
179-
logger.info(`Dropping cancel tx: ${cancelTransactionHashToDrop} from Anvil`);
180-
await ethCheatCodes.dropTransaction(cancelTransactionHashToDrop);
181-
182-
try {
183-
await ethCheatCodes.publicClient.getTransaction({
184-
hash: transactionHashToDrop!,
185-
});
186-
logger.error(`Failed to drop transaction ${cancelTransactionHashToDrop} from Anvil!!`);
187-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
188-
} catch (_) {
189-
// Should always get here
190-
}
191-
192-
await enableMining();
193-
return received;
194-
};
195-
l1Utils[expectedFirstSender].attemptTxCancellation = jest.fn(sendCancelTxThatWeWillDrop);
196-
197-
// The 'second' sender should send the next block, we want this to succeed and we will verify against L1 later that
198-
// the expected publisher was used
199-
const sendTxSuccessfully = async (
200-
request: L1TxRequest,
201-
_gasConfig?: L1GasConfig,
202-
blobInputs?: L1BlobInputs,
203-
stateChange?: TxUtilsState,
204-
) => {
205-
const received = await originalSendFunctions[expectedSecondSender](
206-
request,
207-
_gasConfig,
208-
blobInputs,
209-
stateChange,
210-
);
211-
transactionHashToKeep = received.txHash;
212-
logger.info(`Tx that we expect to mine: ${transactionHashToKeep}`);
213-
return received;
214-
};
215-
l1Utils[expectedSecondSender].sendTransaction = jest.fn(sendTxSuccessfully);
114+
const blockedSender = l1Utils[expectedFirstSender].getSenderAddress();
115+
const blockedTxs: Hex[] = [];
116+
const fallbackSender = l1Utils[expectedSecondSender].getSenderAddress();
117+
const fallbackTxs: Hex[] = [];
118+
119+
// NOTE: we only need to spy on a single client because all l1Utils use the same ViemClient instance
120+
const originalSendRawTransaction = l1Utils[expectedFirstSender].client.sendRawTransaction;
121+
122+
// auto-dispose of this spy at the end of this function
123+
using _ = jest
124+
.spyOn(l1Utils[expectedFirstSender].client, 'sendRawTransaction')
125+
.mockImplementation(async function (this: ViemClient, arg) {
126+
const signerAddress = EthAddress.fromString(
127+
await recoverTransactionAddress({
128+
serializedTransaction: arg.serializedTransaction as TransactionSerialized<'eip1559' | 'eip4844'>,
129+
}),
130+
);
131+
132+
if (blockedSender.equals(signerAddress)) {
133+
const txHash = randomEthTxHash(); // block this sender/ Its txs don't actually reach any L1 nodes
134+
blockedTxs.push(txHash);
135+
return txHash;
136+
} else {
137+
const txHash = await originalSendRawTransaction.call(this, arg);
138+
if (fallbackSender.equals(signerAddress)) {
139+
fallbackTxs.push(txHash);
140+
}
141+
return txHash;
142+
}
143+
});
216144

217145
const tx = deployMethodTx.send();
218146
logger.info(`L2 Tx sent with hash: ${(await tx.getTxHash()).toString()} `);
219147

220148
const receipt = await tx.wait();
221149
expect(receipt.status).toBe(TxStatus.SUCCESS);
222150

223-
logger.info(`Checking sender of transaction with hash ${transactionHashToKeep}`);
151+
expect(blockedTxs.length).toBeGreaterThan(0);
152+
expect(fallbackTxs.length).toBeGreaterThan(0);
224153

154+
const transactionHashToKeep = fallbackTxs.at(-1)!;
225155
const l1Tx = await ethCheatCodes.publicClient.getTransaction({
226-
hash: transactionHashToKeep!,
156+
hash: transactionHashToKeep,
227157
});
228158
const senderEthAddress = EthAddress.fromString(l1Tx.from);
229159
const expectedSenderEthAddress = EthAddress.fromString(sequencerKeysAndAddresses[expectedSecondSender].address);
230160
const areSame = senderEthAddress.equals(expectedSenderEthAddress);
231161
expect(areSame).toBeTrue();
232-
233-
// Re-instate all modified functions
234-
for (let i = 0; i < l1Utils.length; i++) {
235-
l1Utils[i].sendTransaction = originalSendFunctions[i];
236-
l1Utils[i].attemptTxCancellation = originalCancelFunctions[i];
237-
}
238-
239-
// Ensure mining is switched on
240-
await enableMining();
241162
};
242163

243164
it('publishers are rotated by the sequencer', async () => {
@@ -287,3 +208,7 @@ describe('e2e_multi_eoa', () => {
287208
});
288209
});
289210
});
211+
212+
function randomEthTxHash(): Hex {
213+
return `0x${randomBytes(32).toString('hex')}`;
214+
}

0 commit comments

Comments
 (0)