Skip to content

Commit b73f7f9

Browse files
authored
chore: Avoid getport race conditions when starting anvil (#11077)
Our test suite uses `startAnvil` all over the place to start an anvil instance in the background. This was using `get-port`, which was subject to race conditions when checking for port availability if we ran multiple tests in parallel. This PR removes `get-port` and instead uses port zero to ask the OS to allocate a port for us, which should be free of race conditions (see [this SO answer](https://unix.stackexchange.com/a/55918)).
1 parent 649b590 commit b73f7f9

File tree

3 files changed

+26
-9
lines changed

3 files changed

+26
-9
lines changed

yarn-project/ethereum/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
"@aztec/l1-artifacts": "workspace:^",
3434
"@viem/anvil": "^0.0.10",
3535
"dotenv": "^16.0.3",
36-
"get-port": "^7.1.0",
3736
"tslib": "^2.4.0",
3837
"viem": "^2.7.15",
3938
"zod": "^3.23.8"

yarn-project/ethereum/src/test/start_anvil.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import { startAnvil } from './start_anvil.js';
55
describe('start_anvil', () => {
66
it('starts anvil on a free port', async () => {
77
const { anvil, rpcUrl } = await startAnvil();
8+
9+
const port = parseInt(new URL(rpcUrl).port);
10+
expect(port).toBeLessThan(65536);
11+
expect(port).toBeGreaterThan(1024);
12+
expect(anvil.port).toEqual(port);
13+
14+
const host = new URL(rpcUrl).hostname;
15+
expect(anvil.host).toEqual(host);
16+
817
const publicClient = createPublicClient({ transport: http(rpcUrl) });
918
const chainId = await publicClient.getChainId();
1019
expect(chainId).toEqual(31337);

yarn-project/ethereum/src/test/start_anvil.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,47 @@ import { makeBackoff, retry } from '@aztec/foundation/retry';
22
import { fileURLToPath } from '@aztec/foundation/url';
33

44
import { type Anvil, createAnvil } from '@viem/anvil';
5-
import getPort from 'get-port';
65
import { dirname, resolve } from 'path';
76

87
/**
98
* Ensures there's a running Anvil instance and returns the RPC URL.
109
*/
1110
export async function startAnvil(l1BlockTime?: number): Promise<{ anvil: Anvil; rpcUrl: string }> {
12-
let ethereumHostPort: number | undefined;
13-
1411
const anvilBinary = resolve(dirname(fileURLToPath(import.meta.url)), '../../', 'scripts/anvil_kill_wrapper.sh');
1512

13+
let port: number | undefined;
14+
1615
// Start anvil.
1716
// We go via a wrapper script to ensure if the parent dies, anvil dies.
1817
const anvil = await retry(
1918
async () => {
20-
ethereumHostPort = await getPort();
2119
const anvil = createAnvil({
2220
anvilBinary,
23-
port: ethereumHostPort,
21+
port: 0,
2422
blockTime: l1BlockTime,
23+
stopTimeout: 1000,
24+
});
25+
26+
// Listen to the anvil output to get the port.
27+
const removeHandler = anvil.on('message', (message: string) => {
28+
if (port === undefined && message.includes('Listening on')) {
29+
port = parseInt(message.match(/Listening on ([^:]+):(\d+)/)![2]);
30+
}
2531
});
2632
await anvil.start();
33+
removeHandler();
34+
2735
return anvil;
2836
},
2937
'Start anvil',
3038
makeBackoff([5, 5, 5]),
3139
);
3240

33-
if (!ethereumHostPort) {
41+
if (!port) {
3442
throw new Error('Failed to start anvil');
3543
}
3644

37-
const rpcUrl = `http://127.0.0.1:${ethereumHostPort}`;
38-
return { anvil, rpcUrl };
45+
// Monkeypatch the anvil instance to include the actually assigned port
46+
Object.defineProperty(anvil, 'port', { value: port, writable: false });
47+
return { anvil, rpcUrl: `http://127.0.0.1:${port}` };
3948
}

0 commit comments

Comments
 (0)