@aztec/ethereum 0.0.1-commit.321f6a9 → 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.
@@ -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.getInstance(),
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
- * Returns true iff the given original payload is currently the subject of a live (non-terminal)
134
- * Governance proposal. Delegates to `ReadOnlyGovernanceContract.hasActiveProposalWithPayload`, which
135
- * implements the actual sweep against the Governance contract -- this method exists only as a
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 hasActiveProposalWithPayload(payload: Hex): Promise<boolean> {
135
+ public async getPayloadProposalStatus(payload: Hex): Promise<PayloadProposalStatus> {
140
136
  const governance = await this.getGovernance();
141
- return governance.hasActiveProposalWithPayload(payload);
137
+ return governance.getPayloadProposalStatus(payload);
142
138
  }
143
139
 
144
140
  /**
@@ -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 }]);
@@ -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
- const validators = (await this.contract.simulate.getSlashTargetCommittees([round])).result.flat();
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 function to handle vote cast events
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(callback: (args: { round: bigint; proposer: string }) => void): () => void {
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.