Skip to content

Commit dd027ef

Browse files
authored
chore: Do not signal for the same payload twice or for empty payloads (#19023)
Changes: 1. Keep track of signaled payloads and reject if they are signaled twice 2. Before submitting a transaction, check ifthe payload has code Ref: A-164
2 parents dd2953a + 388208b commit dd027ef

File tree

7 files changed

+81
-4
lines changed

7 files changed

+81
-4
lines changed

yarn-project/end-to-end/src/e2e_l1_publisher/e2e_l1_publisher.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,7 @@ describe('L1Publisher integration', () => {
749749

750750
await publisher.enqueueProposeL2Block(block, CommitteeAttestationsAndSigners.empty(), Signature.empty());
751751
await publisher.enqueueGovernanceCastSignal(
752-
EthAddress.random(),
752+
l1ContractAddresses.rollupAddress,
753753
block.slot,
754754
block.timestamp,
755755
EthAddress.random(),

yarn-project/ethereum/src/contracts/empire_base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export interface IEmpireBase {
1212
getRoundInfo(
1313
rollupAddress: Hex,
1414
round: bigint,
15-
): Promise<{ lastSignalSlot: SlotNumber; payloadWithMostSignals: Hex; executed: boolean }>;
15+
): Promise<{ lastSignalSlot: SlotNumber; payloadWithMostSignals: Hex; quorumReached: boolean; executed: boolean }>;
1616
computeRound(slot: SlotNumber): Promise<bigint>;
1717
createSignalRequest(payload: Hex): L1TxRequest;
1818
createSignalRequestWithSignature(

yarn-project/ethereum/src/contracts/empire_slashing_proposer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,16 @@ export class EmpireSlashingProposerContract extends EventEmitter implements IEmp
7979
public async getRoundInfo(
8080
rollupAddress: Hex,
8181
round: bigint,
82-
): Promise<{ lastSignalSlot: SlotNumber; payloadWithMostSignals: Hex; executed: boolean }> {
82+
): Promise<{ lastSignalSlot: SlotNumber; payloadWithMostSignals: Hex; quorumReached: boolean; executed: boolean }> {
8383
const result = await this.proposer.read.getRoundData([rollupAddress, round]);
84+
const [signalCount, quorum] = await Promise.all([
85+
this.proposer.read.signalCount([rollupAddress, round, result.payloadWithMostSignals]),
86+
this.getQuorumSize(),
87+
]);
8488
return {
8589
lastSignalSlot: SlotNumber.fromBigInt(result.lastSignalSlot),
8690
payloadWithMostSignals: result.payloadWithMostSignals,
91+
quorumReached: signalCount >= quorum,
8792
executed: result.executed,
8893
};
8994
}

yarn-project/ethereum/src/contracts/governance_proposer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,16 @@ export class GovernanceProposerContract implements IEmpireBase {
6262
public async getRoundInfo(
6363
rollupAddress: Hex,
6464
round: bigint,
65-
): Promise<{ lastSignalSlot: SlotNumber; payloadWithMostSignals: Hex; executed: boolean }> {
65+
): Promise<{ lastSignalSlot: SlotNumber; payloadWithMostSignals: Hex; quorumReached: boolean; executed: boolean }> {
6666
const result = await this.proposer.read.getRoundData([rollupAddress, round]);
67+
const [signalCount, quorum] = await Promise.all([
68+
this.proposer.read.signalCount([rollupAddress, round, result.payloadWithMostSignals]),
69+
this.getQuorumSize(),
70+
]);
6771
return {
6872
lastSignalSlot: SlotNumber.fromBigInt(result.lastSignalSlot),
6973
payloadWithMostSignals: result.payloadWithMostSignals,
74+
quorumReached: signalCount >= quorum,
7075
executed: result.executed,
7176
};
7277
}

yarn-project/ethereum/src/l1_tx_utils/readonly_l1_tx_utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getKeys, merge, pick, times } from '@aztec/foundation/collection';
2+
import type { EthAddress } from '@aztec/foundation/eth-address';
23
import { type Logger, createLogger } from '@aztec/foundation/log';
34
import { makeBackoff, retry } from '@aztec/foundation/retry';
45
import { DateProvider } from '@aztec/foundation/timer';
@@ -11,6 +12,7 @@ import {
1112
type BaseError,
1213
type BlockOverrides,
1314
type ContractFunctionExecutionError,
15+
type GetCodeReturnType,
1416
type Hex,
1517
MethodNotFoundRpcError,
1618
MethodNotSupportedRpcError,
@@ -67,6 +69,10 @@ export class ReadOnlyL1TxUtils {
6769
return this.client.getBlockNumber();
6870
}
6971

72+
public getCode(address: EthAddress): Promise<GetCodeReturnType> {
73+
return this.client.getCode({ address: address.toString() });
74+
}
75+
7076
/**
7177
* Gets the current gas price with bounds checking
7278
*/

yarn-project/sequencer-client/src/publisher/sequencer-publisher.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import express, { json } from 'express';
2626
import type { Server } from 'http';
2727
import { type MockProxy, mock } from 'jest-mock-extended';
2828
import {
29+
type GetCodeReturnType,
2930
type GetTransactionReceiptReturnType,
3031
type PrivateKeyAccount,
3132
type TransactionReceipt,
@@ -109,6 +110,7 @@ describe('SequencerPublisher', () => {
109110
l1TxUtils.getBlock.mockResolvedValue({ timestamp: 12n } as any);
110111
l1TxUtils.getBlockNumber.mockResolvedValue(1n);
111112
l1TxUtils.getSenderAddress.mockReturnValue(EthAddress.fromString(testHarnessAttesterAccount.address));
113+
l1TxUtils.getCode.mockReturnValue(Promise.resolve(`0x1` as GetCodeReturnType));
112114
const config = {
113115
blobSinkUrl: BLOB_SINK_URL,
114116
l1RpcUrls: [`http://127.0.0.1:8545`],
@@ -224,6 +226,7 @@ describe('SequencerPublisher', () => {
224226
governanceProposerContract.getRoundInfo.mockResolvedValue({
225227
lastSignalSlot: SlotNumber(1),
226228
payloadWithMostSignals: govPayload.toString(),
229+
quorumReached: false,
227230
executed: false,
228231
});
229232
governanceProposerContract.createSignalRequestWithSignature.mockResolvedValue({
@@ -419,4 +422,40 @@ describe('SequencerPublisher', () => {
419422
expect(forwardSpy).not.toHaveBeenCalled();
420423
expect((publisher as any).requests.length).toEqual(0);
421424
});
425+
426+
it('does not signal for payload when quorum is reached', async () => {
427+
const { govPayload } = mockGovernancePayload();
428+
429+
governanceProposerContract.getRoundInfo.mockResolvedValue({
430+
lastSignalSlot: SlotNumber(1),
431+
payloadWithMostSignals: govPayload.toString(),
432+
quorumReached: true,
433+
executed: false,
434+
});
435+
436+
expect(
437+
await publisher.enqueueGovernanceCastSignal(
438+
govPayload,
439+
SlotNumber(2),
440+
1n,
441+
EthAddress.fromString(testHarnessAttesterAccount.address),
442+
msg => testHarnessAttesterAccount.signTypedData(msg),
443+
),
444+
).toEqual(false);
445+
});
446+
447+
it.each<GetCodeReturnType>([undefined])('does not signal for payload with empty code', async code => {
448+
const { govPayload } = mockGovernancePayload();
449+
l1TxUtils.getCode.mockReturnValue(Promise.resolve(code));
450+
451+
expect(
452+
await publisher.enqueueGovernanceCastSignal(
453+
govPayload,
454+
SlotNumber(2),
455+
1n,
456+
EthAddress.fromString(testHarnessAttesterAccount.address),
457+
msg => testHarnessAttesterAccount.signTypedData(msg),
458+
),
459+
).toEqual(false);
460+
});
422461
});

yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ export class SequencerPublisher {
109109

110110
protected lastActions: Partial<Record<Action, SlotNumber>> = {};
111111

112+
private isPayloadEmptyCache: Map<string, boolean> = new Map<string, boolean>();
113+
112114
protected log: Logger;
113115
protected ethereumSlotDuration: bigint;
114116

@@ -661,10 +663,19 @@ export class SequencerPublisher {
661663
const round = await base.computeRound(slotNumber);
662664
const roundInfo = await base.getRoundInfo(this.rollupContract.address, round);
663665

666+
if (roundInfo.quorumReached) {
667+
return false;
668+
}
669+
664670
if (roundInfo.lastSignalSlot >= slotNumber) {
665671
return false;
666672
}
667673

674+
if (await this.isPayloadEmpty(payload)) {
675+
this.log.warn(`Skipping vote cast for payload with empty code`);
676+
return false;
677+
}
678+
668679
const cachedLastVote = this.lastActions[signalType];
669680
this.lastActions[signalType] = slotNumber;
670681
const action = signalType;
@@ -724,6 +735,17 @@ export class SequencerPublisher {
724735
return true;
725736
}
726737

738+
private async isPayloadEmpty(payload: EthAddress): Promise<boolean> {
739+
const key = payload.toString();
740+
const cached = this.isPayloadEmptyCache.get(key);
741+
if (cached) {
742+
return cached;
743+
}
744+
const isEmpty = !(await this.l1TxUtils.getCode(payload));
745+
this.isPayloadEmptyCache.set(key, isEmpty);
746+
return isEmpty;
747+
}
748+
727749
/**
728750
* Enqueues a governance castSignal transaction to cast a signal for a given slot number.
729751
* @param slotNumber - The slot number to cast a signal for.

0 commit comments

Comments
 (0)