Skip to content

Commit a2222ca

Browse files
authored
chore: Slash for inactivity network test (#16718)
Adds a spartan network test for slashing for inactivity. Adds a new config setting to nodes `disabledValidators` which is a list of eth addresses of validators that will be ignored by the validator client. Also allows validator config to be updated via the Admin API. We rely on this to disable a single validator within an epoch and see it is slashed. This allows us to run many validators per node but still slash a single one, as opposed to disabling an entire node and having to slash them all. Most of the files changed in the PR are because I renamed `updateSequencerConfig` to `updateConfig` for consistency.
2 parents b7efa2e + e2e63f6 commit a2222ca

Some content is hidden

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

42 files changed

+835
-327
lines changed

.test_patterns.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ tests:
221221
error_regex: "expect\\(received\\).toContain\\(expected\\)"
222222
owners:
223223
- *palla
224+
# http://ci.aztec-labs.com/3b197577b96c3639
225+
# http://ci.aztec-labs.com/d429457d95d075e2
226+
- regex: "src/e2e_p2p/slash_veto_demo.test.ts"
227+
error_regex: "timeout: sending signal TERM to command|ValidatorSelection__InsufficientValidatorSetSize"
228+
owners:
229+
- *palla
224230

225231
# yarn-project tests
226232
- regex: "p2p/src/services/discv5/discv5_service.test.ts"

spartan/bootstrap.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ function network_test_cmds {
7979
local run_test_script="yarn-project/end-to-end/scripts/run_test.sh"
8080
echo $run_test_script simple src/spartan/smoke.test.ts
8181
echo $run_test_script simple src/spartan/transfer.test.ts
82+
echo $run_test_script simple src/spartan/slash_inactivity.test.ts
8283
}
8384

8485
function single_test {

spartan/environments/scenario.local.env

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,18 @@ SALT=123
55
LABS_INFRA_MNEMONIC="test test test test test test test test test test test junk"
66
FUNDING_PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
77
REAL_VERIFIER=false
8-
AZTEC_EPOCH_DURATION=8
8+
SENTINEL_ENABLED=true
9+
10+
AZTEC_EPOCH_DURATION=4
911
AZTEC_SLOT_DURATION=24
12+
AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS=2
13+
AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS=1
14+
AZTEC_SLASHING_OFFSET_IN_ROUNDS=1
15+
AZTEC_ACTIVATION_THRESHOLD=100000000000000000000
16+
AZTEC_EJECTION_THRESHOLD=50000000000000000000
17+
AZTEC_SLASH_AMOUNT_SMALL=5000000000000000000
18+
AZTEC_SLASH_AMOUNT_MEDIUM=10000000000000000000
19+
AZTEC_SLASH_AMOUNT_LARGE=15000000000000000000
1020

1121
# The following need to be set manually
1222
# AZTEC_DOCKER_IMAGE=aztecprotocol/aztec:whatever

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,7 +1122,7 @@ export class AztecNodeService implements AztecNode, AztecNodeAdmin, Traceable {
11221122

11231123
public async setConfig(config: Partial<AztecNodeAdminConfig>): Promise<void> {
11241124
const newConfig = { ...this.config, ...config };
1125-
this.sequencer?.updateSequencerConfig(config);
1125+
this.sequencer?.updateConfig(config);
11261126
this.slasherClient?.updateConfig(config);
11271127
this.validatorsSentinel?.updateConfig(config);
11281128
// this.blockBuilder.updateConfig(config); // TODO: Spyros has a PR to add the builder to `this`, so we can do this

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

Lines changed: 48 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Secp256k1Signer } from '@aztec/foundation/crypto';
44
import { EthAddress } from '@aztec/foundation/eth-address';
55
import { AztecLMDBStoreV2, openTmpStore } from '@aztec/kv-store/lmdb-v2';
66
import type { P2PClient } from '@aztec/p2p';
7-
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
7+
import { OffenseType, WANT_TO_SLASH_EVENT, type WantToSlashArgs } from '@aztec/slasher';
88
import type { SlasherConfig } from '@aztec/slasher/config';
99
import {
1010
type L2BlockSource,
@@ -24,7 +24,7 @@ import type {
2424
} from '@aztec/stdlib/validators';
2525

2626
import { jest } from '@jest/globals';
27-
import { type MockProxy, mock, mockDeep } from 'jest-mock-extended';
27+
import { type MockProxy, mock } from 'jest-mock-extended';
2828

2929
import { Sentinel } from './sentinel.js';
3030
import { SentinelStore } from './store.js';
@@ -287,8 +287,8 @@ describe('sentinel', () => {
287287
jest.spyOn(sentinel, 'computeStatsForValidator').mockReturnValue({
288288
address: validator,
289289
totalSlots: 2,
290-
missedProposals: { count: 0, currentStreak: 0, rate: 0 },
291-
missedAttestations: { count: 0, currentStreak: 0, rate: 0 },
290+
missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 },
291+
missedAttestations: { count: 0, currentStreak: 0, rate: 0, total: 0 },
292292
history: mockHistory,
293293
});
294294

@@ -298,8 +298,8 @@ describe('sentinel', () => {
298298
validator: {
299299
address: validator,
300300
totalSlots: 2,
301-
missedProposals: { count: 0, currentStreak: 0, rate: 0 },
302-
missedAttestations: { count: 0, currentStreak: 0, rate: 0 },
301+
missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 },
302+
missedAttestations: { count: 0, currentStreak: 0, rate: 0, total: 0 },
303303
history: mockHistory,
304304
},
305305
allTimeProvenPerformance: mockProvenPerformance,
@@ -316,8 +316,8 @@ describe('sentinel', () => {
316316
const computeStatsSpy = jest.spyOn(sentinel, 'computeStatsForValidator').mockReturnValue({
317317
address: validator,
318318
totalSlots: 1,
319-
missedProposals: { count: 0, currentStreak: 0, rate: 0 },
320-
missedAttestations: { count: 0, currentStreak: 0, rate: 0 },
319+
missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 },
320+
missedAttestations: { count: 0, currentStreak: 0, rate: 0, total: 0 },
321321
history: mockHistory,
322322
});
323323

@@ -333,8 +333,8 @@ describe('sentinel', () => {
333333
const computeStatsSpy = jest.spyOn(sentinel, 'computeStatsForValidator').mockReturnValue({
334334
address: validator,
335335
totalSlots: 1,
336-
missedProposals: { count: 0, currentStreak: 0, rate: 0 },
337-
missedAttestations: { count: 0, currentStreak: 0, rate: 0 },
336+
missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 },
337+
missedAttestations: { count: 0, currentStreak: 0, rate: 0, total: 0 },
338338
history: mockHistory,
339339
});
340340

@@ -357,8 +357,8 @@ describe('sentinel', () => {
357357
jest.spyOn(sentinel, 'computeStatsForValidator').mockReturnValue({
358358
address: validator,
359359
totalSlots: 1,
360-
missedProposals: { count: 0, currentStreak: 0, rate: 0 },
361-
missedAttestations: { count: 0, currentStreak: 0, rate: 0 },
360+
missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 },
361+
missedAttestations: { count: 0, currentStreak: 0, rate: 0, total: 0 },
362362
history: mockHistory,
363363
});
364364

@@ -379,65 +379,52 @@ describe('sentinel', () => {
379379
const epochNumber = getEpochAtSlot(slot, l1Constants);
380380
const validator1 = EthAddress.random();
381381
const validator2 = EthAddress.random();
382-
const headerSlots = times(5, i => slot - BigInt(i));
383-
const mockHeaders = headerSlots.map(s => {
384-
const header = mockDeep<PublishedL2Block['block']['header']>();
385-
header.getSlot.mockReturnValue(s);
386-
return header;
387-
});
382+
const validator3 = EthAddress.random();
383+
const headerSlots = times(l1Constants.epochDuration, i => slot - BigInt(i)).reverse();
388384

389385
epochCache.getEpochAndSlotNow.mockReturnValue({ epoch: epochNumber, slot, ts, now: ts });
390386
archiver.getBlock.calledWith(blockNumber).mockResolvedValue(mockBlock.block);
391387
archiver.getL1Constants.mockResolvedValue(l1Constants);
392-
393-
archiver.getBlockHeadersForEpoch.calledWith(epochNumber).mockResolvedValue(mockHeaders as any);
388+
epochCache.getL1Constants.mockReturnValue(l1Constants);
394389

395390
epochCache.getCommittee.mockResolvedValue({
396-
committee: [validator1, validator2],
391+
committee: [validator1, validator2, validator3],
397392
seed: 0n,
398393
epoch: epochNumber,
399394
});
400-
const statsResult = {
395+
396+
const statsResult: ValidatorsStats = {
401397
stats: {
398+
// Validator 1 missed 1 attestation only, we won't slash them
402399
[validator1.toString()]: {
403400
address: validator1,
404401
totalSlots: headerSlots.length,
405-
missedProposals: { count: 0, currentStreak: 0, rate: 0 },
406-
missedAttestations: { count: 1, currentStreak: 0, rate: 1 / 5 },
407-
history: [
408-
{ slot: headerSlots[0], status: 'attestation-sent' },
409-
{ slot: headerSlots[1], status: 'attestation-missed' },
410-
{ slot: headerSlots[2], status: 'attestation-sent' },
411-
{ slot: headerSlots[3], status: 'attestation-sent' },
412-
{ slot: headerSlots[4], status: 'attestation-sent' },
413-
],
414-
} as ValidatorStats,
402+
missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 },
403+
missedAttestations: { count: 1, currentStreak: 0, rate: 1 / 8, total: 8 },
404+
history: [],
405+
},
406+
// Validator 2 missed 7 out of 8, we will slash them
415407
[validator2.toString()]: {
416408
address: validator2,
417409
totalSlots: headerSlots.length,
418-
missedProposals: { count: 0, currentStreak: 0, rate: 0 },
419-
// We should only count the slots that are in the proven epoch (0, 1, 2)!!
420-
missedAttestations: { count: 4, currentStreak: 3, rate: 4 / 5 },
421-
history: [
422-
{ slot: headerSlots[0], status: 'attestation-missed' },
423-
{ slot: headerSlots[1], status: 'attestation-sent' },
424-
{ slot: headerSlots[2], status: 'attestation-missed' },
425-
{ slot: headerSlots[3], status: 'attestation-missed' },
426-
{ slot: headerSlots[4], status: 'attestation-missed' },
427-
],
428-
} as ValidatorStats,
429-
'0xNotAnAddress': {
430-
address: EthAddress.ZERO, // Placeholder
431-
totalSlots: 0,
432-
missedProposals: { count: 0, currentStreak: 0, rate: undefined },
433-
missedAttestations: { count: 0, currentStreak: 0, rate: undefined },
410+
missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 },
411+
missedAttestations: { count: 7, currentStreak: 3, rate: 7 / 8, total: 8 },
412+
history: [],
413+
},
414+
// Validator 3 missed 4 attestations out of 4, so we will slash them even though the epoch has 8 slots
415+
// This difference happens because we don't count attestations for a slot where there was no proposal
416+
[validator3.toString()]: {
417+
address: validator3,
418+
totalSlots: headerSlots.length,
419+
missedProposals: { count: 0, currentStreak: 0, rate: 0, total: 0 },
420+
missedAttestations: { count: 4, currentStreak: 4, rate: 4 / 4, total: 4 },
434421
history: [],
435-
} as ValidatorStats, // To test filtering
422+
},
436423
},
437424
lastProcessedSlot: slot,
438425
initialSlot: 0n,
439426
slotWindow: 15,
440-
} as ValidatorsStats;
427+
};
441428
const computeStatsSpy = jest.spyOn(sentinel, 'computeStats').mockResolvedValue(statsResult);
442429
const emitSpy = jest.spyOn(sentinel, 'emit');
443430

@@ -446,20 +433,20 @@ describe('sentinel', () => {
446433
expect(computeStatsSpy).toHaveBeenCalledWith({
447434
fromSlot: headerSlots[0],
448435
toSlot: headerSlots[headerSlots.length - 1],
436+
validators: [validator1, validator2, validator3],
437+
});
438+
const makeInactivitySlash = (validator: EthAddress): WantToSlashArgs => ({
439+
validator,
440+
amount: config.slashInactivityPenalty,
441+
offenseType: OffenseType.INACTIVITY,
442+
epochOrSlot: 1n,
449443
});
450444

451445
expect(emitSpy).toHaveBeenCalledTimes(1);
452-
expect(emitSpy).toHaveBeenCalledWith(
453-
WANT_TO_SLASH_EVENT,
454-
expect.arrayContaining([
455-
expect.objectContaining({
456-
validator: validator2,
457-
amount: config.slashInactivityPenalty,
458-
offenseType: OffenseType.INACTIVITY,
459-
epochOrSlot: epochNumber,
460-
}),
461-
]),
462-
);
446+
expect(emitSpy).toHaveBeenCalledWith(WANT_TO_SLASH_EVENT, [
447+
makeInactivitySlash(validator2),
448+
makeInactivitySlash(validator3),
449+
]);
463450
});
464451
});
465452

@@ -673,10 +660,6 @@ class TestSentinel extends Sentinel {
673660
return super.handleProvenPerformance(epoch, performance);
674661
}
675662

676-
public override updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
677-
return super.updateProvenPerformance(epoch, performance);
678-
}
679-
680663
public override getValidatorStats(validatorAddress: EthAddress, fromSlot?: bigint, toSlot?: bigint) {
681664
return super.getValidatorStats(validatorAddress, fromSlot, toSlot);
682665
}

0 commit comments

Comments
 (0)