@aztec/ethereum 0.0.1-commit.2f68f620 → 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 +22 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +54 -3
- package/dest/contracts/gse.d.ts +4 -2
- package/dest/contracts/gse.d.ts.map +1 -1
- package/dest/contracts/gse.js +2 -2
- 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/contracts/rollup.d.ts +168 -9
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +15 -5
- package/dest/deploy_aztec_l1_contracts.d.ts +12 -2
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +19 -5
- 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_artifacts.d.ts +1219 -77
- package/dest/l1_artifacts.d.ts.map +1 -1
- package/dest/l1_tx_utils/config.js +3 -3
- 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 +84 -22
- 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/publisher_manager.d.ts +18 -6
- package/dest/publisher_manager.d.ts.map +1 -1
- package/dest/publisher_manager.js +44 -7
- package/dest/queries.d.ts +1 -1
- package/dest/queries.d.ts.map +1 -1
- package/dest/queries.js +7 -1
- package/dest/test/blob_kzg_warmup.d.ts +19 -0
- package/dest/test/blob_kzg_warmup.d.ts.map +1 -0
- package/dest/test/blob_kzg_warmup.js +64 -0
- package/dest/test/chain_monitor.d.ts +33 -2
- package/dest/test/chain_monitor.d.ts.map +1 -1
- package/dest/test/chain_monitor.js +71 -6
- package/dest/test/eth_cheat_codes.d.ts +26 -3
- package/dest/test/eth_cheat_codes.d.ts.map +1 -1
- package/dest/test/eth_cheat_codes.js +72 -37
- package/dest/test/index.d.ts +2 -1
- package/dest/test/index.d.ts.map +1 -1
- package/dest/test/index.js +1 -0
- package/dest/test/rollup_cheat_codes.d.ts +30 -1
- package/dest/test/rollup_cheat_codes.d.ts.map +1 -1
- package/dest/test/rollup_cheat_codes.js +27 -0
- 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/package.json +5 -6
- package/src/config.ts +73 -3
- package/src/contracts/gse.ts +7 -2
- package/src/contracts/multicall.ts +8 -5
- package/src/contracts/rollup.ts +15 -6
- package/src/deploy_aztec_l1_contracts.ts +23 -5
- package/src/foundry_binary.ts +57 -0
- package/src/l1_tx_utils/config.ts +3 -3
- package/src/l1_tx_utils/l1_fee_analyzer.ts +75 -3
- package/src/l1_tx_utils/l1_tx_utils.ts +71 -20
- package/src/l1_tx_utils/types.ts +32 -0
- package/src/publisher_manager.ts +54 -18
- package/src/queries.ts +6 -0
- package/src/test/blob_kzg_warmup.ts +65 -0
- package/src/test/chain_monitor.ts +88 -5
- package/src/test/eth_cheat_codes.ts +66 -41
- package/src/test/index.ts +1 -0
- package/src/test/rollup_cheat_codes.ts +51 -0
- package/src/test/start_anvil.ts +38 -6
- 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,15 +247,75 @@ 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
|
|
|
284
|
+
/**
|
|
285
|
+
* Validates that `ethereumSlotDuration` and `aztecSlotDuration` are positive and that the L2 slot is an exact
|
|
286
|
+
* multiple of the L1 slot. Every L2 slot boundary must land on an L1 slot boundary; a non-multiple pairing
|
|
287
|
+
* desyncs the epoch/checkpoint timing math throughout the sequencer, validator client, and timetables. Returns
|
|
288
|
+
* a list of error messages (empty when valid).
|
|
289
|
+
*/
|
|
290
|
+
export function validateSlotDurations(
|
|
291
|
+
config: Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'>,
|
|
292
|
+
): string[] {
|
|
293
|
+
const errors: string[] = [];
|
|
294
|
+
if (config.ethereumSlotDuration <= 0) {
|
|
295
|
+
errors.push(`ethereumSlotDuration must be positive (got ${config.ethereumSlotDuration})`);
|
|
296
|
+
}
|
|
297
|
+
if (config.aztecSlotDuration <= 0) {
|
|
298
|
+
errors.push(`aztecSlotDuration must be positive (got ${config.aztecSlotDuration})`);
|
|
299
|
+
}
|
|
300
|
+
if (config.ethereumSlotDuration > 0 && config.aztecSlotDuration % config.ethereumSlotDuration !== 0) {
|
|
301
|
+
errors.push(
|
|
302
|
+
`aztecSlotDuration (${config.aztecSlotDuration}s) must be a multiple of ethereumSlotDuration ` +
|
|
303
|
+
`(${config.ethereumSlotDuration}s)`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return errors;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Throws if {@link validateSlotDurations} reports any errors. */
|
|
310
|
+
export function assertValidSlotDurations(
|
|
311
|
+
config: Pick<L1ContractsConfig, 'ethereumSlotDuration' | 'aztecSlotDuration'>,
|
|
312
|
+
): void {
|
|
313
|
+
const errors = validateSlotDurations(config);
|
|
314
|
+
if (errors.length > 0) {
|
|
315
|
+
throw new Error(`Invalid slot duration configuration:\n${errors.map(e => ` - ${e}`).join('\n')}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
249
319
|
export const genesisStateConfigMappings: ConfigMappingsType<GenesisStateConfig> = {
|
|
250
320
|
testAccounts: {
|
|
251
321
|
env: 'TEST_ACCOUNTS',
|
package/src/contracts/gse.ts
CHANGED
|
@@ -47,11 +47,16 @@ export class GSEContract {
|
|
|
47
47
|
return EthAddress.fromString(await this.gse.read.getGovernance());
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
getAttestersFromIndicesAtTime(
|
|
50
|
+
getAttestersFromIndicesAtTime(
|
|
51
|
+
instance: Hex | EthAddress,
|
|
52
|
+
ts: bigint,
|
|
53
|
+
indices: bigint[],
|
|
54
|
+
options?: { blockNumber?: bigint },
|
|
55
|
+
) {
|
|
51
56
|
if (instance instanceof EthAddress) {
|
|
52
57
|
instance = instance.toString();
|
|
53
58
|
}
|
|
54
|
-
return this.gse.read.getAttestersFromIndicesAtTime([instance, ts, indices]);
|
|
59
|
+
return this.gse.read.getAttestersFromIndicesAtTime([instance, ts, indices], options);
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
public async getRegistrationDigest(publicKey: ProjPointType<bigint>): Promise<ProjPointType<bigint>> {
|
|
@@ -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. */
|
package/src/contracts/rollup.ts
CHANGED
|
@@ -83,6 +83,7 @@ export type ViemHeader = {
|
|
|
83
83
|
feeRecipient: `0x${string}`;
|
|
84
84
|
gasFees: ViemGasFees;
|
|
85
85
|
totalManaUsed: bigint;
|
|
86
|
+
accumulatedFees: bigint;
|
|
86
87
|
};
|
|
87
88
|
|
|
88
89
|
export type ViemGasFees = {
|
|
@@ -554,8 +555,9 @@ export class RollupContract {
|
|
|
554
555
|
return EthAddress.fromString(await this.rollup.read.owner());
|
|
555
556
|
}
|
|
556
557
|
|
|
557
|
-
async getActiveAttesterCount(): Promise<number> {
|
|
558
|
-
|
|
558
|
+
async getActiveAttesterCount(options?: { blockNumber?: bigint }): Promise<number> {
|
|
559
|
+
await checkBlockTag(options?.blockNumber, this.client);
|
|
560
|
+
return Number(await this.rollup.read.getActiveAttesterCount(options));
|
|
559
561
|
}
|
|
560
562
|
|
|
561
563
|
public async getSlashingProposerAddress() {
|
|
@@ -812,7 +814,7 @@ export class RollupContract {
|
|
|
812
814
|
}
|
|
813
815
|
|
|
814
816
|
async getEpochProofPublicInputs(
|
|
815
|
-
args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly
|
|
817
|
+
args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly ViemHeader[], `0x${string}`],
|
|
816
818
|
): Promise<Fr[]> {
|
|
817
819
|
const result = await this.rollup.read.getEpochProofPublicInputs(args);
|
|
818
820
|
return result.map(Fr.fromString);
|
|
@@ -1190,13 +1192,20 @@ export class RollupContract {
|
|
|
1190
1192
|
}
|
|
1191
1193
|
|
|
1192
1194
|
async getAttesters(timestamp?: bigint): Promise<EthAddress[]> {
|
|
1193
|
-
|
|
1195
|
+
// Pin every read to a single L1 block so the attester count and the chunked index reads
|
|
1196
|
+
// observe a consistent set. Without this, the count and each chunk default to `latest` and
|
|
1197
|
+
// can straddle a block boundary (or reorg), yielding an inconsistent or truncated set.
|
|
1198
|
+
const block = await this.client.getBlock();
|
|
1199
|
+
const blockNumber = block.number ?? undefined;
|
|
1200
|
+
const ts = timestamp ?? block.timestamp;
|
|
1201
|
+
const attesterSize = await this.getActiveAttesterCount({ blockNumber });
|
|
1194
1202
|
const gse = new GSEContract(this.client, await this.getGSE());
|
|
1195
|
-
const ts = timestamp ?? (await this.client.getBlock()).timestamp;
|
|
1196
1203
|
const indices = Array.from({ length: attesterSize }, (_, i) => BigInt(i));
|
|
1197
1204
|
const chunks = chunk(indices, 1000);
|
|
1198
1205
|
|
|
1199
|
-
const results = await Promise.all(
|
|
1206
|
+
const results = await Promise.all(
|
|
1207
|
+
chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk, { blockNumber })),
|
|
1208
|
+
);
|
|
1200
1209
|
return results.flat().map(addr => EthAddress.fromString(addr));
|
|
1201
1210
|
}
|
|
1202
1211
|
|
|
@@ -19,9 +19,10 @@ import { mainnet, sepolia } from 'viem/chains';
|
|
|
19
19
|
|
|
20
20
|
import { createEthereumChain, isAnvilTestChain } from './chain.js';
|
|
21
21
|
import { createExtendedL1Client } from './client.js';
|
|
22
|
-
import type
|
|
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
|
|
|
@@ -95,11 +96,16 @@ function runProcess<T>(
|
|
|
95
96
|
|
|
96
97
|
// Covers an edge where where we may have a cached BlobLib that is not meant for production.
|
|
97
98
|
// Despite the profile apparently sometimes cached code remains (so says Lasse after his ignition-monorepo arc).
|
|
98
|
-
async function maybeForgeForceProductionBuild(
|
|
99
|
+
async function maybeForgeForceProductionBuild(
|
|
100
|
+
forgeBin: string,
|
|
101
|
+
l1ContractsPath: string,
|
|
102
|
+
script: string,
|
|
103
|
+
chainId: number,
|
|
104
|
+
) {
|
|
99
105
|
if (chainId === mainnet.id) {
|
|
100
106
|
logger.info(`Recompiling ${script} with production profile for mainnet deployment`);
|
|
101
107
|
logger.info('This may take a minute but ensures production BlobLib is used.');
|
|
102
|
-
await runProcess(
|
|
108
|
+
await runProcess(forgeBin, ['build', script, '--force'], { FOUNDRY_PROFILE: 'production' }, l1ContractsPath);
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -282,6 +288,7 @@ export async function deployAztecL1Contracts(
|
|
|
282
288
|
args: DeployAztecL1ContractsArgs,
|
|
283
289
|
): Promise<DeployAztecL1ContractsReturnType> {
|
|
284
290
|
logger.info(`Deploying L1 contracts with config: ${jsonStringify(args)}`);
|
|
291
|
+
assertValidSlotDurations(args);
|
|
285
292
|
if (args.initialValidators && args.initialValidators.length > 0 && args.existingTokenAddress) {
|
|
286
293
|
throw new Error(
|
|
287
294
|
'Cannot deploy with both initialValidators and existingTokenAddress. ' +
|
|
@@ -319,8 +326,9 @@ export async function deployAztecL1Contracts(
|
|
|
319
326
|
// Use foundry-artifacts from l1-artifacts package
|
|
320
327
|
const l1ContractsPath = prepareL1ContractsForDeployment();
|
|
321
328
|
|
|
329
|
+
const forgeBin = resolveFoundryBinary('forge');
|
|
322
330
|
const FORGE_SCRIPT = 'script/deploy/DeployAztecL1Contracts.s.sol';
|
|
323
|
-
await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
|
|
331
|
+
await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
|
|
324
332
|
|
|
325
333
|
// Verify contracts on Etherscan when on mainnet/sepolia and ETHERSCAN_API_KEY is available.
|
|
326
334
|
const isVerifiableChain = chainId === mainnet.id || chainId === sepolia.id;
|
|
@@ -345,6 +353,8 @@ export async function deployAztecL1Contracts(
|
|
|
345
353
|
...(shouldVerify ? ['--verify'] : []),
|
|
346
354
|
];
|
|
347
355
|
const forgeEnv = {
|
|
356
|
+
// Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
|
|
357
|
+
FORGE_BIN: forgeBin,
|
|
348
358
|
// Env vars required by l1-contracts/script/deploy/DeploymentConfiguration.sol.
|
|
349
359
|
NETWORK: getActiveNetworkName(),
|
|
350
360
|
FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
|
|
@@ -589,6 +599,11 @@ export function getDeployRollupForUpgradeEnvVars(
|
|
|
589
599
|
AZTEC_SLASH_AMOUNT_SMALL: args.slashAmountSmall.toString(),
|
|
590
600
|
AZTEC_SLASH_AMOUNT_MEDIUM: args.slashAmountMedium.toString(),
|
|
591
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(),
|
|
592
607
|
} as const;
|
|
593
608
|
}
|
|
594
609
|
|
|
@@ -612,12 +627,15 @@ export const deployRollupForUpgrade = async (
|
|
|
612
627
|
// Use foundry-artifacts from l1-artifacts package
|
|
613
628
|
const l1ContractsPath = prepareL1ContractsForDeployment();
|
|
614
629
|
|
|
630
|
+
const forgeBin = resolveFoundryBinary('forge');
|
|
615
631
|
const FORGE_SCRIPT = 'script/deploy/DeployRollupForUpgrade.s.sol';
|
|
616
|
-
await maybeForgeForceProductionBuild(l1ContractsPath, FORGE_SCRIPT, chainId);
|
|
632
|
+
await maybeForgeForceProductionBuild(forgeBin, l1ContractsPath, FORGE_SCRIPT, chainId);
|
|
617
633
|
|
|
618
634
|
const scriptPath = join(getL1ContractsPath(), 'scripts', 'forge_broadcast.js');
|
|
619
635
|
const forgeArgs = [FORGE_SCRIPT, '--sig', 'run()', '--private-key', privateKey, '--rpc-url', rpcUrl];
|
|
620
636
|
const forgeEnv = {
|
|
637
|
+
// Resolved forge binary picked up by forge_broadcast.js, so it works without forge on PATH.
|
|
638
|
+
FORGE_BIN: forgeBin,
|
|
621
639
|
FOUNDRY_PROFILE: chainId === mainnet.id ? 'production' : undefined,
|
|
622
640
|
// Env vars required by l1-contracts/script/deploy/RollupConfiguration.sol.
|
|
623
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
|
+
}
|
|
@@ -73,7 +73,7 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
|
|
|
73
73
|
gasLimitBufferPercentage: {
|
|
74
74
|
description: 'How much to increase calculated gas limit by (percentage)',
|
|
75
75
|
env: 'L1_GAS_LIMIT_BUFFER_PERCENTAGE',
|
|
76
|
-
...
|
|
76
|
+
...floatConfigHelper(20),
|
|
77
77
|
},
|
|
78
78
|
maxGwei: {
|
|
79
79
|
description: 'Maximum gas price in gwei to be used for transactions.',
|
|
@@ -90,12 +90,12 @@ export const l1TxUtilsConfigMappings: ConfigMappingsType<L1TxUtilsConfig> = {
|
|
|
90
90
|
priorityFeeBumpPercentage: {
|
|
91
91
|
description: 'How much to increase priority fee by each attempt (percentage)',
|
|
92
92
|
env: 'L1_PRIORITY_FEE_BUMP_PERCENTAGE',
|
|
93
|
-
...
|
|
93
|
+
...floatConfigHelper(20),
|
|
94
94
|
},
|
|
95
95
|
priorityFeeRetryBumpPercentage: {
|
|
96
96
|
description: 'How much to increase priority fee by each retry attempt (percentage)',
|
|
97
97
|
env: 'L1_PRIORITY_FEE_RETRY_BUMP_PERCENTAGE',
|
|
98
|
-
...
|
|
98
|
+
...floatConfigHelper(50),
|
|
99
99
|
},
|
|
100
100
|
minimumPriorityFeePerGas: {
|
|
101
101
|
description:
|
|
@@ -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
|
+
}
|
|
@@ -5,7 +5,7 @@ import { InterruptError, TimeoutError } from '@aztec/foundation/error';
|
|
|
5
5
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
6
|
import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
|
|
7
7
|
import { Semaphore } from '@aztec/foundation/queue';
|
|
8
|
-
import { retryUntil } from '@aztec/foundation/retry';
|
|
8
|
+
import { makeBackoff, retry, retryUntil } from '@aztec/foundation/retry';
|
|
9
9
|
import { sleep } from '@aztec/foundation/sleep';
|
|
10
10
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
11
11
|
import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
|
|
@@ -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,
|
|
@@ -44,6 +45,11 @@ import {
|
|
|
44
45
|
|
|
45
46
|
const MAX_L1_TX_STATES = 32;
|
|
46
47
|
|
|
48
|
+
// Backoff (in seconds) for retrying the read-only RPC calls that prepare a tx cancellation. A
|
|
49
|
+
// cancellation is fired in the background after a tx times out and is important (it frees the stuck
|
|
50
|
+
// nonce), so a transient RPC failure while reading the pending nonce or gas price must not abandon it.
|
|
51
|
+
const CANCELLATION_PREP_RETRY_BACKOFF_S = [1, 2, 4, 8];
|
|
52
|
+
|
|
47
53
|
export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
48
54
|
protected txs: L1TxState[] = [];
|
|
49
55
|
/** Last nonce successfully sent to the chain. Used as a lower bound when a fallback RPC node returns a stale count. */
|
|
@@ -301,6 +307,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
301
307
|
txConfigOverrides: gasConfigOverrides ?? {},
|
|
302
308
|
sentAtL1Ts: now,
|
|
303
309
|
lastSentAtL1Ts: now,
|
|
310
|
+
gasPriceHistory: [baseState.gasPrice],
|
|
304
311
|
};
|
|
305
312
|
|
|
306
313
|
// And persist it
|
|
@@ -431,10 +438,23 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
431
438
|
|
|
432
439
|
const initialTxHash = txHashes[0];
|
|
433
440
|
let currentTxHash = txHashes.at(-1)!;
|
|
434
|
-
let l1Timestamp
|
|
441
|
+
let l1Timestamp = 0;
|
|
435
442
|
|
|
436
443
|
while (true) {
|
|
437
|
-
|
|
444
|
+
try {
|
|
445
|
+
l1Timestamp = await this.getL1Timestamp();
|
|
446
|
+
} catch (err) {
|
|
447
|
+
// A transient RPC failure here must not abort monitoring with a rejection: callers that
|
|
448
|
+
// fire this loop in the background would surface it as an unhandled rejection (e.g. when a
|
|
449
|
+
// test tears down its L1 node while a monitor iteration is in flight). Exit quietly if we
|
|
450
|
+
// are shutting down, otherwise retry on the next interval.
|
|
451
|
+
if (this.interrupted) {
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
this.logger.error(`Error fetching L1 timestamp while monitoring tx ${currentTxHash}`, err, { nonce, account });
|
|
455
|
+
await sleep(gasConfig.checkIntervalMs!);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
438
458
|
|
|
439
459
|
try {
|
|
440
460
|
const timePassed = l1Timestamp - state.lastSentAtL1Ts.getTime();
|
|
@@ -486,6 +506,7 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
486
506
|
if (timePassed >= stallTimeMs && attempts <= maxSpeedUpAttempts) {
|
|
487
507
|
const newGasPrice = await this.getGasPrice(gasConfig, isBlobTx, attempts, state.gasPrice);
|
|
488
508
|
state.gasPrice = newGasPrice;
|
|
509
|
+
state.gasPriceHistory?.push(newGasPrice);
|
|
489
510
|
|
|
490
511
|
this.logger.debug(
|
|
491
512
|
`Tx ${currentTxHash} with nonce ${nonce} from ${account} appears stuck. ` +
|
|
@@ -654,8 +675,22 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
654
675
|
blobInputs?: L1BlobInputs,
|
|
655
676
|
): Promise<{ receipt: TransactionReceipt; state: L1TxState }> {
|
|
656
677
|
const { state } = await this.sendTransaction(request, gasConfig, blobInputs);
|
|
657
|
-
|
|
658
|
-
|
|
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
|
+
}
|
|
659
694
|
}
|
|
660
695
|
|
|
661
696
|
public override async simulate(
|
|
@@ -706,9 +741,37 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
706
741
|
return;
|
|
707
742
|
}
|
|
708
743
|
|
|
709
|
-
//
|
|
710
|
-
|
|
711
|
-
|
|
744
|
+
// Resolve the pending nonce and cancellation gas price up front. These are read-only RPC calls, so
|
|
745
|
+
// they are safe to retry; a transient RPC failure here must not permanently abandon the cancellation
|
|
746
|
+
// (which would leave the nonce stuck and the tx state stranded short of CANCELLED). We retry with a
|
|
747
|
+
// real backoff before giving up, unlike getGasPrice's tight internal retry which can be exhausted by
|
|
748
|
+
// a brief node hiccup. The retried block performs no state-changing send, so retrying cannot
|
|
749
|
+
// double-send the cancellation tx.
|
|
750
|
+
const { currentNonce, cancelGasPrice } = await retry(
|
|
751
|
+
async () => {
|
|
752
|
+
const currentNonce = await this.client.getTransactionCount({ address: account, blockTag: 'pending' });
|
|
753
|
+
if (currentNonce >= nonce) {
|
|
754
|
+
// Get gas price with higher priority fee for cancellation
|
|
755
|
+
const cancelGasPrice = await this.getGasPrice(
|
|
756
|
+
{
|
|
757
|
+
...this.config,
|
|
758
|
+
// Use high bump for cancellation to ensure it replaces the original tx
|
|
759
|
+
priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
|
|
760
|
+
},
|
|
761
|
+
isBlobTx,
|
|
762
|
+
state.txHashes.length,
|
|
763
|
+
previousGasPrice,
|
|
764
|
+
);
|
|
765
|
+
return { currentNonce, cancelGasPrice };
|
|
766
|
+
}
|
|
767
|
+
return { currentNonce, cancelGasPrice: undefined };
|
|
768
|
+
},
|
|
769
|
+
`Preparing cancellation for L1 tx from account ${account} with nonce ${nonce}`,
|
|
770
|
+
makeBackoff(CANCELLATION_PREP_RETRY_BACKOFF_S),
|
|
771
|
+
this.logger,
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
if (cancelGasPrice === undefined) {
|
|
712
775
|
this.logger.verbose(
|
|
713
776
|
`Not sending cancellation for L1 tx from account ${account} with nonce ${nonce} as it is dropped`,
|
|
714
777
|
{ nonce, account, currentNonce },
|
|
@@ -717,18 +780,6 @@ export class L1TxUtils extends ReadOnlyL1TxUtils {
|
|
|
717
780
|
return;
|
|
718
781
|
}
|
|
719
782
|
|
|
720
|
-
// Get gas price with higher priority fee for cancellation
|
|
721
|
-
const cancelGasPrice = await this.getGasPrice(
|
|
722
|
-
{
|
|
723
|
-
...this.config,
|
|
724
|
-
// Use high bump for cancellation to ensure it replaces the original tx
|
|
725
|
-
priorityFeeRetryBumpPercentage: 150, // 150% bump should be enough to replace any tx
|
|
726
|
-
},
|
|
727
|
-
isBlobTx,
|
|
728
|
-
state.txHashes.length,
|
|
729
|
-
previousGasPrice,
|
|
730
|
-
);
|
|
731
|
-
|
|
732
783
|
const { maxFeePerGas, maxPriorityFeePerGas, maxFeePerBlobGas } = cancelGasPrice;
|
|
733
784
|
this.logger.verbose(
|
|
734
785
|
`Attempting to cancel L1 ${isBlobTx ? 'blob' : 'vanilla'} transaction from account ${account} with nonce ${nonce} after time out`,
|