@aztec/ethereum 0.0.1-commit.993d240 → 0.0.1-commit.9a89641
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/client.d.ts +11 -3
- package/dest/client.d.ts.map +1 -1
- package/dest/client.js +35 -3
- package/dest/config.d.ts +22 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +54 -3
- package/dest/contracts/governance.d.ts +36 -11
- package/dest/contracts/governance.d.ts.map +1 -1
- package/dest/contracts/governance.js +54 -24
- package/dest/contracts/governance_proposer.d.ts +7 -8
- package/dest/contracts/governance_proposer.d.ts.map +1 -1
- package/dest/contracts/governance_proposer.js +7 -10
- package/dest/contracts/gse.d.ts +8 -2
- package/dest/contracts/gse.d.ts.map +1 -1
- package/dest/contracts/gse.js +11 -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/outbox.d.ts +8 -2
- package/dest/contracts/outbox.d.ts.map +1 -1
- package/dest/contracts/outbox.js +6 -2
- package/dest/contracts/rollup.d.ts +176 -9
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +48 -8
- package/dest/contracts/slashing_proposer.d.ts +37 -6
- package/dest/contracts/slashing_proposer.d.ts.map +1 -1
- package/dest/contracts/slashing_proposer.js +447 -17
- package/dest/deploy_aztec_l1_contracts.d.ts +16 -5
- package/dest/deploy_aztec_l1_contracts.d.ts.map +1 -1
- package/dest/deploy_aztec_l1_contracts.js +26 -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_artifacts.d.ts +1242 -95
- 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/readonly_l1_tx_utils.d.ts +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.d.ts.map +1 -1
- package/dest/l1_tx_utils/readonly_l1_tx_utils.js +2 -1
- 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/dest/utils.d.ts +2 -2
- package/dest/utils.d.ts.map +1 -1
- package/dest/utils.js +15 -6
- package/package.json +5 -6
- package/src/client.ts +39 -1
- package/src/config.ts +73 -3
- package/src/contracts/governance.ts +63 -20
- package/src/contracts/governance_proposer.ts +11 -15
- package/src/contracts/gse.ts +19 -2
- package/src/contracts/multicall.ts +8 -5
- package/src/contracts/outbox.ts +6 -2
- package/src/contracts/rollup.ts +57 -8
- package/src/contracts/slashing_proposer.ts +78 -15
- package/src/deploy_aztec_l1_contracts.ts +31 -13
- 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/readonly_l1_tx_utils.ts +2 -1
- 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/src/utils.ts +12 -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
|
@@ -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/outbox.ts
CHANGED
|
@@ -89,9 +89,13 @@ export class OutboxContract {
|
|
|
89
89
|
* Returns every root stored for `epoch`. Slot `i` of the returned array holds the root inserted
|
|
90
90
|
* for `numCheckpointsInEpoch = i + 1`, or `Fr.ZERO` if no proof of that depth has been inserted
|
|
91
91
|
* yet. The array length is always `MAX_CHECKPOINTS_PER_EPOCH`.
|
|
92
|
+
*
|
|
93
|
+
* @param opts - Optional viem read options. Pass `blockNumber` to pin the read to a specific L1
|
|
94
|
+
* block, which is important for callers that want a consistent snapshot across multiple reads
|
|
95
|
+
* (e.g. the archiver's outbox-trees resolver, which pins to the node's synced L1 block).
|
|
92
96
|
*/
|
|
93
|
-
public async getRoots(epoch: EpochNumber): Promise<Fr[]> {
|
|
94
|
-
const raw = await this.outbox.read.getRoots([BigInt(epoch)]);
|
|
97
|
+
public async getRoots(epoch: EpochNumber, opts?: { blockNumber?: bigint }): Promise<Fr[]> {
|
|
98
|
+
const raw = await this.outbox.read.getRoots([BigInt(epoch)], opts);
|
|
95
99
|
return raw.map(hex => Fr.fromString(hex));
|
|
96
100
|
}
|
|
97
101
|
|
package/src/contracts/rollup.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { EthAddress } from '@aztec/foundation/eth-address';
|
|
|
6
6
|
import type { ViemSignature } from '@aztec/foundation/eth-signature';
|
|
7
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
8
8
|
import { makeBackoff, retry } from '@aztec/foundation/retry';
|
|
9
|
+
import { getErrorCause } from '@aztec/foundation/types';
|
|
9
10
|
import { EscapeHatchAbi } from '@aztec/l1-artifacts/EscapeHatchAbi';
|
|
10
11
|
import { RollupAbi } from '@aztec/l1-artifacts/RollupAbi';
|
|
11
12
|
import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
|
|
@@ -13,11 +14,14 @@ import { RollupStorage } from '@aztec/l1-artifacts/RollupStorage';
|
|
|
13
14
|
import chunk from 'lodash.chunk';
|
|
14
15
|
import {
|
|
15
16
|
type Account,
|
|
17
|
+
ContractFunctionRevertedError,
|
|
16
18
|
type GetContractReturnType,
|
|
17
19
|
type Hex,
|
|
18
20
|
type Log,
|
|
21
|
+
RpcRequestError,
|
|
19
22
|
type StateOverride,
|
|
20
23
|
type WatchContractEventReturnType,
|
|
24
|
+
decodeErrorResult,
|
|
21
25
|
encodeAbiParameters,
|
|
22
26
|
encodeFunctionData,
|
|
23
27
|
getContract,
|
|
@@ -79,6 +83,7 @@ export type ViemHeader = {
|
|
|
79
83
|
feeRecipient: `0x${string}`;
|
|
80
84
|
gasFees: ViemGasFees;
|
|
81
85
|
totalManaUsed: bigint;
|
|
86
|
+
accumulatedFees: bigint;
|
|
82
87
|
};
|
|
83
88
|
|
|
84
89
|
export type ViemGasFees = {
|
|
@@ -230,6 +235,32 @@ export type CheckpointProposedArgs = {
|
|
|
230
235
|
/** Log type for CheckpointProposed events. */
|
|
231
236
|
export type CheckpointProposedLog = L1EventLog<CheckpointProposedArgs>;
|
|
232
237
|
|
|
238
|
+
const INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR = 'ValidatorSelection__InsufficientValidatorSetSize';
|
|
239
|
+
|
|
240
|
+
function isValidatorSelectionError(err: unknown, errorName: string): boolean {
|
|
241
|
+
return (
|
|
242
|
+
getErrorCause(err, ContractFunctionRevertedError)?.data?.errorName === errorName ||
|
|
243
|
+
decodeRpcRequestErrorName(err) === errorName
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function decodeRpcRequestErrorName(err: unknown): string | undefined {
|
|
248
|
+
const data = getErrorCause(err, RpcRequestError)?.data;
|
|
249
|
+
if (!isHexString(data)) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
return decodeErrorResult({ abi: RollupAbi, data }).errorName;
|
|
255
|
+
} catch {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isHexString(value: unknown): value is Hex {
|
|
261
|
+
return typeof value === 'string' && value.startsWith('0x');
|
|
262
|
+
}
|
|
263
|
+
|
|
233
264
|
export class RollupContract {
|
|
234
265
|
private readonly rollup: GetContractReturnType<typeof RollupAbi, ViemClient>;
|
|
235
266
|
private readonly logger = createLogger('ethereum:rollup');
|
|
@@ -524,8 +555,19 @@ export class RollupContract {
|
|
|
524
555
|
return EthAddress.fromString(await this.rollup.read.owner());
|
|
525
556
|
}
|
|
526
557
|
|
|
527
|
-
async getActiveAttesterCount(): Promise<number> {
|
|
528
|
-
|
|
558
|
+
async getActiveAttesterCount(options?: { blockNumber?: bigint }): Promise<number> {
|
|
559
|
+
await checkBlockTag(options?.blockNumber, this.client);
|
|
560
|
+
return Number(await this.rollup.read.getActiveAttesterCount(options));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Number of attesters that were staked at the given (historical) L1 timestamp. This is the count that
|
|
565
|
+
* validator-set sampling uses to decide whether an epoch gets a committee, and the historical counterpart
|
|
566
|
+
* of {@link getActiveAttesterCount} (which reads at the latest L1 block).
|
|
567
|
+
*/
|
|
568
|
+
async getAttesterCountAtTime(timestamp: bigint, options?: { blockNumber?: bigint }): Promise<number> {
|
|
569
|
+
const gse = new GSEContract(this.client, await this.getGSE());
|
|
570
|
+
return gse.getAttesterCountAtTime(this.address, timestamp, options);
|
|
529
571
|
}
|
|
530
572
|
|
|
531
573
|
public async getSlashingProposerAddress() {
|
|
@@ -588,7 +630,7 @@ export class RollupContract {
|
|
|
588
630
|
args: [timestamp],
|
|
589
631
|
})
|
|
590
632
|
.catch(e => {
|
|
591
|
-
if (e
|
|
633
|
+
if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
|
|
592
634
|
return { result: undefined };
|
|
593
635
|
}
|
|
594
636
|
throw e;
|
|
@@ -618,7 +660,7 @@ export class RollupContract {
|
|
|
618
660
|
args: [],
|
|
619
661
|
})
|
|
620
662
|
.catch(e => {
|
|
621
|
-
if (e
|
|
663
|
+
if (isValidatorSelectionError(e, INSUFFICIENT_VALIDATOR_SET_SIZE_ERROR)) {
|
|
622
664
|
return { result: undefined };
|
|
623
665
|
}
|
|
624
666
|
throw e;
|
|
@@ -782,7 +824,7 @@ export class RollupContract {
|
|
|
782
824
|
}
|
|
783
825
|
|
|
784
826
|
async getEpochProofPublicInputs(
|
|
785
|
-
args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly
|
|
827
|
+
args: readonly [bigint, bigint, EpochProofPublicInputArgs, readonly ViemHeader[], `0x${string}`],
|
|
786
828
|
): Promise<Fr[]> {
|
|
787
829
|
const result = await this.rollup.read.getEpochProofPublicInputs(args);
|
|
788
830
|
return result.map(Fr.fromString);
|
|
@@ -1160,13 +1202,20 @@ export class RollupContract {
|
|
|
1160
1202
|
}
|
|
1161
1203
|
|
|
1162
1204
|
async getAttesters(timestamp?: bigint): Promise<EthAddress[]> {
|
|
1163
|
-
|
|
1205
|
+
// Pin every read to a single L1 block so the attester count and the chunked index reads
|
|
1206
|
+
// observe a consistent set. Without this, the count and each chunk default to `latest` and
|
|
1207
|
+
// can straddle a block boundary (or reorg), yielding an inconsistent or truncated set.
|
|
1208
|
+
const block = await this.client.getBlock();
|
|
1209
|
+
const blockNumber = block.number ?? undefined;
|
|
1210
|
+
const ts = timestamp ?? block.timestamp;
|
|
1211
|
+
const attesterSize = await this.getActiveAttesterCount({ blockNumber });
|
|
1164
1212
|
const gse = new GSEContract(this.client, await this.getGSE());
|
|
1165
|
-
const ts = timestamp ?? (await this.client.getBlock()).timestamp;
|
|
1166
1213
|
const indices = Array.from({ length: attesterSize }, (_, i) => BigInt(i));
|
|
1167
1214
|
const chunks = chunk(indices, 1000);
|
|
1168
1215
|
|
|
1169
|
-
const results = await Promise.all(
|
|
1216
|
+
const results = await Promise.all(
|
|
1217
|
+
chunks.map(chunk => gse.getAttestersFromIndicesAtTime(this.address, ts, chunk, { blockNumber })),
|
|
1218
|
+
);
|
|
1170
1219
|
return results.flat().map(addr => EthAddress.fromString(addr));
|
|
1171
1220
|
}
|
|
1172
1221
|
|
|
@@ -3,6 +3,7 @@ import type { ViemClient } from '@aztec/ethereum/types';
|
|
|
3
3
|
import { mergeAbis, tryExtractEvent } from '@aztec/ethereum/utils';
|
|
4
4
|
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
5
|
import { Buffer32 } from '@aztec/foundation/buffer';
|
|
6
|
+
import { memoize } from '@aztec/foundation/decorators';
|
|
6
7
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
7
8
|
import { Signature } from '@aztec/foundation/eth-signature';
|
|
8
9
|
import { hexToBuffer } from '@aztec/foundation/string';
|
|
@@ -25,6 +26,14 @@ import {
|
|
|
25
26
|
export class SlashingProposerContract {
|
|
26
27
|
private readonly contract: GetContractReturnType<typeof SlashingProposerAbi, ViemClient>;
|
|
27
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Slash target validators of the last round asked for. Safe to cache because a round's targets are the committees
|
|
31
|
+
* of epochs that had already ended when the round opened (see SLASH_OFFSET_IN_ROUNDS), and those committees are
|
|
32
|
+
* sampled from validator set and randao snapshots taken before the epoch started, so they cannot change while the
|
|
33
|
+
* round is live. The promise is cached rather than the result, so concurrent callers share one in-flight read.
|
|
34
|
+
*/
|
|
35
|
+
private slashTargetValidators: { round: bigint; validators: Promise<EthAddress[]> } | undefined;
|
|
36
|
+
|
|
28
37
|
constructor(
|
|
29
38
|
public readonly client: ViemClient,
|
|
30
39
|
address: Hex | EthAddress,
|
|
@@ -64,6 +73,8 @@ export class SlashingProposerContract {
|
|
|
64
73
|
return this.contract.read.EXECUTION_DELAY_IN_ROUNDS();
|
|
65
74
|
}
|
|
66
75
|
|
|
76
|
+
/** Returns the slash amounts for the three slash unit levels. Immutable on the contract, so memoized. */
|
|
77
|
+
@memoize
|
|
67
78
|
public getSlashingAmounts(): Promise<[bigint, bigint, bigint]> {
|
|
68
79
|
return Promise.all([
|
|
69
80
|
this.contract.read.SLASH_AMOUNT_SMALL(),
|
|
@@ -234,35 +245,70 @@ export class SlashingProposerContract {
|
|
|
234
245
|
};
|
|
235
246
|
}
|
|
236
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Returns the validators eligible to be voted against in a round, in the order votes encode them. Cached for the
|
|
250
|
+
* last round asked for: reading them runs a committee sampling simulation on L1, and every vote of a round decodes
|
|
251
|
+
* against the same list, so a caller walking a round's votes would otherwise repeat that call per vote. Only the
|
|
252
|
+
* last round is kept since callers move forward round by round.
|
|
253
|
+
*/
|
|
254
|
+
public getSlashTargetValidators(round: bigint): Promise<EthAddress[]> {
|
|
255
|
+
const cached = this.slashTargetValidators;
|
|
256
|
+
if (cached?.round === round) {
|
|
257
|
+
return cached.validators;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const entry = { round, validators: this.fetchSlashTargetValidators(round) };
|
|
261
|
+
this.slashTargetValidators = entry;
|
|
262
|
+
// A failed read must not stay cached, or every later vote of the round would replay the same rejection
|
|
263
|
+
entry.validators.catch(() => {
|
|
264
|
+
if (this.slashTargetValidators === entry) {
|
|
265
|
+
this.slashTargetValidators = undefined;
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
return entry.validators;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private async fetchSlashTargetValidators(round: bigint): Promise<EthAddress[]> {
|
|
272
|
+
const { result } = await this.contract.simulate.getSlashTargetCommittees([round]);
|
|
273
|
+
return result.flat().map(validator => EthAddress.fromString(validator));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Returns the slash amount voted for each target validator by a single vote of a round.
|
|
278
|
+
* @param index - Position of the vote within the round, from 0 (inclusive) to the round's vote count (exclusive)
|
|
279
|
+
*/
|
|
280
|
+
public async getVoteAt(round: bigint, index: bigint): Promise<SlashVoteTarget[]> {
|
|
281
|
+
const [validators, vote, slashAmounts] = await Promise.all([
|
|
282
|
+
this.getSlashTargetValidators(round),
|
|
283
|
+
this.contract.read.getVotes([round, index]),
|
|
284
|
+
this.getSlashingAmounts(),
|
|
285
|
+
]);
|
|
286
|
+
return decodeVote(vote, validators, slashAmounts);
|
|
287
|
+
}
|
|
288
|
+
|
|
237
289
|
/** Returns the last vote emitted for a given round */
|
|
238
290
|
public async getLastVote(round: bigint) {
|
|
239
291
|
const { voteCount } = await this.getRound(round);
|
|
240
|
-
|
|
241
|
-
const vote = await this.contract.read.getVotes([round, voteCount - 1n]);
|
|
242
|
-
const decoded = decodeSlashConsensusVotes(hexToBuffer(vote));
|
|
243
|
-
const slashAmounts = await this.getSlashingAmounts();
|
|
244
|
-
return decoded
|
|
245
|
-
.map((units, i) => ({
|
|
246
|
-
validator: EthAddress.fromString(validators[i]),
|
|
247
|
-
slashAmount: slashAmounts[units - 1] ?? 0n,
|
|
248
|
-
}))
|
|
249
|
-
.filter(v => v.slashAmount > 0n);
|
|
292
|
+
return await this.getVoteAt(round, voteCount - 1n);
|
|
250
293
|
}
|
|
251
294
|
|
|
252
295
|
/**
|
|
253
296
|
* Listen for VoteCast events
|
|
254
|
-
* @param callback - Callback
|
|
297
|
+
* @param callback - Callback receiving the round, the vote's index within the round as accepted by `getVoteAt`,
|
|
298
|
+
* and the proposer that cast it
|
|
255
299
|
* @returns Unwatch function
|
|
256
300
|
*/
|
|
257
|
-
public listenToVoteCast(
|
|
301
|
+
public listenToVoteCast(
|
|
302
|
+
callback: (args: { round: bigint; voteIndex: bigint; proposer: string }) => void,
|
|
303
|
+
): () => void {
|
|
258
304
|
return this.contract.watchEvent.VoteCast(
|
|
259
305
|
{},
|
|
260
306
|
{
|
|
261
307
|
onLogs: logs => {
|
|
262
308
|
for (const log of logs) {
|
|
263
|
-
const { round, proposer } = log.args;
|
|
264
|
-
if (round !== undefined && proposer) {
|
|
265
|
-
callback({ round, proposer });
|
|
309
|
+
const { round, voteIndex, proposer } = log.args;
|
|
310
|
+
if (round !== undefined && voteIndex !== undefined && proposer) {
|
|
311
|
+
callback({ round, voteIndex, proposer });
|
|
266
312
|
}
|
|
267
313
|
}
|
|
268
314
|
},
|
|
@@ -294,6 +340,23 @@ export class SlashingProposerContract {
|
|
|
294
340
|
}
|
|
295
341
|
}
|
|
296
342
|
|
|
343
|
+
/**
|
|
344
|
+
* A validator targeted by a slashing vote, with the amount voted. The position is the validator's index in the
|
|
345
|
+
* round's flattened slash target committees — the unit the contract tallies quorum by. A validator sitting in
|
|
346
|
+
* several of the round's committees holds several positions, each with its own tally.
|
|
347
|
+
*/
|
|
348
|
+
export type SlashVoteTarget = { validator: EthAddress; slashAmount: bigint; position: number };
|
|
349
|
+
|
|
350
|
+
function decodeVote(vote: Hex, validators: EthAddress[], slashAmounts: [bigint, bigint, bigint]): SlashVoteTarget[] {
|
|
351
|
+
return decodeSlashConsensusVotes(hexToBuffer(vote))
|
|
352
|
+
.map((units, position) => ({
|
|
353
|
+
validator: validators[position],
|
|
354
|
+
slashAmount: slashAmounts[units - 1] ?? 0n,
|
|
355
|
+
position,
|
|
356
|
+
}))
|
|
357
|
+
.filter(v => v.slashAmount > 0n);
|
|
358
|
+
}
|
|
359
|
+
|
|
297
360
|
/**
|
|
298
361
|
* Decodes a Buffer containing slash votes back into an array of numbers.
|
|
299
362
|
* Each vote is represented as a 2-bit value (0, 1, 2, or 3) representing slashing units.
|
|
@@ -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';
|
|
@@ -19,14 +19,17 @@ 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
|
|
|
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
|
|
@@ -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
|
+
}
|