Skip to content

Commit 50d1b0f

Browse files
committed
chore: Add e2e test for inactivity slash with consecutive epochs
Equivalent to existing inactivity slash test, but requires N consecutive epochs of inactivity to slash.
1 parent ba13aa6 commit 50d1b0f

File tree

19 files changed

+294
-119
lines changed

19 files changed

+294
-119
lines changed

l1-contracts/test/governance/governance/vote.t.sol

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ contract VoteTest is GovernanceBase {
9797

9898
modifier givenStateIsActive(address _voter, uint256 _depositPower) {
9999
vm.assume(_voter != address(0));
100+
vm.assume(_voter != address(governance));
100101
depositPower = bound(_depositPower, 1, type(uint128).max);
101102

102103
token.mint(_voter, depositPower);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,8 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
416416
if (!options.dontStartSequencer && sequencer) {
417417
await sequencer.start();
418418
log.verbose(`Sequencer started`);
419+
} else if (sequencer) {
420+
log.warn(`Sequencer created but not started`);
419421
}
420422

421423
return new AztecNodeService(
Lines changed: 17 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,37 @@
1-
import type { AztecNodeService } from '@aztec/aztec-node';
2-
import { EthAddress } from '@aztec/aztec.js';
3-
import { RollupContract } from '@aztec/ethereum';
1+
import type { EthAddress } from '@aztec/aztec.js';
42
import { promiseWithResolvers } from '@aztec/foundation/promise';
53

64
import { jest } from '@jest/globals';
7-
import fs from 'fs';
85
import 'jest-extended';
9-
import os from 'os';
10-
import path from 'path';
116

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(1e18);
24-
const SLASHING_AMOUNT = SLASHING_UNIT * 3n;
25-
26-
const DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'inactivity-slash-'));
7+
import { P2PInactivityTest } from './inactivity_slash_test.js';
278

289
jest.setTimeout(1000 * 60 * 10);
2910

11+
const SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD = 1;
12+
3013
describe('e2e_p2p_inactivity_slash', () => {
31-
let t: P2PNetworkTest;
32-
let nodes: AztecNodeService[];
33-
let rollup: RollupContract;
34-
let offlineValidator: EthAddress;
14+
let test: P2PInactivityTest;
3515

3616
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_VALIDATORS,
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-
slashingRoundSizeInEpochs: SLASHING_ROUND_SIZE / EPOCH_DURATION,
55-
slashInactivityTargetPercentage: 0.5,
56-
slashAmountSmall: SLASHING_UNIT,
57-
slashAmountMedium: SLASHING_UNIT * 2n,
58-
slashAmountLarge: SLASHING_UNIT * 3n,
59-
},
60-
});
61-
62-
await t.applyBaseSnapshots();
63-
await t.setup();
64-
65-
// Set slashing penalties for inactivity
66-
({ rollup } = await t.getContracts());
67-
const [activationThreshold, ejectionThreshold, localEjectionThreshold] = await Promise.all([
68-
rollup.getActivationThreshold(),
69-
rollup.getEjectionThreshold(),
70-
rollup.getLocalEjectionThreshold(),
71-
]);
72-
const biggestEjection = ejectionThreshold > localEjectionThreshold ? ejectionThreshold : localEjectionThreshold;
73-
expect(activationThreshold - SLASHING_AMOUNT).toBeLessThan(biggestEjection);
74-
t.ctx.aztecNodeConfig.slashInactivityPenalty = SLASHING_AMOUNT;
75-
76-
nodes = await createNodes(
77-
t.ctx.aztecNodeConfig,
78-
t.ctx.dateProvider,
79-
t.bootstrapNodeEnr,
80-
NUM_NODES, // Note we do not create the last validator yet, so it shows as offline
81-
BOOT_NODE_UDP_PORT,
82-
t.prefilledPublicData,
83-
84-
DATA_DIR,
85-
);
86-
await t.removeInitialNode();
87-
88-
offlineValidator = t.validators.at(-1)!.attester;
89-
t.logger.warn(`Setup complete. Offline validator is ${offlineValidator}.`, {
90-
validators: t.validators,
91-
offlineValidator,
92-
});
17+
test = await P2PInactivityTest.create('e2e_p2p_inactivity_slash', {
18+
slashInactivityConsecutiveEpochThreshold: SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD,
19+
inactiveNodeCount: 1,
20+
}).then(t => t.setup());
9321
});
9422

9523
afterAll(async () => {
96-
await t.stopNodes(nodes);
97-
await t.teardown();
98-
for (let i = 0; i < NUM_NODES; i++) {
99-
fs.rmSync(`${DATA_DIR}-${i}`, { recursive: true, force: true, maxRetries: 3 });
100-
}
24+
await test?.teardown();
10125
});
10226

10327
it('slashes inactive validator', async () => {
104-
const slashPromise = promiseWithResolvers<bigint>();
105-
rollup.listenToSlash(args => {
106-
t.logger.warn(`Slashed ${args.attester.toString()}`);
107-
expect(offlineValidator.toString()).toEqual(args.attester.toString());
108-
expect(args.amount).toEqual(SLASHING_AMOUNT);
109-
slashPromise.resolve(args.amount);
28+
const slashPromise = promiseWithResolvers<{ amount: bigint; attester: EthAddress }>();
29+
test.rollup.listenToSlash(args => {
30+
test.logger.warn(`Slashed ${args.attester.toString()}`);
31+
slashPromise.resolve(args);
11032
});
111-
await slashPromise.promise;
33+
const { amount, attester } = await slashPromise.promise;
34+
expect(test.offlineValidators[0].toString()).toEqual(attester.toString());
35+
expect(amount).toEqual(test.slashingAmount);
11236
});
11337
});
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import type { AztecNodeService } from '@aztec/aztec-node';
2+
import { EthAddress } from '@aztec/aztec.js';
3+
import { RollupContract } from '@aztec/ethereum';
4+
5+
import fs from 'fs';
6+
import 'jest-extended';
7+
import os from 'os';
8+
import path from 'path';
9+
10+
import { createNodes } from '../fixtures/setup_p2p_test.js';
11+
import { P2PNetworkTest } from './p2p_network.js';
12+
13+
const NUM_NODES = 6;
14+
const NUM_VALIDATORS = NUM_NODES;
15+
const COMMITTEE_SIZE = NUM_VALIDATORS;
16+
const SLASHING_QUORUM = 3;
17+
const EPOCH_DURATION = 2;
18+
const SLASHING_ROUND_SIZE_IN_EPOCHS = 2;
19+
const BOOT_NODE_UDP_PORT = 4500;
20+
const ETHEREUM_SLOT_DURATION = 4;
21+
const AZTEC_SLOT_DURATION = 8;
22+
const SLASHING_UNIT = BigInt(1e18);
23+
const SLASHING_AMOUNT = SLASHING_UNIT * 3n;
24+
25+
export class P2PInactivityTest {
26+
public nodes!: AztecNodeService[];
27+
public activeNodes!: AztecNodeService[];
28+
public inactiveNodes!: AztecNodeService[];
29+
30+
public rollup!: RollupContract;
31+
public offlineValidators!: EthAddress[];
32+
33+
private dataDir: string;
34+
private inactiveNodeCount: number;
35+
36+
constructor(
37+
public readonly test: P2PNetworkTest,
38+
opts: { inactiveNodeCount: number },
39+
) {
40+
this.dataDir = fs.mkdtempSync(path.join(os.tmpdir(), test.testName));
41+
this.inactiveNodeCount = opts.inactiveNodeCount;
42+
}
43+
44+
static async create(
45+
testName: string,
46+
opts: { slashInactivityConsecutiveEpochThreshold: number; inactiveNodeCount: number },
47+
) {
48+
const test = await P2PNetworkTest.create({
49+
testName,
50+
numberOfNodes: 0,
51+
numberOfValidators: NUM_VALIDATORS,
52+
basePort: BOOT_NODE_UDP_PORT,
53+
startProverNode: true,
54+
initialConfig: {
55+
proverNodeConfig: { proverNodeEpochProvingDelayMs: AZTEC_SLOT_DURATION * 1000 },
56+
aztecTargetCommitteeSize: COMMITTEE_SIZE,
57+
aztecSlotDuration: AZTEC_SLOT_DURATION,
58+
ethereumSlotDuration: ETHEREUM_SLOT_DURATION,
59+
aztecProofSubmissionEpochs: 1024, // effectively do not reorg
60+
listenAddress: '127.0.0.1',
61+
minTxsPerBlock: 0,
62+
aztecEpochDuration: EPOCH_DURATION,
63+
validatorReexecute: false,
64+
sentinelEnabled: true,
65+
slashingQuorum: SLASHING_QUORUM,
66+
slashingRoundSizeInEpochs: SLASHING_ROUND_SIZE_IN_EPOCHS,
67+
slashInactivityTargetPercentage: 0.5,
68+
slashGracePeriodL2Slots: EPOCH_DURATION, // do not slash during the first epoch
69+
slashAmountSmall: SLASHING_UNIT,
70+
slashAmountMedium: SLASHING_UNIT * 2n,
71+
slashAmountLarge: SLASHING_UNIT * 3n,
72+
...opts,
73+
},
74+
});
75+
return new P2PInactivityTest(test, opts);
76+
}
77+
78+
public async setup() {
79+
await this.test.applyBaseSnapshots();
80+
await this.test.setup();
81+
82+
// Set slashing penalties for inactivity
83+
const { rollup } = await this.test.getContracts();
84+
const [activationThreshold, ejectionThreshold, localEjectionThreshold] = await Promise.all([
85+
rollup.getActivationThreshold(),
86+
rollup.getEjectionThreshold(),
87+
rollup.getLocalEjectionThreshold(),
88+
]);
89+
const biggestEjection = ejectionThreshold > localEjectionThreshold ? ejectionThreshold : localEjectionThreshold;
90+
expect(activationThreshold - SLASHING_AMOUNT).toBeLessThan(biggestEjection);
91+
this.test.ctx.aztecNodeConfig.slashInactivityPenalty = SLASHING_AMOUNT;
92+
this.rollup = rollup;
93+
94+
// The initial validator that ran on this node is picked up by the first new node started below
95+
await this.test.removeInitialNode();
96+
97+
// Create all active nodes
98+
this.activeNodes = await createNodes(
99+
this.test.ctx.aztecNodeConfig,
100+
this.test.ctx.dateProvider,
101+
this.test.bootstrapNodeEnr,
102+
NUM_NODES - this.inactiveNodeCount,
103+
BOOT_NODE_UDP_PORT,
104+
this.test.prefilledPublicData,
105+
this.dataDir,
106+
);
107+
108+
// And the ones with an initially disabled sequencer
109+
const inactiveConfig = { ...this.test.ctx.aztecNodeConfig, dontStartSequencer: true };
110+
this.inactiveNodes = await createNodes(
111+
inactiveConfig,
112+
this.test.ctx.dateProvider,
113+
this.test.bootstrapNodeEnr,
114+
this.inactiveNodeCount,
115+
BOOT_NODE_UDP_PORT,
116+
this.test.prefilledPublicData,
117+
this.dataDir,
118+
undefined,
119+
NUM_NODES - this.inactiveNodeCount,
120+
);
121+
122+
this.nodes = [...this.activeNodes, ...this.inactiveNodes];
123+
124+
this.offlineValidators = this.test.validators
125+
.slice(this.test.validators.length - this.inactiveNodeCount)
126+
.map(a => a.attester);
127+
128+
this.test.logger.warn(`Setup complete. Offline validators are ${this.offlineValidators.join(', ')}.`, {
129+
validators: this.test.validators,
130+
offlineValidators: this.offlineValidators,
131+
});
132+
133+
return this;
134+
}
135+
136+
public async teardown() {
137+
await this.test.stopNodes(this.nodes);
138+
await this.test.teardown();
139+
for (let i = 0; i < NUM_NODES; i++) {
140+
fs.rmSync(`${this.dataDir}-${i}`, { recursive: true, force: true, maxRetries: 3 });
141+
}
142+
}
143+
144+
public get ctx() {
145+
return this.test.ctx;
146+
}
147+
148+
public get logger() {
149+
return this.test.logger;
150+
}
151+
152+
public get slashingAmount() {
153+
return SLASHING_AMOUNT;
154+
}
155+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { EthAddress } from '@aztec/aztec.js';
2+
import { unique } from '@aztec/foundation/collection';
3+
4+
import { jest } from '@jest/globals';
5+
import 'jest-extended';
6+
7+
import { P2PInactivityTest } from './inactivity_slash_test.js';
8+
9+
jest.setTimeout(1000 * 60 * 10);
10+
11+
describe('e2e_p2p_inactivity_slash_with_consecutive_epochs', () => {
12+
let test: P2PInactivityTest;
13+
14+
const slashInactivityConsecutiveEpochThreshold = 3;
15+
16+
beforeAll(async () => {
17+
test = await P2PInactivityTest.create('e2e_p2p_inactivity_slash_with_consecutive_epochs', {
18+
slashInactivityConsecutiveEpochThreshold,
19+
inactiveNodeCount: 2,
20+
}).then(t => t.setup());
21+
});
22+
23+
afterAll(async () => {
24+
await test?.teardown();
25+
});
26+
27+
it('only slashes validator inactive for N consecutive epochs', async () => {
28+
const [offlineValidator, reenabledValidator] = test.offlineValidators;
29+
const { aztecEpochDuration, slashingExecutionDelayInRounds, slashingOffsetInRounds, slashingRoundSizeInEpochs } =
30+
test.ctx.aztecNodeConfig;
31+
32+
const initialEpoch = Number(test.test.monitor.l2EpochNumber) + 1;
33+
test.logger.warn(`Waiting until end of epoch ${initialEpoch} to reenable validator ${reenabledValidator}`);
34+
await test.test.monitor.waitUntilL2Slot(initialEpoch * aztecEpochDuration);
35+
36+
test.logger.warn(`Re-enabling offline validator ${reenabledValidator}`);
37+
const reenabledNode = test.nodes.at(-1)!;
38+
expect(reenabledNode.getSequencer()!.validatorAddresses![0].toString()).toEqual(reenabledValidator.toString());
39+
await reenabledNode.getSequencer()!.start();
40+
41+
test.logger.warn(`Expecting offline validator ${offlineValidator} to be slashed but not ${reenabledValidator}`);
42+
const slashed: EthAddress[] = [];
43+
test.rollup.listenToSlash(args => {
44+
test.logger.warn(`Slashed ${args.attester.toString()}`);
45+
slashed.push(args.attester);
46+
});
47+
48+
// Wait until after the slashing would have executed for inactivity plus a bit for good measure
49+
const targetEpoch =
50+
initialEpoch +
51+
slashInactivityConsecutiveEpochThreshold +
52+
(slashingExecutionDelayInRounds + slashingOffsetInRounds) * slashingRoundSizeInEpochs +
53+
5;
54+
test.logger.warn(`Waiting until slot ${aztecEpochDuration * targetEpoch} (epoch ${targetEpoch})`);
55+
await test.test.monitor.waitUntilL2Slot(aztecEpochDuration * targetEpoch);
56+
expect(unique(slashed.map(addr => addr.toString()))).toEqual([offlineValidator.toString()]);
57+
});
58+
});

yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import {
4343
createSnapshotManager,
4444
deployAccounts,
4545
} from '../fixtures/snapshot_manager.js';
46-
import { getPrivateKeyFromIndex, getSponsoredFPCAddress } from '../fixtures/utils.js';
46+
import { type SetupOptions, getPrivateKeyFromIndex, getSponsoredFPCAddress } from '../fixtures/utils.js';
4747
import { getEndToEndTestTelemetryClient } from '../fixtures/with_telemetry_utils.js';
4848

4949
// Use a fixed bootstrap node private key so that we can re-use the same snapshot and the nodes can find each other
@@ -82,11 +82,11 @@ export class P2PNetworkTest {
8282
public bootstrapNode?: BootstrapNode;
8383

8484
constructor(
85-
testName: string,
85+
public readonly testName: string,
8686
public bootstrapNodeEnr: string,
8787
public bootNodePort: number,
8888
public numberOfValidators: number,
89-
initialValidatorConfig: AztecNodeConfig,
89+
initialValidatorConfig: SetupOptions,
9090
public numberOfNodes = 0,
9191
// If set enable metrics collection
9292
private metricsPort?: number,
@@ -162,7 +162,7 @@ export class P2PNetworkTest {
162162
numberOfValidators: number;
163163
basePort?: number;
164164
metricsPort?: number;
165-
initialConfig?: Partial<AztecNodeConfig>;
165+
initialConfig?: SetupOptions;
166166
startProverNode?: boolean;
167167
mockZkPassportVerifier?: boolean;
168168
}) {

0 commit comments

Comments
 (0)