Skip to content

Commit 620c91e

Browse files
author
AztecBot
committed
Merge branch 'next' into merge-train/barretenberg
2 parents 3d444d4 + 2e62509 commit 620c91e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+765
-540
lines changed

l1-contracts/src/core/RollupCore.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,10 @@ contract RollupCore is EIP712("Aztec Rollup", "1"), Ownable, IStakingCore, IVali
237237

238238
// We call one external library or another based on the slasher flavor
239239
// This allows us to keep the slash flavors in separate external libraries so we do not exceed max contract size
240-
if (_config.slasherFlavor == SlasherFlavor.TALLY) {
240+
// Note that we do not deploy a slasher if we run with no committees (i.e. targetCommitteeSize == 0)
241+
if (_config.targetCommitteeSize == 0) {
242+
slasher = ISlasher(address(0));
243+
} else if (_config.slasherFlavor == SlasherFlavor.TALLY) {
241244
slasher = TallySlasherDeploymentExtLib.deployTallySlasher(
242245
address(this),
243246
_config.slashingVetoer,

l1-contracts/src/core/slashing/TallySlashingProposer.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ contract TallySlashingProposer is EIP712 {
144144
* @notice EIP-712 type hash for the Vote struct used in signature verification
145145
* @dev Defines the structure: Vote(uint256 slot,bytes votes) for EIP-712 signing
146146
*/
147-
bytes32 public constant VOTE_TYPEHASH = keccak256("Vote(uint256 slot,bytes votes)");
147+
bytes32 public constant VOTE_TYPEHASH = keccak256("Vote(bytes votes,uint256 slot)");
148148

149149
/**
150150
* @notice Type of slashing proposer (either Tally or Empire)

l1-contracts/test/benchmark/happy.t.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase {
135135
bool internal IS_IGNITION;
136136

137137
Rollup internal rollup;
138+
Slasher internal slasher;
138139

139140
address internal coinbase = address(bytes20("MONEY MAKER"));
140141
TestERC20 internal asset;
@@ -193,7 +194,8 @@ contract BenchmarkRollupTest is FeeModelTestPoints, DecoderBase {
193194

194195
asset = builder.getConfig().testERC20;
195196
rollup = builder.getConfig().rollup;
196-
slashingProposer = Slasher(rollup.getSlasher()).PROPOSER();
197+
slasher = Slasher(rollup.getSlasher());
198+
slashingProposer = address(slasher) == address(0) ? address(0) : slasher.PROPOSER();
197199

198200
SlashFactory slashFactory = new SlashFactory(IValidatorSelection(address(rollup)));
199201
address[] memory toSlash = new address[](0);

yarn-project/aztec-node/src/sentinel/sentinel.test.ts

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -463,60 +463,6 @@ describe('sentinel', () => {
463463
},
464464
] satisfies WantToSlashArgs[]);
465465
});
466-
467-
it('should agree with slash', async () => {
468-
const performance = Object.fromEntries(
469-
Array.from({ length: 10 }, (_, i) => [
470-
`0x000000000000000000000000000000000000000${i}`,
471-
{
472-
missed: i * 10,
473-
total: 100,
474-
},
475-
]),
476-
);
477-
478-
await sentinel.updateProvenPerformance(1n, performance);
479-
const emitSpy = jest.spyOn(sentinel, 'emit');
480-
481-
sentinel.handleProvenPerformance(1n, performance);
482-
const penalty = config.slashInactivityCreatePenalty;
483-
484-
expect(emitSpy).toHaveBeenCalledWith(WANT_TO_SLASH_EVENT, [
485-
{
486-
validator: EthAddress.fromString(`0x0000000000000000000000000000000000000008`),
487-
amount: penalty,
488-
offenseType: OffenseType.INACTIVITY,
489-
epochOrSlot: 1n,
490-
},
491-
{
492-
validator: EthAddress.fromString(`0x0000000000000000000000000000000000000009`),
493-
amount: penalty,
494-
offenseType: OffenseType.INACTIVITY,
495-
epochOrSlot: 1n,
496-
},
497-
] satisfies WantToSlashArgs[]);
498-
499-
for (let i = 0; i < 10; i++) {
500-
const expectedAgree = i >= 6;
501-
const actualAgree = await sentinel.shouldSlash({
502-
validator: EthAddress.fromString(`0x000000000000000000000000000000000000000${i}`),
503-
amount: config.slashInactivityMaxPenalty,
504-
offenseType: OffenseType.INACTIVITY,
505-
epochOrSlot: 1n,
506-
});
507-
expect(actualAgree).toBe(expectedAgree);
508-
509-
// We never slash if the penalty is above the max penalty
510-
await expect(
511-
sentinel.shouldSlash({
512-
validator: EthAddress.fromString(`0x000000000000000000000000000000000000000${i}`),
513-
amount: config.slashInactivityMaxPenalty + 1n,
514-
offenseType: OffenseType.INACTIVITY,
515-
epochOrSlot: 1n,
516-
}),
517-
).resolves.toBe(false);
518-
}
519-
});
520466
});
521467
});
522468

yarn-project/aztec-node/src/sentinel/sentinel.ts

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@ import { createLogger } from '@aztec/foundation/log';
55
import { RunningPromise } from '@aztec/foundation/running-promise';
66
import { L2TipsMemoryStore, type L2TipsStore } from '@aztec/kv-store/stores';
77
import type { P2PClient } from '@aztec/p2p';
8-
import { OffenseType } from '@aztec/slasher';
9-
import { WANT_TO_SLASH_EVENT, type WantToSlashArgs, type Watcher, type WatcherEmitter } from '@aztec/slasher';
8+
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
109
import type { SlasherConfig } from '@aztec/slasher/config';
1110
import {
1211
type L2BlockSource,
@@ -121,9 +120,9 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
121120
}
122121

123122
const epoch = getEpochAtSlot(block.header.getSlot(), await this.archiver.getL1Constants());
124-
this.logger.info(`Computing proven performance for epoch ${epoch}`);
123+
this.logger.debug(`Computing proven performance for epoch ${epoch}`);
125124
const performance = await this.computeProvenPerformance(epoch);
126-
this.logger.info(`Proven performance for epoch ${epoch}`, performance);
125+
this.logger.info(`Computed proven performance for epoch ${epoch}`, performance);
127126

128127
await this.updateProvenPerformance(epoch, performance);
129128
this.handleProvenPerformance(epoch, performance);
@@ -183,38 +182,12 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
183182
epochOrSlot: epoch,
184183
}));
185184

186-
this.logger.info(`Criminals: ${criminals.length}`, { args });
187-
188185
if (criminals.length > 0) {
186+
this.logger.info(`Identified ${criminals.length} validators to slash due to inactivity`, { args });
189187
this.emit(WANT_TO_SLASH_EVENT, args);
190188
}
191189
}
192190

193-
public async shouldSlash({ validator, amount }: WantToSlashArgs): Promise<boolean> {
194-
const l1Constants = this.epochCache.getL1Constants();
195-
const ttlL2Slots = this.config.slashPayloadTtlSeconds / l1Constants.slotDuration;
196-
const ttlEpochs = BigInt(Math.ceil(ttlL2Slots / l1Constants.epochDuration));
197-
198-
const currentEpoch = this.epochCache.getEpochAndSlotNow().epoch;
199-
const performance = await this.store.getProvenPerformance(validator);
200-
const isCriminal =
201-
performance
202-
.filter(p => p.epoch >= currentEpoch - ttlEpochs)
203-
.findIndex(p => p.missed / p.total >= this.config.slashInactivitySignalTargetPercentage) !== -1;
204-
if (isCriminal) {
205-
if (amount <= this.config.slashInactivityMaxPenalty) {
206-
return true;
207-
} else {
208-
this.logger.warn(`Validator ${validator} is a criminal but the penalty is too high`, {
209-
amount,
210-
maxPenalty: this.config.slashInactivityMaxPenalty,
211-
});
212-
return false;
213-
}
214-
}
215-
return false;
216-
}
217-
218191
/**
219192
* Process data for two L2 slots ago.
220193
* Note that we do not process historical data, since we rely on p2p data for processing,

yarn-project/aztec/src/cli/chain_l2_config.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,10 @@ export const alphaTestnetL2ChainConfig: L2ChainConfig = {
183183
activationThreshold: DefaultL1ContractsConfig.activationThreshold,
184184
/** The minimum stake for a validator. */
185185
ejectionThreshold: DefaultL1ContractsConfig.ejectionThreshold,
186-
/** The slashing quorum */
187-
slashingQuorum: 101,
188186
/** The slashing round size */
189-
slashingRoundSize: 200,
187+
slashingRoundSize: 32 * 6, // 6 epochs
188+
/** The slashing quorum */
189+
slashingQuorum: (32 * 6) / 2 + 1, // 6 epochs, majority of validators
190190
/** Governance proposing quorum */
191191
governanceProposerQuorum: 151,
192192
/** Governance proposing round size */

yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import path from 'path';
88
import { shouldCollectMetrics } from '../fixtures/fixtures.js';
99
import { createNodes } from '../fixtures/setup_p2p_test.js';
1010
import { P2PNetworkTest } from './p2p_network.js';
11-
import { awaitCommitteeExists, awaitCommitteeKicked } from './shared.js';
11+
import { awaitCommitteeExists, awaitCommitteeKicked, awaitOffenseDetected } from './shared.js';
1212

1313
jest.setTimeout(1000000);
1414

@@ -37,6 +37,7 @@ describe('e2e_p2p_data_withholding_slash', () => {
3737
let t: P2PNetworkTest;
3838
let nodes: AztecNodeService[];
3939

40+
const slashingUnit = BigInt(20e18);
4041
const slashingQuorum = 3;
4142
const slashingRoundSize = 4;
4243
// This test needs longer slot window to ensure that the client has enough time to submit their txs,
@@ -58,6 +59,7 @@ describe('e2e_p2p_data_withholding_slash', () => {
5859
aztecProofSubmissionEpochs: 0, // effectively forces instant reorgs
5960
slashingQuorum,
6061
slashingRoundSize,
62+
slashingUnit,
6163
minTxsPerBlock: 0,
6264
},
6365
});
@@ -86,14 +88,18 @@ describe('e2e_p2p_data_withholding_slash', () => {
8688
const { rollup, slashingProposer, slashFactory } = await t.getContracts();
8789

8890
// Jump forward to an epoch in the future such that the validator set is not empty
89-
{
90-
const newTime = await t.ctx.cheatCodes.rollup.advanceToEpoch(4n);
91-
t.ctx.dateProvider.setTime(Number(newTime * 1000n));
92-
// Send tx
93-
await debugRollup();
94-
}
91+
await t.ctx.cheatCodes.rollup.advanceToEpoch(4n, { updateDateProvider: t.ctx.dateProvider });
92+
await debugRollup();
93+
94+
const [activationThreshold, ejectionThreshold] = await Promise.all([
95+
rollup.getActivationThreshold(),
96+
rollup.getEjectionThreshold(),
97+
]);
98+
99+
// Slashing amount should be enough to kick validators out
100+
const slashingAmount = slashingUnit * 3n;
101+
expect(activationThreshold - slashingAmount).toBeLessThan(ejectionThreshold);
95102

96-
const slashingAmount = (await rollup.getActivationThreshold()) - (await rollup.getEjectionThreshold()) + 1n;
97103
t.ctx.aztecNodeConfig.slashPruneEnabled = true;
98104
t.ctx.aztecNodeConfig.slashPrunePenalty = slashingAmount;
99105
t.ctx.aztecNodeConfig.slashPruneMaxPenalty = slashingAmount;
@@ -123,13 +129,9 @@ describe('e2e_p2p_data_withholding_slash', () => {
123129
// Considering the slot duration is 32 seconds,
124130
// Considering the epoch duration is 2 slots,
125131
// we have ~64 seconds to do this.
126-
{
127-
const newTime = await t.ctx.cheatCodes.rollup.advanceToEpoch(8n);
128-
t.ctx.dateProvider.setTime(Number(newTime * 1000n));
129-
// Send L1 tx
130-
await t.sendDummyTx();
131-
await debugRollup();
132-
}
132+
await t.ctx.cheatCodes.rollup.advanceToEpoch(8n, { updateDateProvider: t.ctx.dateProvider });
133+
await t.sendDummyTx();
134+
await debugRollup();
133135

134136
// Send Aztec txs
135137
t.logger.info('Setup account');
@@ -145,8 +147,7 @@ describe('e2e_p2p_data_withholding_slash', () => {
145147
}
146148

147149
// Re-create the nodes.
148-
// ASSUMING they sync in the middle of the epoch, they will "see" the reorg,
149-
// and try to slash.
150+
// ASSUMING they sync in the middle of the epoch, they will "see" the reorg, and try to slash.
150151
t.logger.info('Re-creating nodes');
151152
nodes = await createNodes(
152153
t.ctx.aztecNodeConfig,
@@ -158,6 +159,13 @@ describe('e2e_p2p_data_withholding_slash', () => {
158159
DATA_DIR,
159160
);
160161

162+
await awaitOffenseDetected({
163+
epochDuration: t.ctx.aztecNodeConfig.aztecEpochDuration,
164+
logger: t.logger,
165+
nodeAdmin: nodes[0],
166+
slashingRoundSize,
167+
});
168+
161169
await awaitCommitteeKicked({
162170
rollup,
163171
cheatCodes: t.ctx.cheatCodes.rollup,
@@ -167,7 +175,7 @@ describe('e2e_p2p_data_withholding_slash', () => {
167175
slashingRoundSize,
168176
aztecSlotDuration,
169177
logger: t.logger,
170-
sendDummyTx: () => t.sendDummyTx().then(() => undefined),
178+
dateProvider: t.ctx.dateProvider,
171179
});
172180
});
173181
});
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { AztecNodeService } from '@aztec/aztec-node';
2+
import { EthAddress } from '@aztec/aztec.js';
3+
import { RollupContract } from '@aztec/ethereum';
4+
import { promiseWithResolvers } from '@aztec/foundation/promise';
5+
6+
import { jest } from '@jest/globals';
7+
import fs from 'fs';
8+
import 'jest-extended';
9+
import os from 'os';
10+
import path from 'path';
11+
12+
import { createNodes } from '../fixtures/setup_p2p_test.js';
13+
import { P2PNetworkTest } from './p2p_network.js';
14+
15+
const NUM_NODES = 5;
16+
const NUM_VALIDATORS = NUM_NODES + 1; // We create an extra validator, who will not have a running node
17+
const BOOT_NODE_UDP_PORT = 4500;
18+
const EPOCH_DURATION = 2;
19+
const SLASHING_QUORUM = 3;
20+
const SLASHING_ROUND_SIZE = 4;
21+
const ETHEREUM_SLOT_DURATION = 4;
22+
const AZTEC_SLOT_DURATION = 8;
23+
const SLASHING_UNIT = BigInt(20e18);
24+
const SLASHING_AMOUNT = SLASHING_UNIT * 3n;
25+
26+
const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'inactivity-slash-'));
27+
28+
jest.setTimeout(1000 * 60 * 10);
29+
30+
describe('e2e_p2p_inactivity_slash', () => {
31+
let t: P2PNetworkTest;
32+
let nodes: AztecNodeService[];
33+
let rollup: RollupContract;
34+
let offlineValidator: EthAddress;
35+
36+
beforeAll(async () => {
37+
t = await P2PNetworkTest.create({
38+
testName: 'e2e_p2p_inactivity_slash',
39+
numberOfNodes: 0,
40+
numberOfValidators: NUM_VALIDATORS,
41+
basePort: BOOT_NODE_UDP_PORT,
42+
startProverNode: true,
43+
initialConfig: {
44+
aztecTargetCommitteeSize: NUM_NODES, // ensure we can progress even after slash happens
45+
aztecSlotDuration: AZTEC_SLOT_DURATION,
46+
ethereumSlotDuration: ETHEREUM_SLOT_DURATION,
47+
aztecProofSubmissionEpochs: 1024, // effectively do not reorg
48+
listenAddress: '127.0.0.1',
49+
minTxsPerBlock: 0,
50+
aztecEpochDuration: EPOCH_DURATION,
51+
validatorReexecute: false,
52+
sentinelEnabled: true,
53+
slashingQuorum: SLASHING_QUORUM,
54+
slashingRoundSize: SLASHING_ROUND_SIZE,
55+
slashInactivityCreateTargetPercentage: 0.5,
56+
slashInactivitySignalTargetPercentage: 0.1,
57+
slashingUnit: SLASHING_UNIT,
58+
},
59+
});
60+
61+
await t.applyBaseSnapshots();
62+
await t.setup();
63+
64+
// Set slashing penalties for inactivity
65+
({ rollup } = await t.getContracts());
66+
const [activationThreshold, ejectionThreshold] = await Promise.all([
67+
rollup.getActivationThreshold(),
68+
rollup.getEjectionThreshold(),
69+
]);
70+
expect(activationThreshold - SLASHING_AMOUNT).toBeLessThan(ejectionThreshold);
71+
t.ctx.aztecNodeConfig.slashInactivityEnabled = true;
72+
t.ctx.aztecNodeConfig.slashInactivityCreatePenalty = SLASHING_AMOUNT;
73+
t.ctx.aztecNodeConfig.slashInactivityMaxPenalty = SLASHING_AMOUNT;
74+
75+
nodes = await createNodes(
76+
t.ctx.aztecNodeConfig,
77+
t.ctx.dateProvider,
78+
t.bootstrapNodeEnr,
79+
NUM_NODES, // Note we do not create the last validator yet, so it shows as offline
80+
BOOT_NODE_UDP_PORT,
81+
t.prefilledPublicData,
82+
83+
DATA_DIR,
84+
);
85+
await t.removeInitialNode();
86+
87+
offlineValidator = t.validators.at(-1)!.attester;
88+
t.logger.warn(`Setup complete. Offline validator is ${offlineValidator}.`, {
89+
validators: t.validators,
90+
offlineValidator,
91+
});
92+
});
93+
94+
afterAll(async () => {
95+
await t.stopNodes(nodes);
96+
await t.teardown();
97+
for (let i = 0; i < NUM_NODES; i++) {
98+
fs.rmSync(`${DATA_DIR}-${i}`, { recursive: true, force: true, maxRetries: 3 });
99+
}
100+
});
101+
102+
it('slashes inactive validator', async () => {
103+
const slashPromise = promiseWithResolvers<bigint>();
104+
rollup.listenToSlash(args => {
105+
t.logger.warn(`Slashed ${args.attester.toString()}`);
106+
expect(offlineValidator.toString()).toEqual(args.attester.toString());
107+
expect(args.amount).toEqual(SLASHING_AMOUNT);
108+
slashPromise.resolve(args.amount);
109+
});
110+
await slashPromise.promise;
111+
});
112+
});

0 commit comments

Comments
 (0)