Skip to content

Commit d6cff17

Browse files
authored
chore: Store sentinel proven performance for a longer time (#16955)
To support `slashInactivityConsecutiveEpochThreshold` greater than 1. To avoid also storing a lot of raw stats, this PR also decouples the history length and proven performance length stored.
2 parents 0ca5388 + 99e9c72 commit d6cff17

File tree

8 files changed

+119
-9
lines changed

8 files changed

+119
-9
lines changed

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type ConfigMappingsType, booleanConfigHelper, numberConfigHelper } from
22

33
export type SentinelConfig = {
44
sentinelHistoryLengthInEpochs: number;
5+
sentinelHistoricProvenPerformanceLengthInEpochs: number;
56
sentinelEnabled: boolean;
67
};
78

@@ -11,6 +12,23 @@ export const sentinelConfigMappings: ConfigMappingsType<SentinelConfig> = {
1112
env: 'SENTINEL_HISTORY_LENGTH_IN_EPOCHS',
1213
...numberConfigHelper(24),
1314
},
15+
/**
16+
* The number of L2 epochs kept of proven performance history for each validator.
17+
* This value must be large enough so that we have proven performance for every validator
18+
* for at least slashInactivityConsecutiveEpochThreshold. Assuming this value is 3,
19+
* and the committee size is 48, and we have 10k validators, then we pick 48 out of 10k each draw.
20+
* For any fixed element, per-draw prob = 48/10000 = 0.0048.
21+
* After n draws, count ~ Binomial(n, 0.0048). We want P(X >= 3).
22+
* Results (exact binomial):
23+
* - 90% chance: n = 1108
24+
* - 95% chance: n = 1310
25+
* - 99% chance: n = 1749
26+
*/
27+
sentinelHistoricProvenPerformanceLengthInEpochs: {
28+
description: 'The number of L2 epochs kept of proven performance history for each validator.',
29+
env: 'SENTINEL_HISTORIC_PROVEN_PERFORMANCE_LENGTH_IN_EPOCHS',
30+
...numberConfigHelper(2000),
31+
},
1432
sentinelEnabled: {
1533
description: 'Whether the sentinel is enabled or not.',
1634
env: 'SENTINEL_ENABLED',

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export async function createSentinel(
2727
createLogger('node:sentinel:lmdb'),
2828
);
2929
const storeHistoryLength = config.sentinelHistoryLengthInEpochs * epochCache.getL1Constants().epochDuration;
30-
const sentinelStore = new SentinelStore(kvStore, { historyLength: storeHistoryLength });
30+
const storeHistoricProvenPerformanceLength = config.sentinelHistoricProvenPerformanceLengthInEpochs;
31+
const sentinelStore = new SentinelStore(kvStore, {
32+
historyLength: storeHistoryLength,
33+
historicProvenPerformanceLength: storeHistoricProvenPerformanceLength,
34+
});
3135
return new Sentinel(epochCache, archiver, p2p, sentinelStore, config, logger);
3236
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('sentinel', () => {
6060
blockStream = mock<L2BlockStream>();
6161

6262
kvStore = await openTmpStore('sentinel-test');
63-
store = new SentinelStore(kvStore, { historyLength: 10 });
63+
store = new SentinelStore(kvStore, { historyLength: 10, historicProvenPerformanceLength: 5 });
6464

6565
slot = 10n;
6666
epoch = 0n;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
217217
}));
218218

219219
if (criminals.length > 0) {
220-
this.logger.info(
220+
this.logger.verbose(
221221
`Identified ${criminals.length} validators to slash due to inactivity in at least ${epochThreshold} consecutive epochs`,
222222
{ ...args, epochThreshold },
223223
);

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

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { times } from '@aztec/foundation/collection';
22
import { EthAddress } from '@aztec/foundation/eth-address';
3+
import { type Logger, createLogger } from '@aztec/foundation/log';
34
import { AztecLMDBStoreV2, openTmpStore } from '@aztec/kv-store/lmdb-v2';
45
import type { ValidatorStatusInSlot } from '@aztec/stdlib/validators';
56

@@ -8,11 +9,15 @@ import { SentinelStore } from './store.js';
89
describe('sentinel-store', () => {
910
let kvStore: AztecLMDBStoreV2;
1011
let store: SentinelStore;
12+
let log: Logger;
13+
1114
const historyLength = 4;
15+
const historicProvenPerformanceLength = 3;
1216

1317
beforeEach(async () => {
18+
log = createLogger('sentinel:store:test');
1419
kvStore = await openTmpStore('sentinel-store-test');
15-
store = new SentinelStore(kvStore, { historyLength });
20+
store = new SentinelStore(kvStore, { historyLength, historicProvenPerformanceLength });
1621
});
1722

1823
afterEach(async () => {
@@ -114,6 +119,85 @@ describe('sentinel-store', () => {
114119
]);
115120
});
116121

122+
it('trims proven performance to the specified historicProvenPerformanceLength', async () => {
123+
const validator = EthAddress.random();
124+
125+
// Add 5 epochs worth of proven performance data (more than historicProvenPerformanceLength = 3)
126+
for (let i = 1; i <= 5; i++) {
127+
await store.updateProvenPerformance(BigInt(i), { [validator.toString()]: { missed: i, total: 10 } });
128+
}
129+
130+
const provenPerformance = await store.getProvenPerformance(validator);
131+
132+
// Should only keep the most recent 3 entries (epochs 3, 4, 5)
133+
expect(provenPerformance).toHaveLength(historicProvenPerformanceLength);
134+
expect(provenPerformance).toEqual([
135+
{ epoch: 3n, missed: 3, total: 10 },
136+
{ epoch: 4n, missed: 4, total: 10 },
137+
{ epoch: 5n, missed: 5, total: 10 },
138+
]);
139+
});
140+
141+
it('getHistoricProvenPerformanceLength returns the correct value', () => {
142+
expect(store.getHistoricProvenPerformanceLength()).toBe(historicProvenPerformanceLength);
143+
});
144+
145+
it('proven performance with 2k entries', async () => {
146+
const validator = EthAddress.random();
147+
const totalEntries = 2000;
148+
149+
log.info(`Starting stress test with ${totalEntries} proven performance entries`);
150+
151+
// Track timing for additions
152+
const addTimes: number[] = [];
153+
const startTime = Date.now();
154+
155+
// Add 2k entries
156+
for (let i = 1; i <= totalEntries; i++) {
157+
const addStart = Date.now();
158+
await store.updateProvenPerformance(BigInt(i), {
159+
[validator.toString()]: { missed: i % 10, total: 10 },
160+
});
161+
const addEnd = Date.now();
162+
addTimes.push(addEnd - addStart);
163+
164+
// Log progress every 500 entries
165+
if (i % 500 === 0) {
166+
const avgAddTime = addTimes.slice(-500).reduce((a, b) => a + b, 0) / 500;
167+
log.info(`Added ${i}/${totalEntries} entries, avg time per entry: ${avgAddTime.toFixed(2)}ms`);
168+
}
169+
}
170+
171+
const totalAddTime = Date.now() - startTime;
172+
const avgAddTime = addTimes.reduce((a, b) => a + b, 0) / addTimes.length;
173+
174+
log.info(`Added ${totalEntries} entries in ${totalAddTime}ms with avg ${avgAddTime.toFixed(2)}ms per entry`);
175+
176+
// Track timing for retrievals
177+
const retrievalTimes: number[] = [];
178+
const numRetrievals = 10;
179+
180+
log.info(`Starting ${numRetrievals} retrieval tests`);
181+
182+
for (let i = 0; i < numRetrievals; i++) {
183+
const retrievalStart = Date.now();
184+
const performance = await store.getProvenPerformance(validator);
185+
const retrievalEnd = Date.now();
186+
retrievalTimes.push(retrievalEnd - retrievalStart);
187+
188+
// Verify we only keep the configured number of entries
189+
expect(performance).toHaveLength(historicProvenPerformanceLength);
190+
191+
// Verify we kept the most recent entries
192+
const expectedStartEpoch = totalEntries - historicProvenPerformanceLength + 1;
193+
expect(performance[0].epoch).toBe(BigInt(expectedStartEpoch));
194+
expect(performance[performance.length - 1].epoch).toBe(BigInt(totalEntries));
195+
}
196+
197+
const avgRetrievalTime = retrievalTimes.reduce((a, b) => a + b, 0) / retrievalTimes.length;
198+
log.info(`Completed ${numRetrievals} retrievals with avg time of ${avgRetrievalTime.toFixed(2)}ms per retrieval`);
199+
});
200+
117201
it('does not allow insertion of invalid validator addresses', async () => {
118202
const validator = '0x123';
119203
await expect(store.updateProvenPerformance(1n, { [validator]: { missed: 2, total: 10 } })).rejects.toThrow();

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class SentinelStore {
1919

2020
constructor(
2121
private store: AztecAsyncKVStore,
22-
private config: { historyLength: number },
22+
private config: { historyLength: number; historicProvenPerformanceLength: number },
2323
) {
2424
this.historyMap = store.openMap('sentinel-validator-status');
2525
this.provenMap = store.openMap('sentinel-validator-proven');
@@ -29,6 +29,10 @@ export class SentinelStore {
2929
return this.config.historyLength;
3030
}
3131

32+
public getHistoricProvenPerformanceLength() {
33+
return this.config.historicProvenPerformanceLength;
34+
}
35+
3236
public async updateProvenPerformance(epoch: bigint, performance: ValidatorsEpochPerformance) {
3337
await this.store.transactionAsync(async () => {
3438
for (const [who, { missed, total }] of Object.entries(performance)) {
@@ -65,8 +69,8 @@ export class SentinelStore {
6569
// Since we keep the size small, this is not a big deal.
6670
currentPerformance.sort((a, b) => Number(a.epoch - b.epoch));
6771

68-
// keep the most recent `historyLength` entries.
69-
const performanceToKeep = currentPerformance.slice(-this.config.historyLength);
72+
// keep the most recent `historicProvenPerformanceLength` entries.
73+
const performanceToKeep = currentPerformance.slice(-this.config.historicProvenPerformanceLength);
7074

7175
await this.provenMap.set(who.toString(), this.serializePerformance(performanceToKeep));
7276
}

yarn-project/foundation/src/config/env_var.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ export type EnvVar =
172172
| 'RPC_SIMULATE_PUBLIC_MAX_GAS_LIMIT'
173173
| 'SENTINEL_ENABLED'
174174
| 'SENTINEL_HISTORY_LENGTH_IN_EPOCHS'
175+
| 'SENTINEL_HISTORIC_PROVEN_PERFORMANCE_LENGTH_IN_EPOCHS'
175176
| 'SEQ_MAX_BLOCK_SIZE_IN_BYTES'
176177
| 'SEQ_MAX_TX_PER_BLOCK'
177178
| 'SEQ_MIN_TX_PER_BLOCK'

yarn-project/slasher/src/slash_offenses_collector.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,9 @@ export class SlashOffensesCollector {
9595
* Clears expired offenses from stores.
9696
*/
9797
public async handleNewRound(round: bigint) {
98-
this.log.verbose(`Clearing expired offenses for new slashing round ${round}`);
9998
const cleared = await this.offensesStore.clearExpiredOffenses(round);
10099
if (cleared && cleared > 0) {
101-
this.log.verbose(`Cleared ${cleared} expired offenses`);
100+
this.log.debug(`Cleared ${cleared} expired offenses for round ${round}`);
102101
}
103102
}
104103

0 commit comments

Comments
 (0)