@aztec/slasher 0.0.1-commit.993d240 → 0.0.1-commit.9a89641
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/dest/factory/create_facade.d.ts +5 -2
- package/dest/factory/create_facade.d.ts.map +1 -1
- package/dest/factory/create_facade.js +5 -2
- package/dest/factory/create_implementation.d.ts +3 -2
- package/dest/factory/create_implementation.d.ts.map +1 -1
- package/dest/factory/create_implementation.js +4 -4
- package/dest/metrics.d.ts +23 -0
- package/dest/metrics.d.ts.map +1 -0
- package/dest/metrics.js +38 -0
- package/dest/own_validator_slash_monitor.d.ts +66 -0
- package/dest/own_validator_slash_monitor.d.ts.map +1 -0
- package/dest/own_validator_slash_monitor.js +163 -0
- package/dest/slasher_client.d.ts +9 -3
- package/dest/slasher_client.d.ts.map +1 -1
- package/dest/slasher_client.js +21 -2
- package/dest/slasher_client_facade.d.ts +4 -2
- package/dest/slasher_client_facade.d.ts.map +1 -1
- package/dest/slasher_client_facade.js +4 -2
- package/package.json +9 -9
- package/src/factory/create_facade.ts +5 -1
- package/src/factory/create_implementation.ts +5 -0
- package/src/metrics.ts +59 -0
- package/src/own_validator_slash_monitor.ts +184 -0
- package/src/slasher_client.ts +23 -1
- package/src/slasher_client_facade.ts +3 -0
package/README.md
CHANGED
|
@@ -122,6 +122,12 @@ List of all slashable offenses in the system:
|
|
|
122
122
|
**Target**: Proposer who broadcast the duplicate proposal.
|
|
123
123
|
**Time Unit**: Slot-based offense.
|
|
124
124
|
|
|
125
|
+
### DUPLICATE_ATTESTATION
|
|
126
|
+
**Description**: A validator signed attestations for different checkpoint proposals (different archives) at the same slot. Validators must attest to at most one checkpoint per slot, so signing conflicting attestations is equivocation.
|
|
127
|
+
**Detection**: The P2P AttestationPool tracks attestations by slot and signer. A second attestation from the same signer for a different proposal at the same slot triggers the duplicate-attestation callback; the duplicate is propagated (Accept) so other validators can witness the offense. The ValidatorClient emits the slash event.
|
|
128
|
+
**Target**: The attester who equivocated.
|
|
129
|
+
**Time Unit**: Slot-based offense.
|
|
130
|
+
|
|
125
131
|
### ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL
|
|
126
132
|
**Description**: A committee member attested to a checkpoint proposal in a slot where this node detected a slashable invalid block proposal.
|
|
127
133
|
**Detection**: ValidatorClient marks slots with invalid block proposals detected via reexecution and slashes checkpoint attesters seen for that slot. If proposal equivocation is later detected for the slot, pending bad-attestation offenses are cleared.
|
|
@@ -173,6 +179,7 @@ with divergent validation limits.
|
|
|
173
179
|
- `slashBroadcastedInvalidBlockPenalty`: Penalty for BROADCASTED_INVALID_BLOCK_PROPOSAL
|
|
174
180
|
- `slashBroadcastedInvalidCheckpointProposalPenalty`: Penalty for BROADCASTED_INVALID_CHECKPOINT_PROPOSAL
|
|
175
181
|
- `slashDuplicateProposalPenalty`: Penalty for DUPLICATE_PROPOSAL
|
|
182
|
+
- `slashDuplicateAttestationPenalty`: Penalty for DUPLICATE_ATTESTATION
|
|
176
183
|
- `slashProposeInvalidAttestationsPenalty`: Penalty for PROPOSED_INSUFFICIENT_ATTESTATIONS and PROPOSED_INCORRECT_ATTESTATIONS
|
|
177
184
|
- `slashProposeDescendantOfCheckpointWithInvalidAttestationsPenalty`: Penalty for PROPOSED_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS
|
|
178
185
|
- `slashAttestInvalidCheckpointProposalPenalty`: Penalty for ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL
|
|
@@ -11,6 +11,9 @@ import type { Watcher } from '../watcher.js';
|
|
|
11
11
|
export declare function createSlasherFacade(config: SlasherConfig & DataStoreConfig & {
|
|
12
12
|
ethereumSlotDuration: number;
|
|
13
13
|
}, l1Contracts: Pick<L1ReaderConfig, 'rollupAddress' | 'registryAddress'>, l1Client: ViemClient, watchers: Watcher[], dateProvider: DateProvider, epochCache: EpochCache,
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* List of own validator addresses. Added to the slashValidatorNever list unless slashSelfAllowed is true, and used
|
|
16
|
+
* (independently of slashSelfAllowed) to warn and record metrics when they are targeted by onchain slashing.
|
|
17
|
+
*/
|
|
15
18
|
validatorAddresses?: EthAddress[], logger?: import("@aztec/foundation/log").Logger): Promise<SlasherClientInterface>;
|
|
16
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
19
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlX2ZhY2FkZS5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL2ZhY3RvcnkvY3JlYXRlX2ZhY2FkZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFFaEQsT0FBTyxLQUFLLEVBQUUsY0FBYyxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDaEUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFHeEQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBRTNELE9BQU8sRUFBRSxZQUFZLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUd2RCxPQUFPLEtBQUssRUFBRSxhQUFhLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNyRSxPQUFPLEtBQUssRUFBRSxlQUFlLEVBQUUsTUFBTSx3QkFBd0IsQ0FBQztBQUc5RCxPQUFPLEtBQUssRUFBRSxzQkFBc0IsRUFBRSxNQUFNLGdDQUFnQyxDQUFDO0FBRTdFLE9BQU8sS0FBSyxFQUFFLE9BQU8sRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUU3Qyw4RkFBOEY7QUFDOUYsd0JBQXNCLG1CQUFtQixDQUN2QyxNQUFNLEVBQUUsYUFBYSxHQUFHLGVBQWUsR0FBRztJQUFFLG9CQUFvQixFQUFFLE1BQU0sQ0FBQTtDQUFFLEVBQzFFLFdBQVcsRUFBRSxJQUFJLENBQUMsY0FBYyxFQUFFLGVBQWUsR0FBRyxpQkFBaUIsQ0FBQyxFQUN0RSxRQUFRLEVBQUUsVUFBVSxFQUNwQixRQUFRLEVBQUUsT0FBTyxFQUFFLEVBQ25CLFlBQVksRUFBRSxZQUFZLEVBQzFCLFVBQVUsRUFBRSxVQUFVO0FBQ3RCOzs7R0FHRztBQUNILGtCQUFrQixHQUFFLFVBQVUsRUFBTyxFQUNyQyxNQUFNLHlDQUEwQixHQUMvQixPQUFPLENBQUMsc0JBQXNCLENBQUMsQ0FtRGpDIn0=
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create_facade.d.ts","sourceRoot":"","sources":["../../src/factory/create_facade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAE7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,8FAA8F;AAC9F,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG;IAAE,oBAAoB,EAAE,MAAM,CAAA;CAAE,EAC1E,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,GAAG,iBAAiB,CAAC,EACtE,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,OAAO,EAAE,EACnB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,UAAU;AACtB
|
|
1
|
+
{"version":3,"file":"create_facade.d.ts","sourceRoot":"","sources":["../../src/factory/create_facade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAEhD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAG9D,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAE7E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAE7C,8FAA8F;AAC9F,wBAAsB,mBAAmB,CACvC,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG;IAAE,oBAAoB,EAAE,MAAM,CAAA;CAAE,EAC1E,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE,eAAe,GAAG,iBAAiB,CAAC,EACtE,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,OAAO,EAAE,EACnB,YAAY,EAAE,YAAY,EAC1B,UAAU,EAAE,UAAU;AACtB;;;GAGG;AACH,kBAAkB,GAAE,UAAU,EAAO,EACrC,MAAM,yCAA0B,GAC/B,OAAO,CAAC,sBAAsB,CAAC,CAmDjC"}
|
|
@@ -7,7 +7,10 @@ import { createStore } from '@aztec/kv-store/lmdb-v2';
|
|
|
7
7
|
import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
8
8
|
import { SlasherClientFacade } from '../slasher_client_facade.js';
|
|
9
9
|
import { SCHEMA_VERSION } from '../stores/schema_version.js';
|
|
10
|
-
/** Creates a slasher client facade that updates itself whenever the rollup slasher changes */ export async function createSlasherFacade(config, l1Contracts, l1Client, watchers, dateProvider, epochCache, /**
|
|
10
|
+
/** Creates a slasher client facade that updates itself whenever the rollup slasher changes */ export async function createSlasherFacade(config, l1Contracts, l1Client, watchers, dateProvider, epochCache, /**
|
|
11
|
+
* List of own validator addresses. Added to the slashValidatorNever list unless slashSelfAllowed is true, and used
|
|
12
|
+
* (independently of slashSelfAllowed) to warn and record metrics when they are targeted by onchain slashing.
|
|
13
|
+
*/ validatorAddresses = [], logger = createLogger('slasher')) {
|
|
11
14
|
if (!l1Contracts.rollupAddress || l1Contracts.rollupAddress.equals(EthAddress.ZERO)) {
|
|
12
15
|
throw new Error('Cannot initialize SlasherClient without a Rollup address');
|
|
13
16
|
}
|
|
@@ -42,5 +45,5 @@ import { SCHEMA_VERSION } from '../stores/schema_version.js';
|
|
|
42
45
|
...config,
|
|
43
46
|
slashValidatorsNever
|
|
44
47
|
};
|
|
45
|
-
return new SlasherClientFacade(updatedConfig, rollup, l1Client, watchers, epochCache, dateProvider, kvStore, rollupRegisteredAtL2Slot, logger);
|
|
48
|
+
return new SlasherClientFacade(updatedConfig, rollup, l1Client, watchers, epochCache, dateProvider, kvStore, rollupRegisteredAtL2Slot, validatorAddresses, logger);
|
|
46
49
|
}
|
|
@@ -2,6 +2,7 @@ import { EpochCache } from '@aztec/epoch-cache';
|
|
|
2
2
|
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
3
|
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
4
|
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
6
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
6
7
|
import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
|
|
7
8
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
@@ -12,5 +13,5 @@ import type { Watcher } from '../watcher.js';
|
|
|
12
13
|
/** Creates a slasher client implementation based on the slasher proposer type in the rollup */
|
|
13
14
|
export declare function createSlasherImplementation(config: SlasherConfig & DataStoreConfig & {
|
|
14
15
|
ethereumSlotDuration: number;
|
|
15
|
-
}, rollup: RollupContract, l1Client: ViemClient, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, kvStore: AztecLMDBStoreV2, rollupRegisteredAtL2Slot: SlotNumber, logger?: import("@aztec/foundation/log").Logger): Promise<NullSlasherClient | SlasherClient>;
|
|
16
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
16
|
+
}, rollup: RollupContract, l1Client: ViemClient, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, kvStore: AztecLMDBStoreV2, rollupRegisteredAtL2Slot: SlotNumber, ownValidators?: EthAddress[], logger?: import("@aztec/foundation/log").Logger): Promise<NullSlasherClient | SlasherClient>;
|
|
17
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY3JlYXRlX2ltcGxlbWVudGF0aW9uLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvZmFjdG9yeS9jcmVhdGVfaW1wbGVtZW50YXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFBRSxjQUFjLEVBQTRCLE1BQU0sMkJBQTJCLENBQUM7QUFDckYsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDeEQsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDbEUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFFaEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzNELE9BQU8sS0FBSyxFQUFFLGFBQWEsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQ3JFLE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRTlELE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBQzlELE9BQU8sRUFBRSxhQUFhLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUVyRCxPQUFPLEtBQUssRUFBRSxPQUFPLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFHN0MsK0ZBQStGO0FBQy9GLHdCQUFzQiwyQkFBMkIsQ0FDL0MsTUFBTSxFQUFFLGFBQWEsR0FBRyxlQUFlLEdBQUc7SUFBRSxvQkFBb0IsRUFBRSxNQUFNLENBQUE7Q0FBRSxFQUMxRSxNQUFNLEVBQUUsY0FBYyxFQUN0QixRQUFRLEVBQUUsVUFBVSxFQUNwQixRQUFRLEVBQUUsT0FBTyxFQUFFLEVBQ25CLFVBQVUsRUFBRSxVQUFVLEVBQ3RCLFlBQVksRUFBRSxZQUFZLEVBQzFCLE9BQU8sRUFBRSxnQkFBZ0IsRUFDekIsd0JBQXdCLEVBQUUsVUFBVSxFQUNwQyxhQUFhLEdBQUUsVUFBVSxFQUFPLEVBQ2hDLE1BQU0seUNBQTBCLDhDQW1CakMifQ==
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create_implementation.d.ts","sourceRoot":"","sources":["../../src/factory/create_implementation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,cAAc,EAA4B,MAAM,2BAA2B,CAAC;AACrF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"create_implementation.d.ts","sourceRoot":"","sources":["../../src/factory/create_implementation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,cAAc,EAA4B,MAAM,2BAA2B,CAAC;AACrF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAEhE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAErD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAG7C,+FAA+F;AAC/F,wBAAsB,2BAA2B,CAC/C,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG;IAAE,oBAAoB,EAAE,MAAM,CAAA;CAAE,EAC1E,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,gBAAgB,EACzB,wBAAwB,EAAE,UAAU,EACpC,aAAa,GAAE,UAAU,EAAO,EAChC,MAAM,yCAA0B,8CAmBjC"}
|
|
@@ -3,15 +3,15 @@ import { NullSlasherClient } from '../null_slasher_client.js';
|
|
|
3
3
|
import { SlasherClient } from '../slasher_client.js';
|
|
4
4
|
import { SlasherOffensesStore } from '../stores/offenses_store.js';
|
|
5
5
|
import { getSlasherSettings } from './get_settings.js';
|
|
6
|
-
/** Creates a slasher client implementation based on the slasher proposer type in the rollup */ export async function createSlasherImplementation(config, rollup, l1Client, watchers, epochCache, dateProvider, kvStore, rollupRegisteredAtL2Slot, logger = createLogger('slasher')) {
|
|
6
|
+
/** Creates a slasher client implementation based on the slasher proposer type in the rollup */ export async function createSlasherImplementation(config, rollup, l1Client, watchers, epochCache, dateProvider, kvStore, rollupRegisteredAtL2Slot, ownValidators = [], logger = createLogger('slasher')) {
|
|
7
7
|
const proposer = await rollup.getSlashingProposer();
|
|
8
8
|
if (!proposer) {
|
|
9
9
|
return new NullSlasherClient(config);
|
|
10
10
|
} else {
|
|
11
|
-
return createSlasher(config, rollup, proposer, watchers, dateProvider, epochCache, kvStore, rollupRegisteredAtL2Slot, logger);
|
|
11
|
+
return createSlasher(config, rollup, proposer, watchers, dateProvider, epochCache, kvStore, rollupRegisteredAtL2Slot, ownValidators, logger);
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
async function createSlasher(config, rollup, slashingProposer, watchers, dateProvider, epochCache, kvStore, rollupRegisteredAtL2Slot, logger = createLogger('slasher')) {
|
|
14
|
+
async function createSlasher(config, rollup, slashingProposer, watchers, dateProvider, epochCache, kvStore, rollupRegisteredAtL2Slot, ownValidators = [], logger = createLogger('slasher')) {
|
|
15
15
|
const settings = {
|
|
16
16
|
...await getSlasherSettings(rollup, slashingProposer),
|
|
17
17
|
rollupRegisteredAtL2Slot
|
|
@@ -21,5 +21,5 @@ async function createSlasher(config, rollup, slashingProposer, watchers, datePro
|
|
|
21
21
|
...settings,
|
|
22
22
|
slashOffenseExpirationRounds: config.slashOffenseExpirationRounds
|
|
23
23
|
});
|
|
24
|
-
return new SlasherClient(config, settings, slashingProposer, slasher, rollup, watchers, epochCache, dateProvider, offensesStore, logger);
|
|
24
|
+
return new SlasherClient(config, settings, slashingProposer, slasher, rollup, watchers, epochCache, dateProvider, offensesStore, ownValidators, logger);
|
|
25
25
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type TelemetryClient } from '@aztec/telemetry-client';
|
|
2
|
+
export declare class SlasherMetrics {
|
|
3
|
+
private readonly roundExecuted;
|
|
4
|
+
private readonly ownValidatorTargeted;
|
|
5
|
+
private readonly ownValidatorSlashedCount;
|
|
6
|
+
private readonly ownValidatorSlashedAmount;
|
|
7
|
+
private readonly ownValidatorCurrentRoundVotesMax;
|
|
8
|
+
private readonly quorumSize;
|
|
9
|
+
constructor(client: TelemetryClient, name?: string);
|
|
10
|
+
recordRoundExecuted(): void;
|
|
11
|
+
/** Records the quorum a validator must reach in a round to be slashed, so dashboards can plot the threshold. */
|
|
12
|
+
recordQuorumSize(quorum: number): void;
|
|
13
|
+
/** Records that an onchain slashing vote named one of the node's own validators as a target. */
|
|
14
|
+
recordOwnValidatorTargeted(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Records how close the most-voted committee position held by the node's own validators is to quorum this round.
|
|
17
|
+
* Recorded as an absolute value rather than a delta so a vote seen across a round rollover cannot make it drift.
|
|
18
|
+
*/
|
|
19
|
+
recordCurrentRoundVotesMax(votes: number): void;
|
|
20
|
+
/** Records an executed slash against one of the node's own validators. */
|
|
21
|
+
recordOwnValidatorSlashed(amount: bigint): void;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljcy5kLnRzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vc3JjL21ldHJpY3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUdMLEtBQUssZUFBZSxFQUdyQixNQUFNLHlCQUF5QixDQUFDO0FBSWpDLHFCQUFhLGNBQWM7SUFDekIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxhQUFhLENBQWdCO0lBQzlDLE9BQU8sQ0FBQyxRQUFRLENBQUMsb0JBQW9CLENBQWdCO0lBQ3JELE9BQU8sQ0FBQyxRQUFRLENBQUMsd0JBQXdCLENBQWdCO0lBQ3pELE9BQU8sQ0FBQyxRQUFRLENBQUMseUJBQXlCLENBQWdCO0lBQzFELE9BQU8sQ0FBQyxRQUFRLENBQUMsZ0NBQWdDLENBQVE7SUFDekQsT0FBTyxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQVE7SUFFbkMsWUFBWSxNQUFNLEVBQUUsZUFBZSxFQUFFLElBQUksU0FBWSxFQVdwRDtJQUVNLG1CQUFtQixJQUFJLElBQUksQ0FFakM7SUFFRCxnSEFBZ0g7SUFDekcsZ0JBQWdCLENBQUMsTUFBTSxFQUFFLE1BQU0sR0FBRyxJQUFJLENBRTVDO0lBRUQsZ0dBQWdHO0lBQ3pGLDBCQUEwQixJQUFJLElBQUksQ0FFeEM7SUFFRDs7O09BR0c7SUFDSSwwQkFBMEIsQ0FBQyxLQUFLLEVBQUUsTUFBTSxHQUFHLElBQUksQ0FFckQ7SUFFRCwwRUFBMEU7SUFDbkUseUJBQXlCLENBQUMsTUFBTSxFQUFFLE1BQU0sR0FBRyxJQUFJLENBR3JEO0NBQ0YifQ==
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metrics.d.ts","sourceRoot":"","sources":["../src/metrics.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,eAAe,EAGrB,MAAM,yBAAyB,CAAC;AAIjC,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAgB;IACrD,OAAO,CAAC,QAAQ,CAAC,wBAAwB,CAAgB;IACzD,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAgB;IAC1D,OAAO,CAAC,QAAQ,CAAC,gCAAgC,CAAQ;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IAEnC,YAAY,MAAM,EAAE,eAAe,EAAE,IAAI,SAAY,EAWpD;IAEM,mBAAmB,IAAI,IAAI,CAEjC;IAED,gHAAgH;IACzG,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAE5C;IAED,gGAAgG;IACzF,0BAA0B,IAAI,IAAI,CAExC;IAED;;;OAGG;IACI,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAErD;IAED,0EAA0E;IACnE,yBAAyB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAGrD;CACF"}
|
package/dest/metrics.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Metrics, createUpDownCounterWithDefault } from '@aztec/telemetry-client';
|
|
2
|
+
import { formatEther } from 'viem/utils';
|
|
3
|
+
export class SlasherMetrics {
|
|
4
|
+
roundExecuted;
|
|
5
|
+
ownValidatorTargeted;
|
|
6
|
+
ownValidatorSlashedCount;
|
|
7
|
+
ownValidatorSlashedAmount;
|
|
8
|
+
ownValidatorCurrentRoundVotesMax;
|
|
9
|
+
quorumSize;
|
|
10
|
+
constructor(client, name = 'Slasher'){
|
|
11
|
+
const meter = client.getMeter(name);
|
|
12
|
+
this.roundExecuted = createUpDownCounterWithDefault(meter, Metrics.SLASHER_ROUND_EXECUTED_COUNT);
|
|
13
|
+
this.ownValidatorTargeted = createUpDownCounterWithDefault(meter, Metrics.SLASHER_OWN_VALIDATOR_TARGETED_COUNT);
|
|
14
|
+
this.ownValidatorSlashedCount = createUpDownCounterWithDefault(meter, Metrics.SLASHER_OWN_VALIDATOR_SLASHED_COUNT);
|
|
15
|
+
this.ownValidatorSlashedAmount = createUpDownCounterWithDefault(meter, Metrics.SLASHER_OWN_VALIDATOR_SLASHED_AMOUNT);
|
|
16
|
+
this.ownValidatorCurrentRoundVotesMax = meter.createGauge(Metrics.SLASHER_OWN_VALIDATOR_CURRENT_ROUND_VOTES_MAX);
|
|
17
|
+
this.quorumSize = meter.createGauge(Metrics.SLASHER_QUORUM_SIZE);
|
|
18
|
+
}
|
|
19
|
+
recordRoundExecuted() {
|
|
20
|
+
this.roundExecuted.add(1);
|
|
21
|
+
}
|
|
22
|
+
/** Records the quorum a validator must reach in a round to be slashed, so dashboards can plot the threshold. */ recordQuorumSize(quorum) {
|
|
23
|
+
this.quorumSize.record(quorum);
|
|
24
|
+
}
|
|
25
|
+
/** Records that an onchain slashing vote named one of the node's own validators as a target. */ recordOwnValidatorTargeted() {
|
|
26
|
+
this.ownValidatorTargeted.add(1);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Records how close the most-voted committee position held by the node's own validators is to quorum this round.
|
|
30
|
+
* Recorded as an absolute value rather than a delta so a vote seen across a round rollover cannot make it drift.
|
|
31
|
+
*/ recordCurrentRoundVotesMax(votes) {
|
|
32
|
+
this.ownValidatorCurrentRoundVotesMax.record(votes);
|
|
33
|
+
}
|
|
34
|
+
/** Records an executed slash against one of the node's own validators. */ recordOwnValidatorSlashed(amount) {
|
|
35
|
+
this.ownValidatorSlashedCount.add(1);
|
|
36
|
+
this.ownValidatorSlashedAmount.add(parseFloat(formatEther(amount)));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { SlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
2
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
+
import type { Hex } from 'viem';
|
|
4
|
+
import type { SlasherMetrics } from './metrics.js';
|
|
5
|
+
/**
|
|
6
|
+
* Watches slashing activity that targets the node's own validators: warns and counts on every vote naming one of
|
|
7
|
+
* them, tracks how close the current round's tally is to quorum, and reports executed slashes. Does nothing when
|
|
8
|
+
* the node runs no validators. Lifecycle calls must be sequential: start() before events are wired, and stop()
|
|
9
|
+
* awaited before any restart.
|
|
10
|
+
*/
|
|
11
|
+
export declare class OwnValidatorSlashMonitor {
|
|
12
|
+
private readonly slashingProposer;
|
|
13
|
+
private readonly settings;
|
|
14
|
+
private readonly ownValidators;
|
|
15
|
+
private readonly metrics;
|
|
16
|
+
private readonly log;
|
|
17
|
+
/**
|
|
18
|
+
* Tally for the single round being tracked (votes are only ever cast for the current round). `nextVoteIndex` is
|
|
19
|
+
* the cursor: the index of the first vote in the round not yet processed, advanced from the indices the VoteCast
|
|
20
|
+
* events carry. It is undefined until the first event of a round that started mid-round, whose own vote is then
|
|
21
|
+
* the first one counted, so votes cast before the node subscribed are never replayed into the cumulative metrics.
|
|
22
|
+
* The tally is per flattened committee position because that is the unit the contract tallies quorum by.
|
|
23
|
+
*/
|
|
24
|
+
private state;
|
|
25
|
+
/**
|
|
26
|
+
* All vote processing runs through this queue, one job at a time in arrival order. Serialization is what makes
|
|
27
|
+
* the cursor sound: without it, concurrent event handlers could process the same index twice or emit warnings
|
|
28
|
+
* with out-of-order running tallies. Replaced on every start(), since a queue that has been ended cannot run
|
|
29
|
+
* jobs again.
|
|
30
|
+
*/
|
|
31
|
+
private queue;
|
|
32
|
+
private stopped;
|
|
33
|
+
constructor(slashingProposer: SlashingProposerContract, settings: {
|
|
34
|
+
slashingQuorumSize: number;
|
|
35
|
+
}, ownValidators: EthAddress[], metrics: SlasherMetrics, log?: import("@aztec/foundation/log").Logger);
|
|
36
|
+
private get enabled();
|
|
37
|
+
/**
|
|
38
|
+
* Starts tracking at the given round, leaving the cursor unset so the round's votes already cast are skipped:
|
|
39
|
+
* replaying them would warn about votes the operator can no longer react to sooner, and would double-count
|
|
40
|
+
* cumulative counters across restarts.
|
|
41
|
+
*/
|
|
42
|
+
start(currentRound: bigint): void;
|
|
43
|
+
/** Stops processing and waits for any in-flight drain, so no warning or metric is emitted after shutdown. */
|
|
44
|
+
stop(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Called by the round monitor clock. Strictly greater so the clock catching up to a round an event already rolled
|
|
47
|
+
* to keeps the tally.
|
|
48
|
+
*/
|
|
49
|
+
handleNewRound(round: bigint): void;
|
|
50
|
+
/**
|
|
51
|
+
* Called on each VoteCast event with the index the event carries. Reads every vote from the cursor up to that
|
|
52
|
+
* index, so an event delivery that never arrived is healed by the next one.
|
|
53
|
+
*/
|
|
54
|
+
handleVoteCast(round: bigint, voteIndex: bigint): Promise<void>;
|
|
55
|
+
/** Reports executed slashes against own validators. Called with the Slashed events already fetched by the client. */
|
|
56
|
+
handleSlashes(round: bigint, slashes: {
|
|
57
|
+
attester: EthAddress;
|
|
58
|
+
amount: bigint;
|
|
59
|
+
}[], l1BlockHash: Hex): void;
|
|
60
|
+
private drainVotesTo;
|
|
61
|
+
private processVote;
|
|
62
|
+
/** Starts a fresh tally, zeroing the gauge so a quiet round does not keep the previous round's value. */
|
|
63
|
+
private rollTo;
|
|
64
|
+
private isOwnValidator;
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3duX3ZhbGlkYXRvcl9zbGFzaF9tb25pdG9yLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvb3duX3ZhbGlkYXRvcl9zbGFzaF9tb25pdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFtQix3QkFBd0IsRUFBRSxNQUFNLDJCQUEyQixDQUFDO0FBRTNGLE9BQU8sS0FBSyxFQUFFLFVBQVUsRUFBRSxNQUFNLCtCQUErQixDQUFDO0FBSWhFLE9BQU8sS0FBSyxFQUFFLEdBQUcsRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUVoQyxPQUFPLEtBQUssRUFBRSxjQUFjLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFFbkQ7Ozs7O0dBS0c7QUFDSCxxQkFBYSx3QkFBd0I7SUF5QmpDLE9BQU8sQ0FBQyxRQUFRLENBQUMsZ0JBQWdCO0lBQ2pDLE9BQU8sQ0FBQyxRQUFRLENBQUMsUUFBUTtJQUN6QixPQUFPLENBQUMsUUFBUSxDQUFDLGFBQWE7SUFDOUIsT0FBTyxDQUFDLFFBQVEsQ0FBQyxPQUFPO0lBQ3hCLE9BQU8sQ0FBQyxRQUFRLENBQUMsR0FBRztJQTVCdEI7Ozs7OztPQU1HO0lBQ0gsT0FBTyxDQUFDLEtBQUssQ0FJWDtJQUVGOzs7OztPQUtHO0lBQ0gsT0FBTyxDQUFDLEtBQUssQ0FBcUI7SUFFbEMsT0FBTyxDQUFDLE9BQU8sQ0FBUztJQUV4QixZQUNtQixnQkFBZ0IsRUFBRSx3QkFBd0IsRUFDMUMsUUFBUSxFQUFFO1FBQUUsa0JBQWtCLEVBQUUsTUFBTSxDQUFBO0tBQUUsRUFDeEMsYUFBYSxFQUFFLFVBQVUsRUFBRSxFQUMzQixPQUFPLEVBQUUsY0FBYyxFQUN2QixHQUFHLHlDQUF5QyxFQUc5RDtJQUVELE9BQU8sS0FBSyxPQUFPLEdBRWxCO0lBRUQ7Ozs7T0FJRztJQUNJLEtBQUssQ0FBQyxZQUFZLEVBQUUsTUFBTSxHQUFHLElBQUksQ0FVdkM7SUFFRCw2R0FBNkc7SUFDaEcsSUFBSSxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FLakM7SUFFRDs7O09BR0c7SUFDSSxjQUFjLENBQUMsS0FBSyxFQUFFLE1BQU0sR0FBRyxJQUFJLENBSXpDO0lBRUQ7OztPQUdHO0lBQ0ksY0FBYyxDQUFDLEtBQUssRUFBRSxNQUFNLEVBQUUsU0FBUyxFQUFFLE1BQU0sR0FBRyxPQUFPLENBQUMsSUFBSSxDQUFDLENBU3JFO0lBRUQscUhBQXFIO0lBQzlHLGFBQWEsQ0FBQyxLQUFLLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRTtRQUFFLFFBQVEsRUFBRSxVQUFVLENBQUM7UUFBQyxNQUFNLEVBQUUsTUFBTSxDQUFBO0tBQUUsRUFBRSxFQUFFLFdBQVcsRUFBRSxHQUFHLEdBQUcsSUFBSSxDQWEvRztZQUdhLFlBQVk7SUE0QjFCLE9BQU8sQ0FBQyxXQUFXO0lBd0JuQix5R0FBeUc7SUFDekcsT0FBTyxDQUFDLE1BQU07SUFNZCxPQUFPLENBQUMsY0FBYztDQUd2QiJ9
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"own_validator_slash_monitor.d.ts","sourceRoot":"","sources":["../src/own_validator_slash_monitor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAE3F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAIhE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AAEhC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;GAKG;AACH,qBAAa,wBAAwB;IAyBjC,OAAO,CAAC,QAAQ,CAAC,gBAAgB;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,GAAG;IA5BtB;;;;;;OAMG;IACH,OAAO,CAAC,KAAK,CAIX;IAEF;;;;;OAKG;IACH,OAAO,CAAC,KAAK,CAAqB;IAElC,OAAO,CAAC,OAAO,CAAS;IAExB,YACmB,gBAAgB,EAAE,wBAAwB,EAC1C,QAAQ,EAAE;QAAE,kBAAkB,EAAE,MAAM,CAAA;KAAE,EACxC,aAAa,EAAE,UAAU,EAAE,EAC3B,OAAO,EAAE,cAAc,EACvB,GAAG,yCAAyC,EAG9D;IAED,OAAO,KAAK,OAAO,GAElB;IAED;;;;OAIG;IACI,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAUvC;IAED,6GAA6G;IAChG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAKjC;IAED;;;OAGG;IACI,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAIzC;IAED;;;OAGG;IACI,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CASrE;IAED,qHAAqH;IAC9G,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,QAAQ,EAAE,UAAU,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,EAAE,WAAW,EAAE,GAAG,GAAG,IAAI,CAa/G;YAGa,YAAY;IA4B1B,OAAO,CAAC,WAAW;IAwBnB,yGAAyG;IACzG,OAAO,CAAC,MAAM;IAMd,OAAO,CAAC,cAAc;CAGvB"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { uniqueBy } from '@aztec/foundation/collection';
|
|
2
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
3
|
+
import { SerialQueue } from '@aztec/foundation/queue';
|
|
4
|
+
/**
|
|
5
|
+
* Watches slashing activity that targets the node's own validators: warns and counts on every vote naming one of
|
|
6
|
+
* them, tracks how close the current round's tally is to quorum, and reports executed slashes. Does nothing when
|
|
7
|
+
* the node runs no validators. Lifecycle calls must be sequential: start() before events are wired, and stop()
|
|
8
|
+
* awaited before any restart.
|
|
9
|
+
*/ export class OwnValidatorSlashMonitor {
|
|
10
|
+
slashingProposer;
|
|
11
|
+
settings;
|
|
12
|
+
ownValidators;
|
|
13
|
+
metrics;
|
|
14
|
+
log;
|
|
15
|
+
/**
|
|
16
|
+
* Tally for the single round being tracked (votes are only ever cast for the current round). `nextVoteIndex` is
|
|
17
|
+
* the cursor: the index of the first vote in the round not yet processed, advanced from the indices the VoteCast
|
|
18
|
+
* events carry. It is undefined until the first event of a round that started mid-round, whose own vote is then
|
|
19
|
+
* the first one counted, so votes cast before the node subscribed are never replayed into the cumulative metrics.
|
|
20
|
+
* The tally is per flattened committee position because that is the unit the contract tallies quorum by.
|
|
21
|
+
*/ state;
|
|
22
|
+
/**
|
|
23
|
+
* All vote processing runs through this queue, one job at a time in arrival order. Serialization is what makes
|
|
24
|
+
* the cursor sound: without it, concurrent event handlers could process the same index twice or emit warnings
|
|
25
|
+
* with out-of-order running tallies. Replaced on every start(), since a queue that has been ended cannot run
|
|
26
|
+
* jobs again.
|
|
27
|
+
*/ queue;
|
|
28
|
+
stopped;
|
|
29
|
+
constructor(slashingProposer, settings, ownValidators, metrics, log = createLogger('slasher:own-validators')){
|
|
30
|
+
this.slashingProposer = slashingProposer;
|
|
31
|
+
this.settings = settings;
|
|
32
|
+
this.ownValidators = ownValidators;
|
|
33
|
+
this.metrics = metrics;
|
|
34
|
+
this.log = log;
|
|
35
|
+
this.state = {
|
|
36
|
+
round: -1n,
|
|
37
|
+
nextVoteIndex: undefined,
|
|
38
|
+
countByPosition: new Map()
|
|
39
|
+
};
|
|
40
|
+
this.queue = new SerialQueue();
|
|
41
|
+
this.stopped = false;
|
|
42
|
+
this.queue.start();
|
|
43
|
+
}
|
|
44
|
+
get enabled() {
|
|
45
|
+
return this.ownValidators.length > 0;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Starts tracking at the given round, leaving the cursor unset so the round's votes already cast are skipped:
|
|
49
|
+
* replaying them would warn about votes the operator can no longer react to sooner, and would double-count
|
|
50
|
+
* cumulative counters across restarts.
|
|
51
|
+
*/ start(currentRound) {
|
|
52
|
+
if (!this.enabled) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
this.stopped = false;
|
|
56
|
+
this.queue = new SerialQueue();
|
|
57
|
+
this.queue.start();
|
|
58
|
+
this.state = {
|
|
59
|
+
round: currentRound,
|
|
60
|
+
nextVoteIndex: undefined,
|
|
61
|
+
countByPosition: new Map()
|
|
62
|
+
};
|
|
63
|
+
this.metrics.recordQuorumSize(this.settings.slashingQuorumSize);
|
|
64
|
+
this.metrics.recordCurrentRoundVotesMax(0);
|
|
65
|
+
}
|
|
66
|
+
/** Stops processing and waits for any in-flight drain, so no warning or metric is emitted after shutdown. */ async stop() {
|
|
67
|
+
this.stopped = true;
|
|
68
|
+
// Jobs already queued are no-ops now that the flag is set, so end() runs through them at no cost while still
|
|
69
|
+
// settling the promises their callers hold, where cancel() would discard them and leave those promises pending.
|
|
70
|
+
await this.queue.end();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Called by the round monitor clock. Strictly greater so the clock catching up to a round an event already rolled
|
|
74
|
+
* to keeps the tally.
|
|
75
|
+
*/ handleNewRound(round) {
|
|
76
|
+
if (this.enabled && round > this.state.round) {
|
|
77
|
+
this.rollTo(round);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Called on each VoteCast event with the index the event carries. Reads every vote from the cursor up to that
|
|
82
|
+
* index, so an event delivery that never arrived is healed by the next one.
|
|
83
|
+
*/ handleVoteCast(round, voteIndex) {
|
|
84
|
+
if (!this.enabled || this.stopped) {
|
|
85
|
+
return Promise.resolve();
|
|
86
|
+
}
|
|
87
|
+
// Errors are logged rather than propagated: callers fire and forget, and a failed read is retried by the drain
|
|
88
|
+
// the next event triggers.
|
|
89
|
+
return this.queue.put(()=>this.drainVotesTo(round, voteIndex)).catch((err)=>this.log.error('Error processing slashing votes', err));
|
|
90
|
+
}
|
|
91
|
+
/** Reports executed slashes against own validators. Called with the Slashed events already fetched by the client. */ handleSlashes(round, slashes, l1BlockHash) {
|
|
92
|
+
if (!this.enabled || this.stopped) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const { attester, amount } of slashes.filter((slash)=>this.isOwnValidator(slash.attester))){
|
|
96
|
+
this.log.warn(`Own validator ${attester} was slashed for ${amount}`, {
|
|
97
|
+
round,
|
|
98
|
+
validator: attester.toString(),
|
|
99
|
+
amount,
|
|
100
|
+
l1BlockHash
|
|
101
|
+
});
|
|
102
|
+
this.metrics.recordOwnValidatorSlashed(amount);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** Processes votes [cursor, voteIndex] of a round, in order, advancing the cursor after each success. */ async drainVotesTo(round, voteIndex) {
|
|
106
|
+
if (this.stopped) {
|
|
107
|
+
return; // enqueued before stop() but not yet started: emit nothing and read nothing after shutdown
|
|
108
|
+
}
|
|
109
|
+
if (round < this.state.round) {
|
|
110
|
+
return; // a vote for a round that already closed can no longer reach quorum
|
|
111
|
+
}
|
|
112
|
+
if (round > this.state.round) {
|
|
113
|
+
this.rollTo(round); // the event beat the round-monitor clock to the boundary
|
|
114
|
+
}
|
|
115
|
+
// The first event of a round tracked from mid-round counts its own vote only: earlier ones predate the subscription
|
|
116
|
+
const from = this.state.nextVoteIndex ?? voteIndex;
|
|
117
|
+
if (voteIndex < from) {
|
|
118
|
+
return; // a duplicate or out-of-order delivery of a vote already processed
|
|
119
|
+
}
|
|
120
|
+
for(let index = from; index <= voteIndex; index++){
|
|
121
|
+
const vote = await this.slashingProposer.getVoteAt(round, index);
|
|
122
|
+
if (this.stopped || round !== this.state.round) {
|
|
123
|
+
return; // stopped, or rolled mid-drain: either way this vote must no longer be counted
|
|
124
|
+
}
|
|
125
|
+
this.processVote(round, vote);
|
|
126
|
+
// Advanced only after successful processing, so a failed read is retried on the next event
|
|
127
|
+
this.state.nextVoteIndex = index + 1n;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
processVote(round, vote) {
|
|
131
|
+
const own = vote.filter((target)=>this.isOwnValidator(target.validator));
|
|
132
|
+
for (const { position } of own){
|
|
133
|
+
this.state.countByPosition.set(position, (this.state.countByPosition.get(position) ?? 0) + 1);
|
|
134
|
+
}
|
|
135
|
+
// Warn once per (vote, validator) at the validator's highest position tally: the operator cares about the
|
|
136
|
+
// validator, not which of its committee seats is being voted on. No slash amount (the amount voted is not
|
|
137
|
+
// necessarily the amount slashed) and no proposer (a catch-up drain cannot attribute votes to proposers).
|
|
138
|
+
const quorum = this.settings.slashingQuorumSize;
|
|
139
|
+
for (const { validator } of uniqueBy(own, (target)=>target.validator.toString())){
|
|
140
|
+
const votes = Math.max(...own.filter((t)=>t.validator.equals(validator)).map((t)=>this.state.countByPosition.get(t.position)));
|
|
141
|
+
this.metrics.recordOwnValidatorTargeted();
|
|
142
|
+
this.log.warn(`Own validator ${validator} targeted by slashing vote (${votes} of ${quorum} votes needed to slash)`, {
|
|
143
|
+
round,
|
|
144
|
+
validator: validator.toString(),
|
|
145
|
+
votes,
|
|
146
|
+
quorum
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
this.metrics.recordCurrentRoundVotesMax(Math.max(0, ...this.state.countByPosition.values()));
|
|
150
|
+
}
|
|
151
|
+
/** Starts a fresh tally, zeroing the gauge so a quiet round does not keep the previous round's value. */ rollTo(round) {
|
|
152
|
+
this.state = {
|
|
153
|
+
round,
|
|
154
|
+
nextVoteIndex: 0n,
|
|
155
|
+
countByPosition: new Map()
|
|
156
|
+
};
|
|
157
|
+
this.metrics.recordCurrentRoundVotesMax(0);
|
|
158
|
+
this.metrics.recordQuorumSize(this.settings.slashingQuorumSize); // keeps the gauge fresh across export cycles
|
|
159
|
+
}
|
|
160
|
+
isOwnValidator(address) {
|
|
161
|
+
return this.ownValidators.some((validator)=>validator.equals(address));
|
|
162
|
+
}
|
|
163
|
+
}
|
package/dest/slasher_client.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EthAddress } from '@aztec/aztec.js/addresses';
|
|
1
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
3
|
import { RollupContract, SlasherContract, SlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
3
4
|
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
@@ -6,6 +7,8 @@ import type { Prettify } from '@aztec/foundation/types';
|
|
|
6
7
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
7
8
|
import { type Offense, type ProposerSlashAction, type ProposerSlashActionProvider } from '@aztec/stdlib/slashing';
|
|
8
9
|
import type { Hex } from 'viem';
|
|
10
|
+
import { SlasherMetrics } from './metrics.js';
|
|
11
|
+
import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
|
|
9
12
|
import { SlashOffensesCollector, type SlashOffensesCollectorConfig, type SlashOffensesCollectorSettings } from './slash_offenses_collector.js';
|
|
10
13
|
import { SlashRoundMonitor, type SlashRoundMonitorSettings } from './slash_round_monitor.js';
|
|
11
14
|
import type { SlasherClientInterface } from './slasher_client_interface.js';
|
|
@@ -58,11 +61,14 @@ export declare class SlasherClient implements ProposerSlashActionProvider, Slash
|
|
|
58
61
|
private epochCache;
|
|
59
62
|
private dateProvider;
|
|
60
63
|
private offensesStore;
|
|
64
|
+
private readonly ownValidators;
|
|
61
65
|
private log;
|
|
66
|
+
private readonly metrics;
|
|
62
67
|
protected unwatchCallbacks: (() => void)[];
|
|
63
68
|
protected roundMonitor: SlashRoundMonitor;
|
|
64
69
|
protected offensesCollector: SlashOffensesCollector;
|
|
65
|
-
|
|
70
|
+
protected readonly ownValidatorMonitor: OwnValidatorSlashMonitor;
|
|
71
|
+
constructor(config: SlasherClientConfig, settings: SlasherSettings, slashingProposer: SlashingProposerContract, slasher: SlasherContract, rollup: RollupContract, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, offensesStore: SlasherOffensesStore, ownValidators?: EthAddress[], log?: import("@aztec/foundation/log").Logger, metrics?: SlasherMetrics);
|
|
66
72
|
start(): Promise<void>;
|
|
67
73
|
/** Stop the slasher client */
|
|
68
74
|
stop(): Promise<void>;
|
|
@@ -72,7 +78,7 @@ export declare class SlasherClient implements ProposerSlashActionProvider, Slash
|
|
|
72
78
|
updateConfig(config: Partial<SlasherConfig>): void;
|
|
73
79
|
/** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */
|
|
74
80
|
protected handleNewRound(round: bigint): Promise<void>;
|
|
75
|
-
/** Called when we see a RoundExecuted event on the SlashingProposer (just for logging). */
|
|
81
|
+
/** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */
|
|
76
82
|
protected handleRoundExecuted(round: bigint, slashCount: bigint, l1BlockHash: Hex): Promise<void>;
|
|
77
83
|
/**
|
|
78
84
|
* Get the actions the proposer should take for slashing
|
|
@@ -109,4 +115,4 @@ export declare class SlasherClient implements ProposerSlashActionProvider, Slash
|
|
|
109
115
|
*/
|
|
110
116
|
private getSlashedRound;
|
|
111
117
|
}
|
|
112
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
118
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hlcl9jbGllbnQuZC50cyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9zbGFzaGVyX2NsaWVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDdkQsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDckQsT0FBTyxFQUFFLGNBQWMsRUFBRSxlQUFlLEVBQUUsd0JBQXdCLEVBQUUsTUFBTSwyQkFBMkIsQ0FBQztBQUV0RyxPQUFPLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFHN0QsT0FBTyxLQUFLLEVBQUUsWUFBWSxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDNUQsT0FBTyxLQUFLLEVBQUUsUUFBUSxFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDeEQsT0FBTyxLQUFLLEVBQUUsYUFBYSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDckUsT0FBTyxFQUNMLEtBQUssT0FBTyxFQUVaLEtBQUssbUJBQW1CLEVBQ3hCLEtBQUssMkJBQTJCLEVBSWpDLE1BQU0sd0JBQXdCLENBQUM7QUFHaEMsT0FBTyxLQUFLLEVBQUUsR0FBRyxFQUFFLE1BQU0sTUFBTSxDQUFDO0FBRWhDLE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxjQUFjLENBQUM7QUFDOUMsT0FBTyxFQUFFLHdCQUF3QixFQUFFLE1BQU0sa0NBQWtDLENBQUM7QUFDNUUsT0FBTyxFQUNMLHNCQUFzQixFQUN0QixLQUFLLDRCQUE0QixFQUNqQyxLQUFLLDhCQUE4QixFQUNwQyxNQUFNLCtCQUErQixDQUFDO0FBQ3ZDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxLQUFLLHlCQUF5QixFQUFFLE1BQU0sMEJBQTBCLENBQUM7QUFDN0YsT0FBTyxLQUFLLEVBQUUsc0JBQXNCLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUM1RSxPQUFPLEtBQUssRUFBRSxvQkFBb0Is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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slasher_client.d.ts","sourceRoot":"","sources":["../src/slasher_client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"slasher_client.d.ts","sourceRoot":"","sources":["../src/slasher_client.ts"],"names":[],"mappings":"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"}
|
package/dest/slasher_client.js
CHANGED
|
@@ -3,6 +3,9 @@ import { maxBigint } from '@aztec/foundation/bigint';
|
|
|
3
3
|
import { compactArray, partition, times } from '@aztec/foundation/collection';
|
|
4
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
5
5
|
import { OffenseType, getEpochsForRound, getOffenseTypeName, getSlashConsensusVotesFromOffenses } from '@aztec/stdlib/slashing';
|
|
6
|
+
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
7
|
+
import { SlasherMetrics } from './metrics.js';
|
|
8
|
+
import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
|
|
6
9
|
import { SlashOffensesCollector } from './slash_offenses_collector.js';
|
|
7
10
|
import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
8
11
|
/**
|
|
@@ -39,11 +42,14 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
39
42
|
epochCache;
|
|
40
43
|
dateProvider;
|
|
41
44
|
offensesStore;
|
|
45
|
+
ownValidators;
|
|
42
46
|
log;
|
|
47
|
+
metrics;
|
|
43
48
|
unwatchCallbacks;
|
|
44
49
|
roundMonitor;
|
|
45
50
|
offensesCollector;
|
|
46
|
-
|
|
51
|
+
ownValidatorMonitor;
|
|
52
|
+
constructor(config, settings, slashingProposer, slasher, rollup, watchers, epochCache, dateProvider, offensesStore, ownValidators = [], log = createLogger('slasher:consensus'), metrics = new SlasherMetrics(getTelemetryClient())){
|
|
47
53
|
this.config = config;
|
|
48
54
|
this.settings = settings;
|
|
49
55
|
this.slashingProposer = slashingProposer;
|
|
@@ -52,10 +58,13 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
52
58
|
this.epochCache = epochCache;
|
|
53
59
|
this.dateProvider = dateProvider;
|
|
54
60
|
this.offensesStore = offensesStore;
|
|
61
|
+
this.ownValidators = ownValidators;
|
|
55
62
|
this.log = log;
|
|
63
|
+
this.metrics = metrics;
|
|
56
64
|
this.unwatchCallbacks = [];
|
|
57
65
|
this.roundMonitor = new SlashRoundMonitor(settings, dateProvider);
|
|
58
66
|
this.offensesCollector = new SlashOffensesCollector(config, settings, watchers, offensesStore);
|
|
67
|
+
this.ownValidatorMonitor = new OwnValidatorSlashMonitor(slashingProposer, settings, ownValidators, this.metrics);
|
|
59
68
|
}
|
|
60
69
|
async start() {
|
|
61
70
|
this.log.debug('Starting slasher client...');
|
|
@@ -65,6 +74,12 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
65
74
|
this.unwatchCallbacks.push(this.slashingProposer.listenToRoundExecuted(({ round, slashCount, l1BlockHash })=>void this.handleRoundExecuted(round, slashCount, l1BlockHash).catch((err)=>this.log.error('Error handling round executed', err))));
|
|
66
75
|
// Check for round changes
|
|
67
76
|
this.unwatchCallbacks.push(this.roundMonitor.listenToNewRound((round)=>this.handleNewRound(round)));
|
|
77
|
+
// Warn early when a slashing vote names one of our own validators. Skipped entirely when running none, so
|
|
78
|
+
// those nodes pay no extra L1 subscription.
|
|
79
|
+
if (this.ownValidators.length > 0) {
|
|
80
|
+
this.ownValidatorMonitor.start(this.roundMonitor.getCurrentRound().round);
|
|
81
|
+
this.unwatchCallbacks.push(this.slashingProposer.listenToVoteCast(({ round, voteIndex })=>void this.ownValidatorMonitor.handleVoteCast(round, voteIndex)));
|
|
82
|
+
}
|
|
68
83
|
this.log.info(`Started slasher client`);
|
|
69
84
|
return Promise.resolve();
|
|
70
85
|
}
|
|
@@ -74,6 +89,7 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
74
89
|
unwatchCallback();
|
|
75
90
|
}
|
|
76
91
|
this.roundMonitor.stop();
|
|
92
|
+
await this.ownValidatorMonitor.stop();
|
|
77
93
|
await this.offensesCollector.stop();
|
|
78
94
|
this.log.info('Slasher client stopped');
|
|
79
95
|
}
|
|
@@ -88,13 +104,16 @@ import { SlashRoundMonitor } from './slash_round_monitor.js';
|
|
|
88
104
|
}
|
|
89
105
|
/** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */ async handleNewRound(round) {
|
|
90
106
|
this.log.info(`Starting new slashing round ${round}`);
|
|
107
|
+
this.ownValidatorMonitor.handleNewRound(round);
|
|
91
108
|
await this.offensesCollector.handleNewRound(round);
|
|
92
109
|
}
|
|
93
|
-
/** Called when we see a RoundExecuted event on the SlashingProposer (just for logging). */ async handleRoundExecuted(round, slashCount, l1BlockHash) {
|
|
110
|
+
/** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */ async handleRoundExecuted(round, slashCount, l1BlockHash) {
|
|
111
|
+
this.metrics.recordRoundExecuted();
|
|
94
112
|
const slashes = await this.rollup.getSlashEvents(l1BlockHash);
|
|
95
113
|
this.log.info(`Slashing round ${round} has been executed with ${slashCount} slashes`, {
|
|
96
114
|
slashes
|
|
97
115
|
});
|
|
116
|
+
this.ownValidatorMonitor.handleSlashes(round, slashes, l1BlockHash);
|
|
98
117
|
}
|
|
99
118
|
/**
|
|
100
119
|
* Get the actions the proposer should take for slashing
|
|
@@ -2,6 +2,7 @@ import { EpochCache } from '@aztec/epoch-cache';
|
|
|
2
2
|
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
3
|
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
4
|
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
6
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
6
7
|
import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
|
|
7
8
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
@@ -23,12 +24,13 @@ export declare class SlasherClientFacade implements SlasherClientInterface {
|
|
|
23
24
|
private dateProvider;
|
|
24
25
|
private kvStore;
|
|
25
26
|
private rollupRegisteredAtL2Slot;
|
|
27
|
+
private ownValidators;
|
|
26
28
|
private logger;
|
|
27
29
|
private client;
|
|
28
30
|
private unwatch;
|
|
29
31
|
constructor(config: SlasherConfig & DataStoreConfig & {
|
|
30
32
|
ethereumSlotDuration: number;
|
|
31
|
-
}, rollup: RollupContract, l1Client: ViemClient, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, kvStore: AztecLMDBStoreV2, rollupRegisteredAtL2Slot: SlotNumber, logger?: import("@aztec/foundation/log").Logger);
|
|
33
|
+
}, rollup: RollupContract, l1Client: ViemClient, watchers: Watcher[], epochCache: EpochCache, dateProvider: DateProvider, kvStore: AztecLMDBStoreV2, rollupRegisteredAtL2Slot: SlotNumber, ownValidators?: EthAddress[], logger?: import("@aztec/foundation/log").Logger);
|
|
32
34
|
start(): Promise<void>;
|
|
33
35
|
stop(): Promise<void>;
|
|
34
36
|
getConfig(): SlasherConfig;
|
|
@@ -39,4 +41,4 @@ export declare class SlasherClientFacade implements SlasherClientInterface {
|
|
|
39
41
|
private createSlasherClient;
|
|
40
42
|
private handleSlasherChange;
|
|
41
43
|
}
|
|
42
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
44
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2xhc2hlcl9jbGllbnRfZmFjYWRlLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvc2xhc2hlcl9jbGllbnRfZmFjYWRlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxVQUFVLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUNoRCxPQUFPLEVBQUUsY0FBYyxFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDM0QsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDeEQsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0saUNBQWlDLENBQUM7QUFDbEUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFFaEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sRUFBRSxnQkFBZ0IsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzNELE9BQU8sS0FBSyxFQUFFLGFBQWEsRUFBRSxNQUFNLGlDQUFpQyxDQUFDO0FBQ3JFLE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQzlELE9BQU8sS0FBSyxFQUFFLE9BQU8sRUFBRSxtQkFBbUIsRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBRzNFLE9BQU8sS0FBSyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sK0JBQStCLENBQUM7QUFDNUUsT0FBTyxLQUFLLEVBQUUsT0FBTyxFQUFFLE1BQU0sY0FBYyxDQUFDO0FBRTVDOzs7O0dBSUc7QUFDSCxxQkFBYSxtQkFBb0IsWUFBVyxzQkFBc0I7SUFLOUQsT0FBTyxDQUFDLE1BQU07SUFDZCxPQUFPLENBQUMsTUFBTTtJQUNkLE9BQU8sQ0FBQyxRQUFRO0lBQ2hCLE9BQU8sQ0FBQyxRQUFRO0lBQ2hCLE9BQU8sQ0FBQyxVQUFVO0lBQ2xCLE9BQU8sQ0FBQyxZQUFZO0lBQ3BCLE9BQU8sQ0FBQyxPQUFPO0lBQ2YsT0FBTyxDQUFDLHdCQUF3QjtJQUNoQyxPQUFPLENBQUMsYUFBYTtJQUNyQixPQUFPLENBQUMsTUFBTTtJQWJoQixPQUFPLENBQUMsTUFBTSxDQUFxQztJQUNuRCxPQUFPLENBQUMsT0FBTyxDQUEyQjtJQUUxQyxZQUNVLE1BQU0sRUFBRSxhQUFhLEdBQUcsZUFBZSxHQUFHO1FBQUUsb0JBQW9CLEVBQUUsTUFBTSxDQUFBO0tBQUUsRUFDMUUsTUFBTSxFQUFFLGNBQWMsRUFDdEIsUUFBUSxFQUFFLFVBQVUsRUFDcEIsUUFBUSxFQUFFLE9BQU8sRUFBRSxFQUNuQixVQUFVLEVBQUUsVUFBVSxFQUN0QixZQUFZLEVBQUUsWUFBWSxFQUMxQixPQUFPLEVBQUUsZ0JBQWdCLEVBQ3pCLHdCQUF3QixFQUFFLFVBQVUsRUFDcEMsYUFBYSxHQUFFLFVBQVUsRUFBTyxFQUNoQyxNQUFNLHlDQUEwQixFQUN0QztJQUVTLEtBQUssSUFBSSxPQUFPLENBQUMsSUFBSSxDQUFDLENBU2xDO0lBRVksSUFBSSxJQUFJLE9BQU8sQ0FBQyxJQUFJLENBQUMsQ0FJakM7SUFFTSxTQUFTLElBQUksYUFBYSxDQUVoQztJQUVNLFlBQVksQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxHQUFHLElBQUksQ0FJeEQ7SUFFTSxzQkFBc0IsQ0FBQyxLQUFLLENBQUMsRUFBRSxNQUFNLEdBQUcsT0FBTyxDQUFDLE9BQU8sRUFBRSxDQUFDLENBRWhFO0lBRU0sV0FBVyxJQUFJLE9BQU8sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUV2QztJQUVNLGtCQUFrQixDQUFDLFVBQVUsRUFBRSxVQUFVLEdBQUcsT0FBTyxDQUFDLG1CQUFtQixFQUFFLENBQUMsQ0FFaEY7SUFFRCxPQUFPLENBQUMsbUJBQW1CO1lBZWIsbUJBQW1CO0NBTWxDIn0=
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slasher_client_facade.d.ts","sourceRoot":"","sources":["../src/slasher_client_facade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"slasher_client_facade.d.ts","sourceRoot":"","sources":["../src/slasher_client_facade.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAEhE,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAG3E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;GAIG;AACH,qBAAa,mBAAoB,YAAW,sBAAsB;IAK9D,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IACf,OAAO,CAAC,wBAAwB;IAChC,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,MAAM;IAbhB,OAAO,CAAC,MAAM,CAAqC;IACnD,OAAO,CAAC,OAAO,CAA2B;IAE1C,YACU,MAAM,EAAE,aAAa,GAAG,eAAe,GAAG;QAAE,oBAAoB,EAAE,MAAM,CAAA;KAAE,EAC1E,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,OAAO,EAAE,EACnB,UAAU,EAAE,UAAU,EACtB,YAAY,EAAE,YAAY,EAC1B,OAAO,EAAE,gBAAgB,EACzB,wBAAwB,EAAE,UAAU,EACpC,aAAa,GAAE,UAAU,EAAO,EAChC,MAAM,yCAA0B,EACtC;IAES,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CASlC;IAEY,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAIjC;IAEM,SAAS,IAAI,aAAa,CAEhC;IAEM,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,CAAC,GAAG,IAAI,CAIxD;IAEM,sBAAsB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAEhE;IAEM,WAAW,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAEvC;IAEM,kBAAkB,CAAC,UAAU,EAAE,UAAU,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAEhF;IAED,OAAO,CAAC,mBAAmB;YAeb,mBAAmB;CAMlC"}
|
|
@@ -13,10 +13,11 @@ import { createSlasherImplementation } from './factory/create_implementation.js'
|
|
|
13
13
|
dateProvider;
|
|
14
14
|
kvStore;
|
|
15
15
|
rollupRegisteredAtL2Slot;
|
|
16
|
+
ownValidators;
|
|
16
17
|
logger;
|
|
17
18
|
client;
|
|
18
19
|
unwatch;
|
|
19
|
-
constructor(config, rollup, l1Client, watchers, epochCache, dateProvider, kvStore, rollupRegisteredAtL2Slot, logger = createLogger('slasher')){
|
|
20
|
+
constructor(config, rollup, l1Client, watchers, epochCache, dateProvider, kvStore, rollupRegisteredAtL2Slot, ownValidators = [], logger = createLogger('slasher')){
|
|
20
21
|
this.config = config;
|
|
21
22
|
this.rollup = rollup;
|
|
22
23
|
this.l1Client = l1Client;
|
|
@@ -25,6 +26,7 @@ import { createSlasherImplementation } from './factory/create_implementation.js'
|
|
|
25
26
|
this.dateProvider = dateProvider;
|
|
26
27
|
this.kvStore = kvStore;
|
|
27
28
|
this.rollupRegisteredAtL2Slot = rollupRegisteredAtL2Slot;
|
|
29
|
+
this.ownValidators = ownValidators;
|
|
28
30
|
this.logger = logger;
|
|
29
31
|
}
|
|
30
32
|
async start() {
|
|
@@ -62,7 +64,7 @@ import { createSlasherImplementation } from './factory/create_implementation.js'
|
|
|
62
64
|
return this.client?.getProposerActions(slotNumber) ?? Promise.reject(new Error('Slasher client not initialized'));
|
|
63
65
|
}
|
|
64
66
|
createSlasherClient() {
|
|
65
|
-
return createSlasherImplementation(this.config, this.rollup, this.l1Client, this.watchers, this.epochCache, this.dateProvider, this.kvStore, this.rollupRegisteredAtL2Slot, this.logger);
|
|
67
|
+
return createSlasherImplementation(this.config, this.rollup, this.l1Client, this.watchers, this.epochCache, this.dateProvider, this.kvStore, this.rollupRegisteredAtL2Slot, this.ownValidators, this.logger);
|
|
66
68
|
}
|
|
67
69
|
async handleSlasherChange() {
|
|
68
70
|
this.logger.warn('Slasher contract changed, recreating slasher client');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/slasher",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.9a89641",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
@@ -56,20 +56,20 @@
|
|
|
56
56
|
]
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@aztec/epoch-cache": "0.0.1-commit.
|
|
60
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
61
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
62
|
-
"@aztec/kv-store": "0.0.1-commit.
|
|
63
|
-
"@aztec/l1-artifacts": "0.0
|
|
64
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
65
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
59
|
+
"@aztec/epoch-cache": "0.0.1-commit.9a89641",
|
|
60
|
+
"@aztec/ethereum": "0.0.1-commit.9a89641",
|
|
61
|
+
"@aztec/foundation": "0.0.1-commit.9a89641",
|
|
62
|
+
"@aztec/kv-store": "0.0.1-commit.9a89641",
|
|
63
|
+
"@aztec/l1-artifacts": "6.0.0-nightly.20260807",
|
|
64
|
+
"@aztec/stdlib": "0.0.1-commit.9a89641",
|
|
65
|
+
"@aztec/telemetry-client": "0.0.1-commit.9a89641",
|
|
66
66
|
"source-map-support": "^0.5.21",
|
|
67
67
|
"tslib": "^2.4.0",
|
|
68
68
|
"viem": "npm:@aztec/viem@2.38.2",
|
|
69
69
|
"zod": "^4"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
|
-
"@aztec/aztec.js": "0.0.1-commit.
|
|
72
|
+
"@aztec/aztec.js": "0.0.1-commit.9a89641",
|
|
73
73
|
"@jest/globals": "^30.0.0",
|
|
74
74
|
"@types/jest": "^30.0.0",
|
|
75
75
|
"@types/node": "^22.15.17",
|
|
@@ -25,7 +25,10 @@ export async function createSlasherFacade(
|
|
|
25
25
|
watchers: Watcher[],
|
|
26
26
|
dateProvider: DateProvider,
|
|
27
27
|
epochCache: EpochCache,
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* List of own validator addresses. Added to the slashValidatorNever list unless slashSelfAllowed is true, and used
|
|
30
|
+
* (independently of slashSelfAllowed) to warn and record metrics when they are targeted by onchain slashing.
|
|
31
|
+
*/
|
|
29
32
|
validatorAddresses: EthAddress[] = [],
|
|
30
33
|
logger = createLogger('slasher'),
|
|
31
34
|
): Promise<SlasherClientInterface> {
|
|
@@ -76,6 +79,7 @@ export async function createSlasherFacade(
|
|
|
76
79
|
dateProvider,
|
|
77
80
|
kvStore,
|
|
78
81
|
rollupRegisteredAtL2Slot,
|
|
82
|
+
validatorAddresses,
|
|
79
83
|
logger,
|
|
80
84
|
);
|
|
81
85
|
}
|
|
@@ -2,6 +2,7 @@ import { EpochCache } from '@aztec/epoch-cache';
|
|
|
2
2
|
import { RollupContract, SlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
3
3
|
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
4
|
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
7
8
|
import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
|
|
@@ -24,6 +25,7 @@ export async function createSlasherImplementation(
|
|
|
24
25
|
dateProvider: DateProvider,
|
|
25
26
|
kvStore: AztecLMDBStoreV2,
|
|
26
27
|
rollupRegisteredAtL2Slot: SlotNumber,
|
|
28
|
+
ownValidators: EthAddress[] = [],
|
|
27
29
|
logger = createLogger('slasher'),
|
|
28
30
|
) {
|
|
29
31
|
const proposer = await rollup.getSlashingProposer();
|
|
@@ -39,6 +41,7 @@ export async function createSlasherImplementation(
|
|
|
39
41
|
epochCache,
|
|
40
42
|
kvStore,
|
|
41
43
|
rollupRegisteredAtL2Slot,
|
|
44
|
+
ownValidators,
|
|
42
45
|
logger,
|
|
43
46
|
);
|
|
44
47
|
}
|
|
@@ -53,6 +56,7 @@ async function createSlasher(
|
|
|
53
56
|
epochCache: EpochCache,
|
|
54
57
|
kvStore: AztecLMDBStoreV2,
|
|
55
58
|
rollupRegisteredAtL2Slot: SlotNumber,
|
|
59
|
+
ownValidators: EthAddress[] = [],
|
|
56
60
|
logger = createLogger('slasher'),
|
|
57
61
|
): Promise<SlasherClient> {
|
|
58
62
|
const settings = { ...(await getSlasherSettings(rollup, slashingProposer)), rollupRegisteredAtL2Slot };
|
|
@@ -73,6 +77,7 @@ async function createSlasher(
|
|
|
73
77
|
epochCache,
|
|
74
78
|
dateProvider,
|
|
75
79
|
offensesStore,
|
|
80
|
+
ownValidators,
|
|
76
81
|
logger,
|
|
77
82
|
);
|
|
78
83
|
}
|
package/src/metrics.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Gauge,
|
|
3
|
+
Metrics,
|
|
4
|
+
type TelemetryClient,
|
|
5
|
+
type UpDownCounter,
|
|
6
|
+
createUpDownCounterWithDefault,
|
|
7
|
+
} from '@aztec/telemetry-client';
|
|
8
|
+
|
|
9
|
+
import { formatEther } from 'viem/utils';
|
|
10
|
+
|
|
11
|
+
export class SlasherMetrics {
|
|
12
|
+
private readonly roundExecuted: UpDownCounter;
|
|
13
|
+
private readonly ownValidatorTargeted: UpDownCounter;
|
|
14
|
+
private readonly ownValidatorSlashedCount: UpDownCounter;
|
|
15
|
+
private readonly ownValidatorSlashedAmount: UpDownCounter;
|
|
16
|
+
private readonly ownValidatorCurrentRoundVotesMax: Gauge;
|
|
17
|
+
private readonly quorumSize: Gauge;
|
|
18
|
+
|
|
19
|
+
constructor(client: TelemetryClient, name = 'Slasher') {
|
|
20
|
+
const meter = client.getMeter(name);
|
|
21
|
+
this.roundExecuted = createUpDownCounterWithDefault(meter, Metrics.SLASHER_ROUND_EXECUTED_COUNT);
|
|
22
|
+
this.ownValidatorTargeted = createUpDownCounterWithDefault(meter, Metrics.SLASHER_OWN_VALIDATOR_TARGETED_COUNT);
|
|
23
|
+
this.ownValidatorSlashedCount = createUpDownCounterWithDefault(meter, Metrics.SLASHER_OWN_VALIDATOR_SLASHED_COUNT);
|
|
24
|
+
this.ownValidatorSlashedAmount = createUpDownCounterWithDefault(
|
|
25
|
+
meter,
|
|
26
|
+
Metrics.SLASHER_OWN_VALIDATOR_SLASHED_AMOUNT,
|
|
27
|
+
);
|
|
28
|
+
this.ownValidatorCurrentRoundVotesMax = meter.createGauge(Metrics.SLASHER_OWN_VALIDATOR_CURRENT_ROUND_VOTES_MAX);
|
|
29
|
+
this.quorumSize = meter.createGauge(Metrics.SLASHER_QUORUM_SIZE);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public recordRoundExecuted(): void {
|
|
33
|
+
this.roundExecuted.add(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Records the quorum a validator must reach in a round to be slashed, so dashboards can plot the threshold. */
|
|
37
|
+
public recordQuorumSize(quorum: number): void {
|
|
38
|
+
this.quorumSize.record(quorum);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Records that an onchain slashing vote named one of the node's own validators as a target. */
|
|
42
|
+
public recordOwnValidatorTargeted(): void {
|
|
43
|
+
this.ownValidatorTargeted.add(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Records how close the most-voted committee position held by the node's own validators is to quorum this round.
|
|
48
|
+
* Recorded as an absolute value rather than a delta so a vote seen across a round rollover cannot make it drift.
|
|
49
|
+
*/
|
|
50
|
+
public recordCurrentRoundVotesMax(votes: number): void {
|
|
51
|
+
this.ownValidatorCurrentRoundVotesMax.record(votes);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Records an executed slash against one of the node's own validators. */
|
|
55
|
+
public recordOwnValidatorSlashed(amount: bigint): void {
|
|
56
|
+
this.ownValidatorSlashedCount.add(1);
|
|
57
|
+
this.ownValidatorSlashedAmount.add(parseFloat(formatEther(amount)));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { SlashVoteTarget, SlashingProposerContract } from '@aztec/ethereum/contracts';
|
|
2
|
+
import { uniqueBy } from '@aztec/foundation/collection';
|
|
3
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
4
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
5
|
+
import { SerialQueue } from '@aztec/foundation/queue';
|
|
6
|
+
|
|
7
|
+
import type { Hex } from 'viem';
|
|
8
|
+
|
|
9
|
+
import type { SlasherMetrics } from './metrics.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Watches slashing activity that targets the node's own validators: warns and counts on every vote naming one of
|
|
13
|
+
* them, tracks how close the current round's tally is to quorum, and reports executed slashes. Does nothing when
|
|
14
|
+
* the node runs no validators. Lifecycle calls must be sequential: start() before events are wired, and stop()
|
|
15
|
+
* awaited before any restart.
|
|
16
|
+
*/
|
|
17
|
+
export class OwnValidatorSlashMonitor {
|
|
18
|
+
/**
|
|
19
|
+
* Tally for the single round being tracked (votes are only ever cast for the current round). `nextVoteIndex` is
|
|
20
|
+
* the cursor: the index of the first vote in the round not yet processed, advanced from the indices the VoteCast
|
|
21
|
+
* events carry. It is undefined until the first event of a round that started mid-round, whose own vote is then
|
|
22
|
+
* the first one counted, so votes cast before the node subscribed are never replayed into the cumulative metrics.
|
|
23
|
+
* The tally is per flattened committee position because that is the unit the contract tallies quorum by.
|
|
24
|
+
*/
|
|
25
|
+
private state: { round: bigint; nextVoteIndex: bigint | undefined; countByPosition: Map<number, number> } = {
|
|
26
|
+
round: -1n,
|
|
27
|
+
nextVoteIndex: undefined,
|
|
28
|
+
countByPosition: new Map(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* All vote processing runs through this queue, one job at a time in arrival order. Serialization is what makes
|
|
33
|
+
* the cursor sound: without it, concurrent event handlers could process the same index twice or emit warnings
|
|
34
|
+
* with out-of-order running tallies. Replaced on every start(), since a queue that has been ended cannot run
|
|
35
|
+
* jobs again.
|
|
36
|
+
*/
|
|
37
|
+
private queue = new SerialQueue();
|
|
38
|
+
|
|
39
|
+
private stopped = false;
|
|
40
|
+
|
|
41
|
+
constructor(
|
|
42
|
+
private readonly slashingProposer: SlashingProposerContract,
|
|
43
|
+
private readonly settings: { slashingQuorumSize: number },
|
|
44
|
+
private readonly ownValidators: EthAddress[],
|
|
45
|
+
private readonly metrics: SlasherMetrics,
|
|
46
|
+
private readonly log = createLogger('slasher:own-validators'),
|
|
47
|
+
) {
|
|
48
|
+
this.queue.start();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private get enabled(): boolean {
|
|
52
|
+
return this.ownValidators.length > 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Starts tracking at the given round, leaving the cursor unset so the round's votes already cast are skipped:
|
|
57
|
+
* replaying them would warn about votes the operator can no longer react to sooner, and would double-count
|
|
58
|
+
* cumulative counters across restarts.
|
|
59
|
+
*/
|
|
60
|
+
public start(currentRound: bigint): void {
|
|
61
|
+
if (!this.enabled) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this.stopped = false;
|
|
65
|
+
this.queue = new SerialQueue();
|
|
66
|
+
this.queue.start();
|
|
67
|
+
this.state = { round: currentRound, nextVoteIndex: undefined, countByPosition: new Map() };
|
|
68
|
+
this.metrics.recordQuorumSize(this.settings.slashingQuorumSize);
|
|
69
|
+
this.metrics.recordCurrentRoundVotesMax(0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Stops processing and waits for any in-flight drain, so no warning or metric is emitted after shutdown. */
|
|
73
|
+
public async stop(): Promise<void> {
|
|
74
|
+
this.stopped = true;
|
|
75
|
+
// Jobs already queued are no-ops now that the flag is set, so end() runs through them at no cost while still
|
|
76
|
+
// settling the promises their callers hold, where cancel() would discard them and leave those promises pending.
|
|
77
|
+
await this.queue.end();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Called by the round monitor clock. Strictly greater so the clock catching up to a round an event already rolled
|
|
82
|
+
* to keeps the tally.
|
|
83
|
+
*/
|
|
84
|
+
public handleNewRound(round: bigint): void {
|
|
85
|
+
if (this.enabled && round > this.state.round) {
|
|
86
|
+
this.rollTo(round);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Called on each VoteCast event with the index the event carries. Reads every vote from the cursor up to that
|
|
92
|
+
* index, so an event delivery that never arrived is healed by the next one.
|
|
93
|
+
*/
|
|
94
|
+
public handleVoteCast(round: bigint, voteIndex: bigint): Promise<void> {
|
|
95
|
+
if (!this.enabled || this.stopped) {
|
|
96
|
+
return Promise.resolve();
|
|
97
|
+
}
|
|
98
|
+
// Errors are logged rather than propagated: callers fire and forget, and a failed read is retried by the drain
|
|
99
|
+
// the next event triggers.
|
|
100
|
+
return this.queue
|
|
101
|
+
.put(() => this.drainVotesTo(round, voteIndex))
|
|
102
|
+
.catch(err => this.log.error('Error processing slashing votes', err));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Reports executed slashes against own validators. Called with the Slashed events already fetched by the client. */
|
|
106
|
+
public handleSlashes(round: bigint, slashes: { attester: EthAddress; amount: bigint }[], l1BlockHash: Hex): void {
|
|
107
|
+
if (!this.enabled || this.stopped) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
for (const { attester, amount } of slashes.filter(slash => this.isOwnValidator(slash.attester))) {
|
|
111
|
+
this.log.warn(`Own validator ${attester} was slashed for ${amount}`, {
|
|
112
|
+
round,
|
|
113
|
+
validator: attester.toString(),
|
|
114
|
+
amount,
|
|
115
|
+
l1BlockHash,
|
|
116
|
+
});
|
|
117
|
+
this.metrics.recordOwnValidatorSlashed(amount);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Processes votes [cursor, voteIndex] of a round, in order, advancing the cursor after each success. */
|
|
122
|
+
private async drainVotesTo(round: bigint, voteIndex: bigint): Promise<void> {
|
|
123
|
+
if (this.stopped) {
|
|
124
|
+
return; // enqueued before stop() but not yet started: emit nothing and read nothing after shutdown
|
|
125
|
+
}
|
|
126
|
+
if (round < this.state.round) {
|
|
127
|
+
return; // a vote for a round that already closed can no longer reach quorum
|
|
128
|
+
}
|
|
129
|
+
if (round > this.state.round) {
|
|
130
|
+
this.rollTo(round); // the event beat the round-monitor clock to the boundary
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The first event of a round tracked from mid-round counts its own vote only: earlier ones predate the subscription
|
|
134
|
+
const from = this.state.nextVoteIndex ?? voteIndex;
|
|
135
|
+
if (voteIndex < from) {
|
|
136
|
+
return; // a duplicate or out-of-order delivery of a vote already processed
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (let index = from; index <= voteIndex; index++) {
|
|
140
|
+
const vote = await this.slashingProposer.getVoteAt(round, index);
|
|
141
|
+
if (this.stopped || round !== this.state.round) {
|
|
142
|
+
return; // stopped, or rolled mid-drain: either way this vote must no longer be counted
|
|
143
|
+
}
|
|
144
|
+
this.processVote(round, vote);
|
|
145
|
+
// Advanced only after successful processing, so a failed read is retried on the next event
|
|
146
|
+
this.state.nextVoteIndex = index + 1n;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private processVote(round: bigint, vote: SlashVoteTarget[]): void {
|
|
151
|
+
const own = vote.filter(target => this.isOwnValidator(target.validator));
|
|
152
|
+
for (const { position } of own) {
|
|
153
|
+
this.state.countByPosition.set(position, (this.state.countByPosition.get(position) ?? 0) + 1);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Warn once per (vote, validator) at the validator's highest position tally: the operator cares about the
|
|
157
|
+
// validator, not which of its committee seats is being voted on. No slash amount (the amount voted is not
|
|
158
|
+
// necessarily the amount slashed) and no proposer (a catch-up drain cannot attribute votes to proposers).
|
|
159
|
+
const quorum = this.settings.slashingQuorumSize;
|
|
160
|
+
for (const { validator } of uniqueBy(own, target => target.validator.toString())) {
|
|
161
|
+
const votes = Math.max(
|
|
162
|
+
...own.filter(t => t.validator.equals(validator)).map(t => this.state.countByPosition.get(t.position)!),
|
|
163
|
+
);
|
|
164
|
+
this.metrics.recordOwnValidatorTargeted();
|
|
165
|
+
this.log.warn(
|
|
166
|
+
`Own validator ${validator} targeted by slashing vote (${votes} of ${quorum} votes needed to slash)`,
|
|
167
|
+
{ round, validator: validator.toString(), votes, quorum },
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.metrics.recordCurrentRoundVotesMax(Math.max(0, ...this.state.countByPosition.values()));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Starts a fresh tally, zeroing the gauge so a quiet round does not keep the previous round's value. */
|
|
175
|
+
private rollTo(round: bigint): void {
|
|
176
|
+
this.state = { round, nextVoteIndex: 0n, countByPosition: new Map() };
|
|
177
|
+
this.metrics.recordCurrentRoundVotesMax(0);
|
|
178
|
+
this.metrics.recordQuorumSize(this.settings.slashingQuorumSize); // keeps the gauge fresh across export cycles
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private isOwnValidator(address: EthAddress): boolean {
|
|
182
|
+
return this.ownValidators.some(validator => validator.equals(address));
|
|
183
|
+
}
|
|
184
|
+
}
|
package/src/slasher_client.ts
CHANGED
|
@@ -17,9 +17,12 @@ import {
|
|
|
17
17
|
getOffenseTypeName,
|
|
18
18
|
getSlashConsensusVotesFromOffenses,
|
|
19
19
|
} from '@aztec/stdlib/slashing';
|
|
20
|
+
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
20
21
|
|
|
21
22
|
import type { Hex } from 'viem';
|
|
22
23
|
|
|
24
|
+
import { SlasherMetrics } from './metrics.js';
|
|
25
|
+
import { OwnValidatorSlashMonitor } from './own_validator_slash_monitor.js';
|
|
23
26
|
import {
|
|
24
27
|
SlashOffensesCollector,
|
|
25
28
|
type SlashOffensesCollectorConfig,
|
|
@@ -89,6 +92,7 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
|
|
|
89
92
|
protected unwatchCallbacks: (() => void)[] = [];
|
|
90
93
|
protected roundMonitor: SlashRoundMonitor;
|
|
91
94
|
protected offensesCollector: SlashOffensesCollector;
|
|
95
|
+
protected readonly ownValidatorMonitor: OwnValidatorSlashMonitor;
|
|
92
96
|
|
|
93
97
|
constructor(
|
|
94
98
|
private config: SlasherClientConfig,
|
|
@@ -100,10 +104,13 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
|
|
|
100
104
|
private epochCache: EpochCache,
|
|
101
105
|
private dateProvider: DateProvider,
|
|
102
106
|
private offensesStore: SlasherOffensesStore,
|
|
107
|
+
private readonly ownValidators: EthAddress[] = [],
|
|
103
108
|
private log = createLogger('slasher:consensus'),
|
|
109
|
+
private readonly metrics = new SlasherMetrics(getTelemetryClient()),
|
|
104
110
|
) {
|
|
105
111
|
this.roundMonitor = new SlashRoundMonitor(settings, dateProvider);
|
|
106
112
|
this.offensesCollector = new SlashOffensesCollector(config, settings, watchers, offensesStore);
|
|
113
|
+
this.ownValidatorMonitor = new OwnValidatorSlashMonitor(slashingProposer, settings, ownValidators, this.metrics);
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
public async start() {
|
|
@@ -125,6 +132,17 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
|
|
|
125
132
|
// Check for round changes
|
|
126
133
|
this.unwatchCallbacks.push(this.roundMonitor.listenToNewRound(round => this.handleNewRound(round)));
|
|
127
134
|
|
|
135
|
+
// Warn early when a slashing vote names one of our own validators. Skipped entirely when running none, so
|
|
136
|
+
// those nodes pay no extra L1 subscription.
|
|
137
|
+
if (this.ownValidators.length > 0) {
|
|
138
|
+
this.ownValidatorMonitor.start(this.roundMonitor.getCurrentRound().round);
|
|
139
|
+
this.unwatchCallbacks.push(
|
|
140
|
+
this.slashingProposer.listenToVoteCast(
|
|
141
|
+
({ round, voteIndex }) => void this.ownValidatorMonitor.handleVoteCast(round, voteIndex),
|
|
142
|
+
),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
128
146
|
this.log.info(`Started slasher client`);
|
|
129
147
|
return Promise.resolve();
|
|
130
148
|
}
|
|
@@ -138,6 +156,7 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
|
|
|
138
156
|
}
|
|
139
157
|
|
|
140
158
|
this.roundMonitor.stop();
|
|
159
|
+
await this.ownValidatorMonitor.stop();
|
|
141
160
|
await this.offensesCollector.stop();
|
|
142
161
|
|
|
143
162
|
this.log.info('Slasher client stopped');
|
|
@@ -156,13 +175,16 @@ export class SlasherClient implements ProposerSlashActionProvider, SlasherClient
|
|
|
156
175
|
/** Triggered on a time basis when we enter a new slashing round. Clears expired offenses. */
|
|
157
176
|
protected async handleNewRound(round: bigint) {
|
|
158
177
|
this.log.info(`Starting new slashing round ${round}`);
|
|
178
|
+
this.ownValidatorMonitor.handleNewRound(round);
|
|
159
179
|
await this.offensesCollector.handleNewRound(round);
|
|
160
180
|
}
|
|
161
181
|
|
|
162
|
-
/** Called when we see a RoundExecuted event on the SlashingProposer (just for logging). */
|
|
182
|
+
/** Called when we see a RoundExecuted event on the SlashingProposer (just for logging and metrics). */
|
|
163
183
|
protected async handleRoundExecuted(round: bigint, slashCount: bigint, l1BlockHash: Hex) {
|
|
184
|
+
this.metrics.recordRoundExecuted();
|
|
164
185
|
const slashes = await this.rollup.getSlashEvents(l1BlockHash);
|
|
165
186
|
this.log.info(`Slashing round ${round} has been executed with ${slashCount} slashes`, { slashes });
|
|
187
|
+
this.ownValidatorMonitor.handleSlashes(round, slashes, l1BlockHash);
|
|
166
188
|
}
|
|
167
189
|
|
|
168
190
|
/**
|
|
@@ -2,6 +2,7 @@ import { EpochCache } from '@aztec/epoch-cache';
|
|
|
2
2
|
import { RollupContract } from '@aztec/ethereum/contracts';
|
|
3
3
|
import type { ViemClient } from '@aztec/ethereum/types';
|
|
4
4
|
import type { SlotNumber } from '@aztec/foundation/branded-types';
|
|
5
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
5
6
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
7
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
7
8
|
import { AztecLMDBStoreV2 } from '@aztec/kv-store/lmdb-v2';
|
|
@@ -31,6 +32,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
|
|
|
31
32
|
private dateProvider: DateProvider,
|
|
32
33
|
private kvStore: AztecLMDBStoreV2,
|
|
33
34
|
private rollupRegisteredAtL2Slot: SlotNumber,
|
|
35
|
+
private ownValidators: EthAddress[] = [],
|
|
34
36
|
private logger = createLogger('slasher'),
|
|
35
37
|
) {}
|
|
36
38
|
|
|
@@ -83,6 +85,7 @@ export class SlasherClientFacade implements SlasherClientInterface {
|
|
|
83
85
|
this.dateProvider,
|
|
84
86
|
this.kvStore,
|
|
85
87
|
this.rollupRegisteredAtL2Slot,
|
|
88
|
+
this.ownValidators,
|
|
86
89
|
this.logger,
|
|
87
90
|
);
|
|
88
91
|
}
|