@aztec/ethereum 0.0.1-commit.3100065 → 0.0.1-commit.330febf
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/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 +5 -1
- package/dest/contracts/gse.d.ts.map +1 -1
- package/dest/contracts/gse.js +9 -0
- 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 +9 -1
- package/dest/contracts/rollup.d.ts.map +1 -1
- package/dest/contracts/rollup.js +8 -0
- 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 +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_artifacts.d.ts +24 -19
- package/dest/l1_artifacts.d.ts.map +1 -1
- 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/governance.ts +63 -20
- package/src/contracts/governance_proposer.ts +11 -15
- package/src/contracts/gse.ts +12 -0
- package/src/contracts/multicall.ts +8 -5
- package/src/contracts/rollup.ts +10 -0
- package/src/contracts/slashing_proposer.ts +78 -15
- 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
|
@@ -37,7 +37,7 @@ export type L1GovernanceContractAddresses = Pick<
|
|
|
37
37
|
'governanceAddress' | 'rollupAddress' | 'registryAddress' | 'governanceProposerAddress'
|
|
38
38
|
>;
|
|
39
39
|
|
|
40
|
-
// NOTE: Must be kept in sync with
|
|
40
|
+
// NOTE: Must be kept in sync with IGovernance.ProposalState in l1-contracts
|
|
41
41
|
export enum ProposalState {
|
|
42
42
|
Pending,
|
|
43
43
|
Active,
|
|
@@ -45,10 +45,21 @@ export enum ProposalState {
|
|
|
45
45
|
Executable,
|
|
46
46
|
Rejected,
|
|
47
47
|
Executed,
|
|
48
|
+
Droppable,
|
|
48
49
|
Dropped,
|
|
49
50
|
Expired,
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Outcome of {@link ReadOnlyGovernanceContract.getPayloadProposalStatus} for a queried payload.
|
|
55
|
+
* - `'live'`: a proposal referencing the payload is still progressing (Pending/Active/Queued/
|
|
56
|
+
* Executable) or is `Droppable`, so signalling for it again is redundant or premature.
|
|
57
|
+
* - `'executed'`: no live proposal references the payload, but one was already executed within the
|
|
58
|
+
* bounded lookback (or a prior sweep observed the execution and memoized it).
|
|
59
|
+
* - `'none'`: no live or executed proposal references the payload within the bounded lookback.
|
|
60
|
+
*/
|
|
61
|
+
export type PayloadProposalStatus = 'live' | 'executed' | 'none';
|
|
62
|
+
|
|
52
63
|
/** Vote tallies on a single proposal. Both fields are mutated by `Governance.vote`. */
|
|
53
64
|
export interface Ballot {
|
|
54
65
|
yea: bigint;
|
|
@@ -119,6 +130,17 @@ const TERMINAL_PROPOSAL_STATES: ReadonlySet<ProposalState> = new Set([
|
|
|
119
130
|
ProposalState.Expired,
|
|
120
131
|
]);
|
|
121
132
|
|
|
133
|
+
// Set of `ProposalState` values in which a proposal is still progressing towards execution.
|
|
134
|
+
// `Droppable` is deliberately excluded: it is neither live (it cannot progress on its own) nor
|
|
135
|
+
// terminal (it can resume its lifecycle if the governanceProposer is restored), so it is handled
|
|
136
|
+
// separately and is never memoized as immutable.
|
|
137
|
+
const LIVE_PROPOSAL_STATES: ReadonlySet<ProposalState> = new Set([
|
|
138
|
+
ProposalState.Pending,
|
|
139
|
+
ProposalState.Active,
|
|
140
|
+
ProposalState.Queued,
|
|
141
|
+
ProposalState.Executable,
|
|
142
|
+
]);
|
|
143
|
+
|
|
122
144
|
// Hard upper bound on the wall-clock lifetime of any Governance proposal, in seconds.
|
|
123
145
|
// Each proposal stores its own snapshot of `ProposalConfiguration` at creation time and progresses
|
|
124
146
|
// through Pending -> Active -> Queued -> Executable using those frozen durations
|
|
@@ -173,6 +195,14 @@ export class ReadOnlyGovernanceContract {
|
|
|
173
195
|
*/
|
|
174
196
|
private readonly originalPayloadCache: Map<Hex, Hex | undefined> = new Map();
|
|
175
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Payloads (lowercased hex) observed as the subject of an `Executed` Governance proposal. Execution
|
|
200
|
+
* is immutable on-chain, so this verdict never expires within a process. It restores the
|
|
201
|
+
* `'executed'` classification for payloads whose executed proposal has since aged past the bounded
|
|
202
|
+
* lookback below. Lost on restart, at which point the bounded-lookback limitation applies again.
|
|
203
|
+
*/
|
|
204
|
+
private readonly executedPayloads: Set<string> = new Set();
|
|
205
|
+
|
|
176
206
|
constructor(
|
|
177
207
|
address: Hex,
|
|
178
208
|
public readonly client: ViemClient,
|
|
@@ -263,30 +293,38 @@ export class ReadOnlyGovernanceContract {
|
|
|
263
293
|
}
|
|
264
294
|
|
|
265
295
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
296
|
+
* Classifies the given original payload against the Governance proposal history. Distinguishes a
|
|
297
|
+
* payload that is still the subject of a live proposal (`'live'`) from one whose proposal was
|
|
298
|
+
* already executed (`'executed'`) from one that has no relevant proposal (`'none'`), so callers can
|
|
299
|
+
* stop re-signalling an executed payload while still re-signalling one whose proposal was merely
|
|
300
|
+
* rejected/dropped/expired (each GovernanceProposer round is independent and there is no
|
|
301
|
+
* payload-level uniqueness check on-chain).
|
|
302
|
+
*
|
|
303
|
+
* A proposal matches the payload either directly (its stored `payload` equals the target, as for
|
|
304
|
+
* `proposeWithLock` proposals) or via its `GSEPayload` wrapper unwrapping to the target. `'live'`
|
|
305
|
+
* (including `Droppable`) takes precedence over `'executed'`, so a payload re-submitted while a
|
|
306
|
+
* prior execution is still in the lookback window reads as `'live'`.
|
|
272
307
|
*
|
|
273
308
|
* Implemented as a bounded view-call sweep over `Governance.proposals` rather than an event scan,
|
|
274
309
|
* because `eth_getLogs` over the full deployment history of a long-lived rollup exceeds typical
|
|
275
310
|
* RPC block-range caps. The number of proposals (`proposalCount`) is small in practice, and we
|
|
276
|
-
* walk newest -> oldest with a hard early-stop on the protocol-wide proposal lifetime cap.
|
|
311
|
+
* walk newest -> oldest with a hard early-stop on the protocol-wide proposal lifetime cap. This
|
|
312
|
+
* makes the `'executed'` verdict a *bounded lookback* rather than permanent suppression: a proposal
|
|
313
|
+
* executed longer ago than the lifetime cap is only reported once it has been observed and
|
|
314
|
+
* memoized in-process (memo is lost on restart).
|
|
277
315
|
*/
|
|
278
|
-
public async
|
|
316
|
+
public async getPayloadProposalStatus(payload: Hex): Promise<PayloadProposalStatus> {
|
|
317
|
+
const target = payload.toLowerCase() as Hex;
|
|
318
|
+
|
|
279
319
|
const proposalCount = await this.getProposalCount();
|
|
280
320
|
if (proposalCount === 0n) {
|
|
281
|
-
return
|
|
321
|
+
return this.executedPayloads.has(target) ? 'executed' : 'none';
|
|
282
322
|
}
|
|
283
323
|
|
|
284
324
|
// Anything created before this cutoff is guaranteed terminal regardless of its frozen config.
|
|
285
325
|
const block = await this.client.getBlock();
|
|
286
326
|
const hardCutoff = block.timestamp - MAX_PROPOSAL_LIFETIME_SECONDS;
|
|
287
327
|
|
|
288
|
-
const target = payload.toLowerCase() as Hex;
|
|
289
|
-
|
|
290
328
|
// Proposals are append-only with monotonically non-decreasing creation timestamps, so iterating
|
|
291
329
|
// from newest -> oldest lets us early-stop as soon as we cross the lifetime cutoff.
|
|
292
330
|
for (let id = proposalCount - 1n; id >= 0n; id--) {
|
|
@@ -294,24 +332,29 @@ export class ReadOnlyGovernanceContract {
|
|
|
294
332
|
|
|
295
333
|
// Hard early-stop: every older proposal is also older than the cutoff and therefore terminal.
|
|
296
334
|
if (proposal.creation < hardCutoff) {
|
|
297
|
-
|
|
335
|
+
break;
|
|
298
336
|
}
|
|
299
337
|
|
|
338
|
+
const proposalPayload = proposal.payload.toString().toLowerCase();
|
|
300
339
|
const original = await this.getOriginalPayload(proposal.payload);
|
|
301
|
-
|
|
340
|
+
const matches = proposalPayload === target || (original !== undefined && original.toLowerCase() === target);
|
|
341
|
+
if (!matches) {
|
|
302
342
|
continue;
|
|
303
343
|
}
|
|
304
344
|
|
|
305
|
-
//
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
345
|
+
// A live or Droppable proposal blocks re-signalling and wins over any executed one.
|
|
346
|
+
if (LIVE_PROPOSAL_STATES.has(proposal.state) || proposal.state === ProposalState.Droppable) {
|
|
347
|
+
return 'live';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (proposal.state === ProposalState.Executed) {
|
|
351
|
+
this.executedPayloads.add(target);
|
|
309
352
|
}
|
|
310
353
|
|
|
311
|
-
|
|
354
|
+
// Rejected/Dropped/Expired proposals allow re-proposing the same payload; keep scanning.
|
|
312
355
|
}
|
|
313
356
|
|
|
314
|
-
return
|
|
357
|
+
return this.executedPayloads.has(target) ? 'executed' : 'none';
|
|
315
358
|
}
|
|
316
359
|
|
|
317
360
|
/**
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import type { L1TxRequest, L1TxUtils } from '../l1_tx_utils/index.js';
|
|
16
16
|
import type { ViemClient } from '../types.js';
|
|
17
17
|
import { type IEmpireBase, encodeSignal, encodeSignalWithSignature, signSignalWithSig } from './empire_base.js';
|
|
18
|
-
import { ReadOnlyGovernanceContract, extractProposalIdFromLogs } from './governance.js';
|
|
18
|
+
import { type PayloadProposalStatus, ReadOnlyGovernanceContract, extractProposalIdFromLogs } from './governance.js';
|
|
19
19
|
|
|
20
20
|
export class GovernanceProposerContract implements IEmpireBase {
|
|
21
21
|
private readonly proposer: GetContractReturnType<typeof GovernanceProposerAbi, ViemClient>;
|
|
@@ -38,16 +38,16 @@ export class GovernanceProposerContract implements IEmpireBase {
|
|
|
38
38
|
this.proposer = getContract({ address, abi: GovernanceProposerAbi, client });
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
public get address() {
|
|
41
|
+
public get address(): EthAddress {
|
|
42
42
|
return EthAddress.fromString(this.proposer.address);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
public async getRollupAddress() {
|
|
45
|
+
public async getRollupAddress(): Promise<EthAddress> {
|
|
46
46
|
return EthAddress.fromString(await this.proposer.read.getInstance());
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
@memoize
|
|
50
|
-
public async getRegistryAddress() {
|
|
50
|
+
public async getRegistryAddress(): Promise<EthAddress> {
|
|
51
51
|
return EthAddress.fromString(await this.proposer.read.REGISTRY());
|
|
52
52
|
}
|
|
53
53
|
|
|
@@ -59,10 +59,6 @@ export class GovernanceProposerContract implements IEmpireBase {
|
|
|
59
59
|
return this.proposer.read.ROUND_SIZE();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
public getInstance() {
|
|
63
|
-
return this.proposer.read.getInstance();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
62
|
public computeRound(slot: SlotNumber): Promise<bigint> {
|
|
67
63
|
return this.proposer.read.computeRound([BigInt(slot)]);
|
|
68
64
|
}
|
|
@@ -107,7 +103,7 @@ export class GovernanceProposerContract implements IEmpireBase {
|
|
|
107
103
|
signer,
|
|
108
104
|
payload,
|
|
109
105
|
slot,
|
|
110
|
-
await this.
|
|
106
|
+
(await this.getRollupAddress()).toString(),
|
|
111
107
|
this.address.toString(),
|
|
112
108
|
chainId,
|
|
113
109
|
);
|
|
@@ -130,15 +126,15 @@ export class GovernanceProposerContract implements IEmpireBase {
|
|
|
130
126
|
}
|
|
131
127
|
|
|
132
128
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* implements the actual sweep against the Governance contract -- this method exists only as
|
|
136
|
-
* convenience wrapper so callers that already hold a GovernanceProposer reference don't have to
|
|
129
|
+
* Classifies the given original payload against the Governance proposal history (`'live'` /
|
|
130
|
+
* `'executed'` / `'none'`). Delegates to `ReadOnlyGovernanceContract.getPayloadProposalStatus`,
|
|
131
|
+
* which implements the actual sweep against the Governance contract -- this method exists only as
|
|
132
|
+
* a convenience wrapper so callers that already hold a GovernanceProposer reference don't have to
|
|
137
133
|
* resolve the Governance address themselves.
|
|
138
134
|
*/
|
|
139
|
-
public async
|
|
135
|
+
public async getPayloadProposalStatus(payload: Hex): Promise<PayloadProposalStatus> {
|
|
140
136
|
const governance = await this.getGovernance();
|
|
141
|
-
return governance.
|
|
137
|
+
return governance.getPayloadProposalStatus(payload);
|
|
142
138
|
}
|
|
143
139
|
|
|
144
140
|
/**
|
package/src/contracts/gse.ts
CHANGED
|
@@ -59,6 +59,18 @@ export class GSEContract {
|
|
|
59
59
|
return this.gse.read.getAttestersFromIndicesAtTime([instance, ts, indices], options);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Number of attesters registered to `instance` as of the given L1 timestamp. */
|
|
63
|
+
public async getAttesterCountAtTime(
|
|
64
|
+
instance: Hex | EthAddress,
|
|
65
|
+
ts: bigint,
|
|
66
|
+
options?: { blockNumber?: bigint },
|
|
67
|
+
): Promise<number> {
|
|
68
|
+
if (instance instanceof EthAddress) {
|
|
69
|
+
instance = instance.toString();
|
|
70
|
+
}
|
|
71
|
+
return Number(await this.gse.read.getAttesterCountAtTime([instance, ts], options));
|
|
72
|
+
}
|
|
73
|
+
|
|
62
74
|
public async getRegistrationDigest(publicKey: ProjPointType<bigint>): Promise<ProjPointType<bigint>> {
|
|
63
75
|
const affinePublicKey = publicKey.toAffine();
|
|
64
76
|
const g1PointDigest = await this.gse.read.getRegistrationDigest([{ x: affinePublicKey.x, y: affinePublicKey.y }]);
|
|
@@ -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
|
@@ -560,6 +560,16 @@ export class RollupContract {
|
|
|
560
560
|
return Number(await this.rollup.read.getActiveAttesterCount(options));
|
|
561
561
|
}
|
|
562
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);
|
|
571
|
+
}
|
|
572
|
+
|
|
563
573
|
public async getSlashingProposerAddress() {
|
|
564
574
|
const slasher = await this.getSlasherContract();
|
|
565
575
|
if (!slasher) {
|
|
@@ -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';
|
|
@@ -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
|
+
}
|