@aztec/ethereum 0.0.1-commit.3100065 → 0.0.1-commit.321f6a9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dest/config.d.ts +13 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +30 -3
- package/dest/contracts/multicall.d.ts +5 -3
- package/dest/contracts/multicall.d.ts.map +1 -1
- package/dest/contracts/multicall.js +7 -5
- package/dest/deploy_aztec_l1_contracts.d.ts +15 -4
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +24 -13
- package/dest/foundry_binary.d.ts +13 -0
- package/dest/foundry_binary.d.ts.map +1 -0
- package/dest/foundry_binary.js +52 -0
- package/dest/l1_tx_utils/l1_fee_analyzer.d.ts +23 -4
- package/dest/l1_tx_utils/l1_fee_analyzer.d.ts.map +1 -1
- package/dest/l1_tx_utils/l1_fee_analyzer.js +65 -3
- package/dest/l1_tx_utils/l1_tx_utils.d.ts +1 -1
- package/dest/l1_tx_utils/l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/l1_tx_utils.js +27 -7
- package/dest/l1_tx_utils/types.d.ts +27 -1
- package/dest/l1_tx_utils/types.d.ts.map +1 -1
- package/dest/l1_tx_utils/types.js +10 -0
- package/dest/queries.d.ts +1 -1
- package/dest/queries.d.ts.map +1 -1
- package/dest/queries.js +7 -1
- package/dest/test/eth_cheat_codes.d.ts +7 -1
- package/dest/test/eth_cheat_codes.d.ts.map +1 -1
- package/dest/test/eth_cheat_codes.js +29 -4
- package/dest/test/start_anvil.d.ts +1 -1
- package/dest/test/start_anvil.d.ts.map +1 -1
- package/dest/test/start_anvil.js +41 -5
- package/dest/utils.js +8 -11
- package/package.json +5 -6
- package/src/config.ts +38 -3
- package/src/contracts/multicall.ts +8 -5
- package/src/deploy_aztec_l1_contracts.ts +29 -12
- package/src/foundry_binary.ts +57 -0
- package/src/l1_tx_utils/l1_fee_analyzer.ts +75 -3
- package/src/l1_tx_utils/l1_tx_utils.ts +19 -2
- package/src/l1_tx_utils/types.ts +32 -0
- package/src/queries.ts +6 -0
- package/src/test/eth_cheat_codes.ts +23 -2
- package/src/test/start_anvil.ts +38 -6
- package/src/utils.ts +11 -11
- package/dest/generated/l1-contracts-defaults.d.ts +0 -30
- package/dest/generated/l1-contracts-defaults.d.ts.map +0 -1
- package/dest/generated/l1-contracts-defaults.js +0 -30
- package/src/generated/l1-contracts-defaults.ts +0 -32
package/src/config.ts
CHANGED
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
optionalNumberConfigHelper,
|
|
10
10
|
} from '@aztec/foundation/config';
|
|
11
11
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
12
|
+
import l1ContractsDefaultEnv from '@aztec/l1-artifacts/network-defaults.json' with { type: 'json' };
|
|
12
13
|
|
|
13
|
-
import { l1ContractsDefaultEnv } from './generated/l1-contracts-defaults.js';
|
|
14
14
|
import { type L1TxUtilsConfig, l1TxUtilsConfigMappings } from './l1_tx_utils/config.js';
|
|
15
15
|
|
|
16
16
|
export type GenesisStateConfig = {
|
|
@@ -81,11 +81,21 @@ export type L1ContractsConfig = {
|
|
|
81
81
|
initialEthPerFeeAsset: bigint;
|
|
82
82
|
/** The number of seconds to wait for an exit */
|
|
83
83
|
exitDelaySeconds: number;
|
|
84
|
+
/** Validator set size at or below which the entry queue uses the bootstrap flush size. */
|
|
85
|
+
entryQueueBootstrapValidatorSetSize: number;
|
|
86
|
+
/** Number of validators admitted from the entry queue per flush during the bootstrap phase. */
|
|
87
|
+
entryQueueBootstrapFlushSize: number;
|
|
88
|
+
/** Minimum number of validators admitted from the entry queue per flush. */
|
|
89
|
+
entryQueueFlushSizeMin: number;
|
|
90
|
+
/** Divisor applied to the queue size to derive the normal per-flush admission count. */
|
|
91
|
+
entryQueueFlushSizeQuotient: number;
|
|
92
|
+
/** Maximum number of validators admitted from the entry queue per flush. */
|
|
93
|
+
entryQueueMaxFlushSize: number;
|
|
84
94
|
} & L1TxUtilsConfig;
|
|
85
95
|
|
|
86
96
|
/**
|
|
87
97
|
* Config mappings for L1ContractsConfig.
|
|
88
|
-
* Default values come from
|
|
98
|
+
* Default values come from l1-contracts/scripts/network-defaults.json (published via @aztec/l1-artifacts).
|
|
89
99
|
* Real deployments use forge scripts which require explicit env vars (vm.envUint).
|
|
90
100
|
*/
|
|
91
101
|
export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> = {
|
|
@@ -237,12 +247,37 @@ export const l1ContractsConfigMappings: ConfigMappingsType<L1ContractsConfig> =
|
|
|
237
247
|
description: 'The delay before a validator can exit the set',
|
|
238
248
|
...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_EXIT_DELAY_SECONDS),
|
|
239
249
|
},
|
|
250
|
+
entryQueueBootstrapValidatorSetSize: {
|
|
251
|
+
env: 'AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE',
|
|
252
|
+
description: 'Validator set size at or below which the entry queue uses the bootstrap flush size.',
|
|
253
|
+
...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE),
|
|
254
|
+
},
|
|
255
|
+
entryQueueBootstrapFlushSize: {
|
|
256
|
+
env: 'AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE',
|
|
257
|
+
description: 'Number of validators admitted from the entry queue per flush during the bootstrap phase.',
|
|
258
|
+
...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE),
|
|
259
|
+
},
|
|
260
|
+
entryQueueFlushSizeMin: {
|
|
261
|
+
env: 'AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN',
|
|
262
|
+
description: 'Minimum number of validators admitted from the entry queue per flush.',
|
|
263
|
+
...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN),
|
|
264
|
+
},
|
|
265
|
+
entryQueueFlushSizeQuotient: {
|
|
266
|
+
env: 'AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT',
|
|
267
|
+
description: 'Divisor applied to the queue size to derive the normal per-flush admission count.',
|
|
268
|
+
...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT),
|
|
269
|
+
},
|
|
270
|
+
entryQueueMaxFlushSize: {
|
|
271
|
+
env: 'AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE',
|
|
272
|
+
description: 'Maximum number of validators admitted from the entry queue per flush.',
|
|
273
|
+
...numberConfigHelper(l1ContractsDefaultEnv.AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE),
|
|
274
|
+
},
|
|
240
275
|
...omitConfigMappings(l1TxUtilsConfigMappings, ['ethereumSlotDuration']),
|
|
241
276
|
};
|
|
242
277
|
|
|
243
278
|
/**
|
|
244
279
|
* Default L1 contracts configuration derived from l1ContractsConfigMappings.
|
|
245
|
-
* Source of truth:
|
|
280
|
+
* Source of truth: l1-contracts/scripts/network-defaults.json (published via @aztec/l1-artifacts).
|
|
246
281
|
*/
|
|
247
282
|
export const DefaultL1ContractsConfig = getDefaultConfig(l1ContractsConfigMappings);
|
|
248
283
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
multicall3Abi,
|
|
15
15
|
} from 'viem';
|
|
16
16
|
|
|
17
|
-
import type { L1BlobInputs, L1TxConfig, L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
|
|
17
|
+
import type { L1BlobInputs, L1TxConfig, L1TxRequest, L1TxState, L1TxUtils } from '../l1_tx_utils/index.js';
|
|
18
18
|
import type { ExtendedViemWalletClient } from '../types.js';
|
|
19
19
|
import { tryDecodeRevertReason } from '../utils.js';
|
|
20
20
|
|
|
@@ -26,7 +26,10 @@ export const MULTI_CALL_3_ADDRESS = '0xcA11bde05977b3631167028862bE2a173976CA11'
|
|
|
26
26
|
* treat it as a fatal on-chain failure rather than retrying on a different publisher.
|
|
27
27
|
*/
|
|
28
28
|
export class MulticallForwarderRevertedError extends Error {
|
|
29
|
-
constructor(
|
|
29
|
+
constructor(
|
|
30
|
+
public readonly receipt: TransactionReceipt,
|
|
31
|
+
public readonly txState?: L1TxState,
|
|
32
|
+
) {
|
|
30
33
|
super(`Multicall3 forwarder tx reverted: ${receipt.transactionHash}`);
|
|
31
34
|
this.name = 'MulticallForwarderRevertedError';
|
|
32
35
|
}
|
|
@@ -209,7 +212,7 @@ export class Multicall3 {
|
|
|
209
212
|
args: [args],
|
|
210
213
|
});
|
|
211
214
|
|
|
212
|
-
const { receipt } = await l1TxUtils.sendAndMonitorTransaction(
|
|
215
|
+
const { receipt, state } = await l1TxUtils.sendAndMonitorTransaction(
|
|
213
216
|
{
|
|
214
217
|
to: MULTI_CALL_3_ADDRESS,
|
|
215
218
|
data: encodedForwarderData,
|
|
@@ -223,11 +226,11 @@ export class Multicall3 {
|
|
|
223
226
|
// allowFailure to true for all calls, so a reverted status here would indicate a problem with
|
|
224
227
|
// the Multicall3 contract itself or the forwarder transaction (such as an out-of-gas).
|
|
225
228
|
if (receipt.status !== 'success') {
|
|
226
|
-
throw new MulticallForwarderRevertedError(receipt);
|
|
229
|
+
throw new MulticallForwarderRevertedError(receipt, state);
|
|
227
230
|
}
|
|
228
231
|
|
|
229
232
|
const stats = await l1TxUtils.getTransactionStats(receipt.transactionHash);
|
|
230
|
-
return { receipt, stats, multicallData: encodedForwarderData };
|
|
233
|
+
return { receipt, stats, multicallData: encodedForwarderData, state };
|
|
231
234
|
}
|
|
232
235
|
|
|
233
236
|
/** Batch multiple value transfers into a single aggregate3Value call on Multicall3. */
|
|
@@ -5,12 +5,12 @@ import { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
|
5
5
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
6
|
import { promiseWithResolvers } from '@aztec/foundation/promise';
|
|
7
7
|
import type { Fr } from '@aztec/foundation/schemas';
|
|
8
|
-
import { fileURLToPath } from '@aztec/foundation/url';
|
|
9
8
|
|
|
10
9
|
import { bn254 } from '@noble/curves/bn254';
|
|
11
10
|
import type { Abi, Narrow } from 'abitype';
|
|
12
11
|
import { spawn } from 'child_process';
|
|
13
12
|
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
14
|
import { tmpdir } from 'os';
|
|
15
15
|
import { dirname, join, resolve } from 'path';
|
|
16
16
|
import readline from 'readline';
|
|
@@ -22,11 +22,14 @@ import { createExtendedL1Client } from './client.js';
|
|
|
22
22
|
import { type L1ContractsConfig, assertValidSlotDurations } from './config.js';
|
|
23
23
|
import { deployMulticall3 } from './contracts/multicall.js';
|
|
24
24
|
import { RollupContract } from './contracts/rollup.js';
|
|
25
|
+
import { resolveFoundryBinary } from './foundry_binary.js';
|
|
25
26
|
import type { L1ContractAddresses } from './l1_contract_addresses.js';
|
|
26
27
|
import type { ExtendedViemWalletClient } from './types.js';
|
|
27
28
|
|
|
28
29
|
const logger = createLogger('ethereum:deploy_aztec_l1_contracts');
|
|
29
30
|
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
|
|
30
33
|
const JSON_DEPLOY_RESULT_PREFIX = 'JSON DEPLOY RESULT:';
|
|
31
34
|
|
|
32
35
|
/**
|
|
@@ -93,11 +96,16 @@ function runProcess<T>(
|
|
|
93
96
|
|
|
94
97
|
// Covers an edge where where we may have a cached BlobLib that is not meant for production.
|
|
95
98
|
// Despite the profile apparently sometimes cached code remains (so says Lasse after his ignition-monorepo arc).
|
|
96
|
-
async function maybeForgeForceProductionBuild(
|
|
99
|
+
async function maybeForgeForceProductionBuild(
|
|
100
|
+
forgeBin: string,
|
|
101
|
+
l1ContractsPath: string,
|
|
102
|
+
script: string,
|
|
103
|
+
chainId: number,
|
|
104
|
+
) {
|
|
97
105
|
if (chainId === mainnet.id) {
|
|
98
106
|
logger.info(`Recompiling ${script} with production profile for mainnet deployment`);
|
|
99
107
|
logger.info('This may take a minute but ensures production BlobLib is used.');
|
|
100
|
-
await runProcess(
|
|
108
|
+
await runProcess(forgeBin, ['build', script, '--force'], { FOUNDRY_PROFILE: 'production' }, l1ContractsPath);
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
|
|
@@ -123,15 +131,13 @@ export interface ValidatorJson {
|
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
/**
|
|
126
|
-
* Gets the path to the l1-contracts foundry artifacts directory.
|
|
127
|
-
*
|
|
128
|
-
*
|
|
134
|
+
* Gets the path to the l1-contracts foundry artifacts directory bundled inside @aztec/l1-artifacts.
|
|
135
|
+
* Resolved through the package (its "." export -> dest/index.js) so it works whether the package is
|
|
136
|
+
* linked via portal (monorepo) or installed under node_modules (published npm) — resolution follows
|
|
137
|
+
* the symlink in the portal case. The bundled foundry subtree sits alongside dest/, at <pkg>/l1-contracts.
|
|
129
138
|
*/
|
|
130
139
|
export function getL1ContractsPath(): string {
|
|
131
|
-
|
|
132
|
-
// Go up from yarn-project/ethereum/dest to yarn-project, then to l1-artifacts/l1-contracts
|
|
133
|
-
const l1ContractsPath = resolve(currentDir, '..', '..', 'l1-artifacts', 'l1-contracts');
|
|
134
|
-
return l1ContractsPath;
|
|
140
|
+
return resolve(dirname(require.resolve('@aztec/l1-artifacts')), '..', 'l1-contracts');
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
// Cached deployment directory
|
|
@@ -320,8 +326,9 @@ export async function deployAztecL1Contracts(
|
|
|
320
326
|
// Use foundry-artifacts from l1-artifacts package
|
|
321
327
|
const l1ContractsPath = prepareL1ContractsForDeployment();
|
|
322
328
|
|
|
329
|
+
const forgeBin = resolveFoundryBinary('forge');
|
|
323
330
|
const FORGE_SCRIPT = 'script/deploy/DeployAztecL1Contracts.s.sol';
|
|
324
|
-
await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
|
|
331
|
+
await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
|
|
325
332
|
|
|
326
333
|
// Verify contracts on Etherscan when on mainnet/sepolia and ETHERSCAN_API_KEY is available.
|
|
327
334
|
const isVerifiableChain = chainId === mainnet.id || chainId === sepolia.id;
|
|
@@ -346,6 +353,8 @@ export async function deployAztecL1Contracts(
|
|
|
346
353
|
...(shouldVerify ? ['--verify'] : []),
|
|
347
354
|
];
|
|
348
355
|
const forgeEnv = {
|
|
356
|
+
// Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
|
|
357
|
+
FORGE_BIN: forgeBin,
|
|
349
358
|
// Env vars required by l1-contracts/script/deploy/DeploymentConfiguration.sol.
|
|
350
359
|
NETWORK: getActiveNetworkName(),
|
|
351
360
|
FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
|
|
@@ -590,6 +599,11 @@ export function getDeployRollupForUpgradeEnvVars(
|
|
|
590
599
|
AZTEC_SLASH_AMOUNT_SMALL: args.slashAmountSmall.toString(),
|
|
591
600
|
AZTEC_SLASH_AMOUNT_MEDIUM: args.slashAmountMedium.toString(),
|
|
592
601
|
AZTEC_SLASH_AMOUNT_LARGE: args.slashAmountLarge.toString(),
|
|
602
|
+
AZTEC_ENTRY_QUEUE_BOOTSTRAP_VALIDATOR_SET_SIZE: args.entryQueueBootstrapValidatorSetSize.toString(),
|
|
603
|
+
AZTEC_ENTRY_QUEUE_BOOTSTRAP_FLUSH_SIZE: args.entryQueueBootstrapFlushSize.toString(),
|
|
604
|
+
AZTEC_ENTRY_QUEUE_FLUSH_SIZE_MIN: args.entryQueueFlushSizeMin.toString(),
|
|
605
|
+
AZTEC_ENTRY_QUEUE_FLUSH_SIZE_QUOTIENT: args.entryQueueFlushSizeQuotient.toString(),
|
|
606
|
+
AZTEC_ENTRY_QUEUE_MAX_FLUSH_SIZE: args.entryQueueMaxFlushSize.toString(),
|
|
593
607
|
} as const;
|
|
594
608
|
}
|
|
595
609
|
|
|
@@ -613,12 +627,15 @@ export const deployRollupForUpgrade = async (
|
|
|
613
627
|
// Use foundry-artifacts from l1-artifacts package
|
|
614
628
|
const l1ContractsPath = prepareL1ContractsForDeployment();
|
|
615
629
|
|
|
630
|
+
const forgeBin = resolveFoundryBinary('forge');
|
|
616
631
|
const FORGE_SCRIPT = 'script/deploy/DeployRollupForUpgrade.s.sol';
|
|
617
|
-
await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
|
|
632
|
+
await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
|
|
618
633
|
|
|
619
634
|
const scriptPath = join(getL1ContractsPath(), 'scripts', 'forge_broadcast.js');
|
|
620
635
|
const forgeArgs = [FORGE_SCRIPT, '--sig', 'run()', '--private-key', privateKey, '--rpc-url', rpcUrl];
|
|
621
636
|
const forgeEnv = {
|
|
637
|
+
// Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
|
|
638
|
+
FORGE_BIN: forgeBin,
|
|
622
639
|
FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
|
|
623
640
|
// Env vars required by l1-contracts/script/deploy/RollupConfiguration.sol.
|
|
624
641
|
REGISTRY_ADDRESS: registryAddress.toString(),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { spawnSync } from 'child_process';
|
|
2
|
+
import { accessSync, constants } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
|
|
6
|
+
function isExecutable(path: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
accessSync(path, constants.X_OK);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Locate a Foundry binary (`anvil`, `forge`, ...) without relying on the caller's PATH. Order:
|
|
17
|
+
* 1. `$<NAME>_BIN` (e.g. `$ANVIL_BIN`, `$FORGE_BIN`) — explicit override, e.g. for CI with a pinned
|
|
18
|
+
* version. Throws if set but not pointing at an executable, instead of silently falling back.
|
|
19
|
+
* 2. `~/.aztec/current/internal-bin/<name>` — where aztec-up installs it.
|
|
20
|
+
* 3. `~/.aztec/current/bin/aztec-<name>` — the publicly-exposed symlink.
|
|
21
|
+
* 4. `~/.foundry/bin/<name>` — standalone foundryup install.
|
|
22
|
+
* 5. `command -v <name>` — anything else on PATH.
|
|
23
|
+
*
|
|
24
|
+
* Throws with a directive message if none work.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveFoundryBinary(name: string): string {
|
|
27
|
+
const envVar = `${name.toUpperCase()}_BIN`;
|
|
28
|
+
const envBin = process.env[envVar];
|
|
29
|
+
if (envBin) {
|
|
30
|
+
if (!isExecutable(envBin)) {
|
|
31
|
+
throw new Error(`$${envVar} is set to ${envBin}, which does not exist or is not executable.`);
|
|
32
|
+
}
|
|
33
|
+
return envBin;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const candidates = [
|
|
37
|
+
join(homedir(), '.aztec', 'current', 'internal-bin', name),
|
|
38
|
+
join(homedir(), '.aztec', 'current', 'bin', `aztec-${name}`),
|
|
39
|
+
join(homedir(), '.foundry', 'bin', name),
|
|
40
|
+
];
|
|
41
|
+
for (const path of candidates) {
|
|
42
|
+
if (isExecutable(path)) {
|
|
43
|
+
return path;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const which = spawnSync('sh', ['-c', `command -v ${name}`], { encoding: 'utf8' });
|
|
48
|
+
if (which.status === 0 && which.stdout.trim()) {
|
|
49
|
+
return which.stdout.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(
|
|
53
|
+
`${name} binary not found. Tried $${envVar}, ~/.aztec/current/internal-bin/${name}, ` +
|
|
54
|
+
`~/.aztec/current/bin/aztec-${name}, ~/.foundry/bin/${name}, and $PATH. ` +
|
|
55
|
+
`Install via \`aztec-up\` or set ${envVar} to a working binary.`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -791,9 +791,6 @@ export class L1FeeAnalyzer {
|
|
|
791
791
|
});
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
-
/**
|
|
795
|
-
* Gets the minimum value from an array of bigints
|
|
796
|
-
*/
|
|
797
794
|
private minBigInt(values: bigint[]): bigint {
|
|
798
795
|
if (values.length === 0) {
|
|
799
796
|
return 0n;
|
|
@@ -801,3 +798,78 @@ export class L1FeeAnalyzer {
|
|
|
801
798
|
return values.reduce((min, val) => (val < min ? val : min), values[0]);
|
|
802
799
|
}
|
|
803
800
|
}
|
|
801
|
+
|
|
802
|
+
/** Per-block fee data for one mined L1 block, used to diagnose whether a tx was underpriced for it. */
|
|
803
|
+
export interface WindowBlockFees {
|
|
804
|
+
blockNumber: bigint;
|
|
805
|
+
timestamp: bigint;
|
|
806
|
+
baseFeePerGas: bigint;
|
|
807
|
+
/** 75th percentile priority fee among the block's txs (gas-weighted, from eth_feeHistory). */
|
|
808
|
+
p75PriorityFee: bigint;
|
|
809
|
+
/** Minimum priority fee among all included txs — the inclusion bar for that block. */
|
|
810
|
+
minIncludedPriorityFee: bigint;
|
|
811
|
+
blockBlobsFull: boolean;
|
|
812
|
+
includedBlobCount: number;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Safety bound on how far back the window scan walks if the chain head is far ahead of the window. */
|
|
816
|
+
const MAX_WINDOW_SCAN_BLOCKS = 64;
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Reads the already-mined L1 blocks whose timestamps fall in [windowStartS, windowEndS) — the L1
|
|
820
|
+
* inclusion window of an L2 slot — and extracts per-block fee data. Walks block headers to map the
|
|
821
|
+
* window to block numbers, then reads the priority-fee stats from a single eth_feeHistory call
|
|
822
|
+
* (percentile 0 is the cheapest included tx, i.e. the inclusion bar), so no transaction bodies are
|
|
823
|
+
* ever downloaded. Historical reads only, so it never waits on a future block. Returns the blocks in
|
|
824
|
+
* chronological order, [] on error, or the subset mined so far if the window is still in progress.
|
|
825
|
+
* Never throws.
|
|
826
|
+
*/
|
|
827
|
+
export async function captureWindowBlockFees(
|
|
828
|
+
client: ViemClient,
|
|
829
|
+
windowStartS: bigint,
|
|
830
|
+
windowEndS: bigint,
|
|
831
|
+
): Promise<WindowBlockFees[]> {
|
|
832
|
+
try {
|
|
833
|
+
let current = await client.getBlock({ blockTag: 'latest' });
|
|
834
|
+
const inWindow: (typeof current)[] = [];
|
|
835
|
+
let scanned = 0;
|
|
836
|
+
// Walk back from the head, collecting blocks inside the window and skipping any past its end,
|
|
837
|
+
// until we cross below the window start (or hit the safety cap / genesis).
|
|
838
|
+
while (current.timestamp >= windowStartS && scanned < MAX_WINDOW_SCAN_BLOCKS) {
|
|
839
|
+
if (current.timestamp < windowEndS) {
|
|
840
|
+
inWindow.push(current);
|
|
841
|
+
}
|
|
842
|
+
if (current.number === 0n) {
|
|
843
|
+
break;
|
|
844
|
+
}
|
|
845
|
+
current = await client.getBlock({ blockNumber: current.number - 1n });
|
|
846
|
+
scanned++;
|
|
847
|
+
}
|
|
848
|
+
if (inWindow.length === 0) {
|
|
849
|
+
return [];
|
|
850
|
+
}
|
|
851
|
+
inWindow.reverse();
|
|
852
|
+
const newest = inWindow[inWindow.length - 1];
|
|
853
|
+
const feeHistory = await client.getFeeHistory({
|
|
854
|
+
blockCount: Number(newest.number - inWindow[0].number) + 1,
|
|
855
|
+
blockNumber: newest.number,
|
|
856
|
+
rewardPercentiles: [0, 75],
|
|
857
|
+
});
|
|
858
|
+
return inWindow.map(block => {
|
|
859
|
+
const idx = Number(block.number - feeHistory.oldestBlock);
|
|
860
|
+
const [minIncludedPriorityFee, p75PriorityFee] = feeHistory.reward?.[idx] ?? [0n, 0n];
|
|
861
|
+
const blobsInBlock = block.blobGasUsed ? Number(block.blobGasUsed / GAS_PER_BLOB) : 0;
|
|
862
|
+
return {
|
|
863
|
+
blockNumber: block.number,
|
|
864
|
+
timestamp: block.timestamp,
|
|
865
|
+
baseFeePerGas: block.baseFeePerGas ?? 0n,
|
|
866
|
+
p75PriorityFee,
|
|
867
|
+
minIncludedPriorityFee,
|
|
868
|
+
blockBlobsFull: blobsInBlock >= getMaxBlobCapacity(block.timestamp),
|
|
869
|
+
includedBlobCount: blobsInBlock,
|
|
870
|
+
};
|
|
871
|
+
});
|
|
872
|
+
} catch {
|
|
873
|
+
return [];
|
|
874
|
+
}
|
|
875
|
+
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type L1TxConfig,
|
|
37
37
|
type L1TxRequest,
|
|
38
38
|
type L1TxState,
|
|
39
|
+
L1TxTimeoutError,
|
|
39
40
|
type SigningCallback,
|
|
40
41
|
TerminalTxUtilsState,
|
|
41
42
|
TxUtilsState,
|
|
@@ -306,6 +307,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
306
307
|
txConfigOverrides: gasConfigOverrides ?? {},
|
|
307
308
|
sentAtL1Ts: now,
|
|
308
309
|
lastSentAtL1Ts: now,
|
|
310
|
+
gasPriceHistory: [baseState.gasPrice],
|
|
309
311
|
};
|
|
310
312
|
|
|
311
313
|
// And persist it
|
|
@@ -504,6 +506,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
504
506
|
if (timePassed >= stallTimeMs && attempts <= maxSpeedUpAttempts) {
|
|
505
507
|
const newGasPrice = await this.getGasPrice(gasConfig, isBlobTx, attempts, state.gasPrice);
|
|
506
508
|
state.gasPrice = newGasPrice;
|
|
509
|
+
state.gasPriceHistory?.push(newGasPrice);
|
|
507
510
|
|
|
508
511
|
this.logger.debug(
|
|
509
512
|
`Tx ${currentTxHash} with nonce ${nonce} from ${account} appears stuck. ` +
|
|
@@ -672,8 +675,22 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
672
675
|
blobInputs?: L1BlobInputs,
|
|
673
676
|
): Promise<{ receipt: TransactionReceipt; state: L1TxState }> {
|
|
674
677
|
const { state } = await this.sendTransaction(request, gasConfig, blobInputs);
|
|
675
|
-
|
|
676
|
-
|
|
678
|
+
try {
|
|
679
|
+
const receipt = await this.monitorTransaction(state);
|
|
680
|
+
return { receipt, state };
|
|
681
|
+
} catch (err) {
|
|
682
|
+
if (err instanceof TimeoutError) {
|
|
683
|
+
// Snapshot the ladder now: the fire-and-forget cancellation mutates state.gasPrice moments later.
|
|
684
|
+
throw new L1TxTimeoutError(err.message, {
|
|
685
|
+
gasPriceHistory: state.gasPriceHistory ? [...state.gasPriceHistory] : undefined,
|
|
686
|
+
finalGasPrice: state.gasPrice,
|
|
687
|
+
attempts: state.txHashes.length,
|
|
688
|
+
nonce: state.nonce,
|
|
689
|
+
gasLimit: state.gasLimit,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
throw err;
|
|
693
|
+
}
|
|
677
694
|
}
|
|
678
695
|
|
|
679
696
|
public override async simulate(
|
package/src/l1_tx_utils/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlobKzgInstance } from '@aztec/blob-lib/types';
|
|
2
|
+
import { TimeoutError } from '@aztec/foundation/error';
|
|
2
3
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
4
|
import type { ViemTransactionSignature } from '@aztec/foundation/eth-signature';
|
|
4
5
|
|
|
@@ -55,6 +56,12 @@ export type L1TxState = {
|
|
|
55
56
|
cancelTxHashes: Hex[];
|
|
56
57
|
gasLimit: bigint;
|
|
57
58
|
gasPrice: GasPrice;
|
|
59
|
+
/**
|
|
60
|
+
* Prices used for each attempt (initial send followed by each speed-up), in order. Always set on
|
|
61
|
+
* newly sent txs; optional because states restored from the state store predate the field.
|
|
62
|
+
* In-memory only — not persisted by the state store.
|
|
63
|
+
*/
|
|
64
|
+
gasPriceHistory?: GasPrice[];
|
|
58
65
|
txConfigOverrides: L1TxConfig;
|
|
59
66
|
request: L1TxRequest;
|
|
60
67
|
status: TxUtilsState;
|
|
@@ -83,3 +90,28 @@ export class DroppedTransactionError extends Error {
|
|
|
83
90
|
this.name = 'DroppedTransactionError';
|
|
84
91
|
}
|
|
85
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Snapshot of what a timed-out L1 tx tried to pay, taken when the timeout is raised. */
|
|
95
|
+
export type TimedOutTxState = {
|
|
96
|
+
/** Prices used across the initial send and each speed-up, in order (undefined only for restored states). */
|
|
97
|
+
gasPriceHistory?: GasPrice[];
|
|
98
|
+
/** The last price the tx was sent at before timing out. */
|
|
99
|
+
finalGasPrice: GasPrice;
|
|
100
|
+
/** Number of send attempts (initial + speed-ups). */
|
|
101
|
+
attempts: number;
|
|
102
|
+
nonce: number;
|
|
103
|
+
gasLimit: bigint;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Thrown by sendAndMonitorTransaction when a tx times out. Subclasses TimeoutError so existing
|
|
108
|
+
* `instanceof TimeoutError` checks keep working, while carrying the gas-price ladder for diagnostics.
|
|
109
|
+
*/
|
|
110
|
+
export class L1TxTimeoutError extends TimeoutError {
|
|
111
|
+
constructor(
|
|
112
|
+
message: string,
|
|
113
|
+
public readonly txState: TimedOutTxState,
|
|
114
|
+
) {
|
|
115
|
+
super(message);
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/queries.ts
CHANGED
|
@@ -186,5 +186,11 @@ export async function getL1ContractsConfig(
|
|
|
186
186
|
slashAmountMedium: slashingAmounts[1],
|
|
187
187
|
slashAmountLarge: slashingAmounts[2],
|
|
188
188
|
initialEthPerFeeAsset: DefaultL1ContractsConfig.initialEthPerFeeAsset,
|
|
189
|
+
// Not exposed by the rollup contract; fall back to defaults like the other non-on-chain fields above.
|
|
190
|
+
entryQueueBootstrapValidatorSetSize: DefaultL1ContractsConfig.entryQueueBootstrapValidatorSetSize,
|
|
191
|
+
entryQueueBootstrapFlushSize: DefaultL1ContractsConfig.entryQueueBootstrapFlushSize,
|
|
192
|
+
entryQueueFlushSizeMin: DefaultL1ContractsConfig.entryQueueFlushSizeMin,
|
|
193
|
+
entryQueueFlushSizeQuotient: DefaultL1ContractsConfig.entryQueueFlushSizeQuotient,
|
|
194
|
+
entryQueueMaxFlushSize: DefaultL1ContractsConfig.entryQueueMaxFlushSize,
|
|
189
195
|
};
|
|
190
196
|
}
|
|
@@ -505,19 +505,40 @@ export class EthCheatCodes {
|
|
|
505
505
|
* reorg is needed because anvil applies a new gas limit only to future blocks: without it the just-mined
|
|
506
506
|
* blocks would keep the tiny gas limit, and an `eth_call` against `latest` (whose gas is capped by the
|
|
507
507
|
* block gas limit) would revert with "intrinsic gas too high".
|
|
508
|
+
*
|
|
509
|
+
* The replacement blocks advance L1 time. `anvil_reorg` stamps each replacement as `parent + N*interval`,
|
|
510
|
+
* so on its own it would freeze L1 time at the parent timestamp; callers that measure elapsed L1 time
|
|
511
|
+
* (L1TxUtils stall/timeout detection, `syncDateProvider`-driven slot advancement) rely on it moving
|
|
512
|
+
* forward. We reproduce the wall-clock advance the mine step just made (at least 1s per block) via a
|
|
513
|
+
* temporary block-timestamp interval, then restore any pre-existing interval.
|
|
508
514
|
*/
|
|
509
515
|
public async mineEmptyBlock(blockCount: number = 1): Promise<void> {
|
|
510
516
|
await this.execWithPausedAnvil(async () => {
|
|
511
517
|
const originalGasLimit = await this.getBlockGasLimit();
|
|
518
|
+
const parentTimestamp = await this.lastBlockTimestamp();
|
|
519
|
+
// anvil has no getter for the block-timestamp interval, but the pending block is stamped
|
|
520
|
+
// `latest + interval`, so this delta is the standing interval (0 when none is set).
|
|
521
|
+
const priorInterval = (await this.nextBlockTimestamp()) - parentTimestamp;
|
|
512
522
|
try {
|
|
513
523
|
await this.setBlockGasLimit(1n);
|
|
514
524
|
await this.doMine(blockCount);
|
|
515
525
|
} finally {
|
|
516
526
|
await this.setBlockGasLimit(originalGasLimit);
|
|
517
527
|
}
|
|
528
|
+
const advance = (await this.lastBlockTimestamp()) - parentTimestamp;
|
|
529
|
+
const perBlockInterval = Math.max(1, Math.floor(advance / blockCount));
|
|
518
530
|
// Replace the tiny-gas-limit blocks with empty blocks at the restored gas limit, keeping the same
|
|
519
|
-
// height
|
|
520
|
-
await this.doRpcCall('
|
|
531
|
+
// height. The reorged-out blocks held no transactions, so nothing returns to the pool.
|
|
532
|
+
await this.doRpcCall('anvil_setBlockTimestampInterval', [perBlockInterval]);
|
|
533
|
+
try {
|
|
534
|
+
await this.doRpcCall('anvil_reorg', [blockCount, []]);
|
|
535
|
+
} finally {
|
|
536
|
+
if (priorInterval > 0) {
|
|
537
|
+
await this.doRpcCall('anvil_setBlockTimestampInterval', [priorInterval]);
|
|
538
|
+
} else {
|
|
539
|
+
await this.doRpcCall('anvil_removeBlockTimestampInterval', []);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
521
542
|
});
|
|
522
543
|
|
|
523
544
|
this.logger.warn(`Mined ${blockCount} empty L1 ${pluralize('block', blockCount)}`);
|
package/src/test/start_anvil.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createLogger } from '@aztec/foundation/log';
|
|
2
2
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
3
3
|
import type { TestDateProvider } from '@aztec/foundation/timer';
|
|
4
|
-
import { fileURLToPath } from '@aztec/foundation/url';
|
|
5
4
|
|
|
6
5
|
import { type ChildProcess, spawn } from 'child_process';
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
import { resolveFoundryBinary } from '../foundry_binary.js';
|
|
8
8
|
|
|
9
9
|
/** Minimal interface matching the @viem/anvil Anvil shape used by callers. */
|
|
10
10
|
export interface Anvil {
|
|
@@ -14,6 +14,30 @@ export interface Anvil {
|
|
|
14
14
|
stop(): Promise<void>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
// Watchdog wrapper: instead of spawning anvil directly, we spawn a small bash supervisor that runs
|
|
18
|
+
// anvil as a background child and polls its own parent (this node process). If the parent dies for
|
|
19
|
+
// ANY reason — including SIGKILL / crash / OOM, where node's own exit handlers never run — the poll
|
|
20
|
+
// loop ends and the EXIT trap reaps anvil. The script is inlined (rather than shipped as a `.sh`) so
|
|
21
|
+
// it works from the published npm tarball too, and the resolved anvil binary is passed via
|
|
22
|
+
// `$ANVIL_BIN` so it works without `anvil` on PATH.
|
|
23
|
+
//
|
|
24
|
+
// `$@` is the anvil argv; `bash -c <script> bash <...args>` puts the args in `$@` and `$0` = 'bash'.
|
|
25
|
+
//
|
|
26
|
+
// The EXIT trap reaps anvil; INT/TERM just `exit` (which fires the EXIT trap) so a signal terminates
|
|
27
|
+
// the supervisor promptly instead of being swallowed — a trapped TERM does NOT terminate the shell,
|
|
28
|
+
// so trapping the kill directly on TERM would leave the poll loop running and the caller's teardown
|
|
29
|
+
// hanging until its SIGKILL escalation. `sleep & wait` makes the poll interruptible, so INT/TERM are
|
|
30
|
+
// handled immediately rather than after the current `sleep` returns.
|
|
31
|
+
const ANVIL_WATCHDOG = `
|
|
32
|
+
set -u
|
|
33
|
+
parent=$PPID
|
|
34
|
+
"$ANVIL_BIN" "$@" &
|
|
35
|
+
anvil_pid=$!
|
|
36
|
+
trap 'kill "$anvil_pid" 2>/dev/null' EXIT
|
|
37
|
+
trap 'exit 0' INT TERM
|
|
38
|
+
while kill -0 "$parent" 2>/dev/null; do sleep 1 & wait $!; done
|
|
39
|
+
`;
|
|
40
|
+
|
|
17
41
|
/**
|
|
18
42
|
* Ensures there's a running Anvil instance and returns the RPC URL.
|
|
19
43
|
*/
|
|
@@ -42,7 +66,7 @@ export async function startAnvil(
|
|
|
42
66
|
dateProvider?: TestDateProvider;
|
|
43
67
|
} = {},
|
|
44
68
|
): Promise<{ anvil: Anvil; methodCalls?: string[]; rpcUrl: string; stop: () => Promise<void> }> {
|
|
45
|
-
const anvilBinary =
|
|
69
|
+
const anvilBinary = resolveFoundryBinary('anvil');
|
|
46
70
|
const logger = opts.log ? createLogger('ethereum:anvil') : undefined;
|
|
47
71
|
const methodCalls = opts.captureMethodCalls ? ([] as string[]) : undefined;
|
|
48
72
|
|
|
@@ -50,6 +74,9 @@ export async function startAnvil(
|
|
|
50
74
|
|
|
51
75
|
const anvil = await retry(
|
|
52
76
|
async () => {
|
|
77
|
+
// `--port 0` lets anvil bind an OS-assigned ephemeral port; the actual port is read back from
|
|
78
|
+
// its "Listening on host:port" stdout below, so independent suites can spawn their own anvil
|
|
79
|
+
// in parallel without fighting over a fixed port.
|
|
53
80
|
const port = opts.port ?? (process.env.ANVIL_PORT ? parseInt(process.env.ANVIL_PORT) : 8545);
|
|
54
81
|
const args: string[] = [
|
|
55
82
|
'--host',
|
|
@@ -71,9 +98,11 @@ export async function startAnvil(
|
|
|
71
98
|
}
|
|
72
99
|
args.push('--slots-in-an-epoch', String(opts.slotsInAnEpoch ?? 1));
|
|
73
100
|
|
|
74
|
-
|
|
101
|
+
// Spawn the watchdog (see ANVIL_WATCHDOG). It launches anvil with these args and reaps it if we
|
|
102
|
+
// die; `$0` is 'bash' and `$@` is the anvil argv.
|
|
103
|
+
const child = spawn('bash', ['-c', ANVIL_WATCHDOG, 'bash', ...args], {
|
|
75
104
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
|
-
env: { ...process.env, RAYON_NUM_THREADS: '1' },
|
|
105
|
+
env: { ...process.env, ANVIL_BIN: anvilBinary, RAYON_NUM_THREADS: '1' },
|
|
77
106
|
});
|
|
78
107
|
|
|
79
108
|
// Wait for "Listening on" or an early exit.
|
|
@@ -183,7 +212,10 @@ function syncDateProviderFromAnvilOutput(text: string, dateProvider: TestDatePro
|
|
|
183
212
|
}
|
|
184
213
|
}
|
|
185
214
|
|
|
186
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* Send SIGTERM to the watchdog, wait up to 5 s, then SIGKILL. The watchdog's trap forwards the
|
|
217
|
+
* signal to anvil, so terminating it tears down anvil too. All timers are always cleared.
|
|
218
|
+
*/
|
|
187
219
|
function killChild(child: ChildProcess): Promise<void> {
|
|
188
220
|
return new Promise<void>(resolve => {
|
|
189
221
|
if (child.exitCode !== null || child.killed) {
|
package/src/utils.ts
CHANGED
|
@@ -235,18 +235,18 @@ export function formatViemError(error: any, abi: Abi = ErrorsAbi): FormattedViem
|
|
|
235
235
|
// If decoding fails, we fall back to the original formatting
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
-
// Strip ABI from the error object before formatting
|
|
239
|
-
// caller's error, but structuredClone throws DataCloneError on values it cannot clone (e.g.
|
|
240
|
-
// viem RPC errors carrying function-valued request context). If cloning fails, fall back to
|
|
241
|
-
// formatting the original error untouched rather than letting the clone failure mask it.
|
|
238
|
+
// Strip ABI from the error object before formatting
|
|
242
239
|
if (error && typeof error === 'object') {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
240
|
+
// Create a clone to avoid modifying the original
|
|
241
|
+
const errorClone = structuredClone(error);
|
|
242
|
+
|
|
243
|
+
// Helper function to recursively remove ABI properties
|
|
244
|
+
|
|
245
|
+
// Strip ABIs from the clone
|
|
246
|
+
stripAbis(errorClone);
|
|
247
|
+
|
|
248
|
+
// Use the cleaned clone for further processing
|
|
249
|
+
error = errorClone;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
// If it's a regular Error instance, return it with its message
|