@aztec/slasher 0.0.1-commit.b655e406 → 0.0.1-commit.b8a057fa
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 +90 -76
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +53 -41
- package/dest/factory/create_facade.d.ts +5 -4
- package/dest/factory/create_facade.d.ts.map +1 -1
- package/dest/factory/create_facade.js +26 -3
- package/dest/factory/create_implementation.d.ts +7 -8
- package/dest/factory/create_implementation.d.ts.map +1 -1
- package/dest/factory/create_implementation.js +8 -56
- package/dest/factory/get_settings.d.ts +4 -4
- package/dest/factory/get_settings.d.ts.map +1 -1
- package/dest/factory/get_settings.js +3 -3
- package/dest/factory/index.d.ts +2 -2
- package/dest/factory/index.d.ts.map +1 -1
- package/dest/factory/index.js +1 -1
- package/dest/generated/slasher-defaults.d.ts +21 -0
- package/dest/generated/slasher-defaults.d.ts.map +1 -0
- package/dest/generated/slasher-defaults.js +21 -0
- package/dest/index.d.ts +6 -4
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +5 -3
- package/dest/metrics.d.ts +7 -0
- package/dest/metrics.d.ts.map +1 -0
- package/dest/metrics.js +11 -0
- package/dest/null_slasher_client.d.ts +5 -5
- package/dest/null_slasher_client.d.ts.map +1 -1
- package/dest/null_slasher_client.js +1 -4
- package/dest/slash_offenses_collector.d.ts +10 -9
- package/dest/slash_offenses_collector.d.ts.map +1 -1
- package/dest/slash_offenses_collector.js +50 -30
- package/dest/slash_round_monitor.d.ts +5 -4
- package/dest/slash_round_monitor.d.ts.map +1 -1
- package/dest/slasher_client.d.ts +114 -0
- package/dest/slasher_client.d.ts.map +1 -0
- package/dest/{tally_slasher_client.js → slasher_client.js} +55 -45
- package/dest/slasher_client_facade.d.ts +9 -10
- package/dest/slasher_client_facade.d.ts.map +1 -1
- package/dest/slasher_client_facade.js +6 -9
- package/dest/slasher_client_interface.d.ts +9 -22
- package/dest/slasher_client_interface.d.ts.map +1 -1
- package/dest/slasher_client_interface.js +1 -4
- package/dest/stores/offenses_store.d.ts +12 -12
- package/dest/stores/offenses_store.d.ts.map +1 -1
- package/dest/stores/offenses_store.js +64 -39
- package/dest/stores/schema_version.d.ts +1 -1
- package/dest/test/dummy_watcher.d.ts +1 -1
- package/dest/test/dummy_watcher.d.ts.map +1 -1
- package/dest/watcher.d.ts +8 -1
- package/dest/watcher.d.ts.map +1 -1
- package/dest/watcher.js +1 -0
- package/dest/watchers/attestations_block_watcher.d.ts +29 -15
- package/dest/watchers/attestations_block_watcher.d.ts.map +1 -1
- package/dest/watchers/attestations_block_watcher.js +91 -69
- package/dest/watchers/attested_invalid_proposal_watcher.d.ts +42 -0
- package/dest/watchers/attested_invalid_proposal_watcher.d.ts.map +1 -0
- package/dest/watchers/attested_invalid_proposal_watcher.js +117 -0
- package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts +38 -0
- package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.d.ts.map +1 -0
- package/dest/watchers/broadcasted_invalid_checkpoint_proposal_watcher.js +138 -0
- package/dest/watchers/checkpoint_equivocation_watcher.d.ts +30 -0
- package/dest/watchers/checkpoint_equivocation_watcher.d.ts.map +1 -0
- package/dest/watchers/checkpoint_equivocation_watcher.js +69 -0
- package/dest/watchers/data_withholding_watcher.d.ts +63 -0
- package/dest/watchers/data_withholding_watcher.d.ts.map +1 -0
- package/dest/watchers/data_withholding_watcher.js +193 -0
- package/package.json +18 -15
- package/src/config.ts +61 -41
- package/src/factory/create_facade.ts +35 -6
- package/src/factory/create_implementation.ts +25 -106
- package/src/factory/get_settings.ts +8 -8
- package/src/factory/index.ts +1 -1
- package/src/generated/slasher-defaults.ts +23 -0
- package/src/index.ts +5 -3
- package/src/metrics.ts +19 -0
- package/src/null_slasher_client.ts +4 -7
- package/src/slash_offenses_collector.ts +70 -32
- package/src/slash_round_monitor.ts +3 -2
- package/src/{tally_slasher_client.ts → slasher_client.ts} +82 -58
- package/src/slasher_client_facade.ts +9 -13
- package/src/slasher_client_interface.ts +8 -22
- package/src/stores/offenses_store.ts +76 -48
- package/src/watcher.ts +9 -1
- package/src/watchers/attestations_block_watcher.ts +108 -88
- package/src/watchers/attested_invalid_proposal_watcher.ts +168 -0
- package/src/watchers/broadcasted_invalid_checkpoint_proposal_watcher.ts +192 -0
- package/src/watchers/checkpoint_equivocation_watcher.ts +96 -0
- package/src/watchers/data_withholding_watcher.ts +225 -0
- package/dest/empire_slasher_client.d.ts +0 -189
- package/dest/empire_slasher_client.d.ts.map +0 -1
- package/dest/empire_slasher_client.js +0 -572
- package/dest/stores/payloads_store.d.ts +0 -29
- package/dest/stores/payloads_store.d.ts.map +0 -1
- package/dest/stores/payloads_store.js +0 -125
- package/dest/tally_slasher_client.d.ts +0 -129
- package/dest/tally_slasher_client.d.ts.map +0 -1
- package/dest/watchers/epoch_prune_watcher.d.ts +0 -37
- package/dest/watchers/epoch_prune_watcher.d.ts.map +0 -1
- package/dest/watchers/epoch_prune_watcher.js +0 -135
- package/src/empire_slasher_client.ts +0 -656
- package/src/stores/payloads_store.ts +0 -146
- package/src/watchers/epoch_prune_watcher.ts +0 -192
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { CheckpointProposalHash, SlotNumber } from '@aztec/foundation/branded-types';
|
|
2
|
+
import { compactArray, merge, pick } from '@aztec/foundation/collection';
|
|
3
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
4
|
+
import { RunningPromise } from '@aztec/foundation/promise';
|
|
5
|
+
import { getAttestationInfoFromPublishedCheckpoint } from '@aztec/stdlib/block';
|
|
6
|
+
import { ConsensusPayload } from '@aztec/stdlib/p2p';
|
|
7
|
+
import { OffenseType, getOffenseTypeName } from '@aztec/stdlib/slashing';
|
|
8
|
+
import EventEmitter from 'node:events';
|
|
9
|
+
import { WANT_TO_SLASH_EVENT } from '../watcher.js';
|
|
10
|
+
const DataWithholdingWatcherConfigKeys = [
|
|
11
|
+
'slashDataWithholdingPenalty',
|
|
12
|
+
'slashDataWithholdingToleranceSlots'
|
|
13
|
+
];
|
|
14
|
+
/**
|
|
15
|
+
* Detects data-withholding offenses by probing the local mempool for the txs in published
|
|
16
|
+
* checkpoints once they are old enough that an honest node should have collected them.
|
|
17
|
+
*
|
|
18
|
+
* Per AZIP-7: once `slashDataWithholdingToleranceSlots` full slots have elapsed after the
|
|
19
|
+
* checkpoint's slot — i.e. at `slotStart(checkpoint.slot + slashDataWithholdingToleranceSlots
|
|
20
|
+
* + 1)` — if any tx from the checkpoint's blocks is still missing locally, the checkpoint's
|
|
21
|
+
* attesters are considered at fault for not making the data available, and we emit a slash
|
|
22
|
+
* for them.
|
|
23
|
+
*
|
|
24
|
+
* The watcher ticks at quarter-eth-slot cadence (matching the Sentinel template). On boot it
|
|
25
|
+
* floors processing at the current slot — restart-time gaps are accepted and not back-filled,
|
|
26
|
+
* matching the Sentinel approach.
|
|
27
|
+
*/ export class DataWithholdingWatcher extends EventEmitter {
|
|
28
|
+
epochCache;
|
|
29
|
+
l2BlockSource;
|
|
30
|
+
txProvider;
|
|
31
|
+
p2p;
|
|
32
|
+
reexecutionTracker;
|
|
33
|
+
signatureContext;
|
|
34
|
+
log;
|
|
35
|
+
runningPromise;
|
|
36
|
+
initialSlot;
|
|
37
|
+
lastCheckedSlot;
|
|
38
|
+
config;
|
|
39
|
+
constructor(epochCache, l2BlockSource, txProvider, p2p, reexecutionTracker, signatureContext, config, log = createLogger('data-withholding-watcher')){
|
|
40
|
+
super(), this.epochCache = epochCache, this.l2BlockSource = l2BlockSource, this.txProvider = txProvider, this.p2p = p2p, this.reexecutionTracker = reexecutionTracker, this.signatureContext = signatureContext, this.log = log;
|
|
41
|
+
this.config = pick(config, ...DataWithholdingWatcherConfigKeys);
|
|
42
|
+
const interval = epochCache.getL1Constants().ethereumSlotDuration * 1000 / 4;
|
|
43
|
+
this.runningPromise = new RunningPromise(this.work.bind(this), log, interval);
|
|
44
|
+
this.log.verbose(`DataWithholdingWatcher initialized`, this.config);
|
|
45
|
+
}
|
|
46
|
+
async start() {
|
|
47
|
+
// Floor processing at the archiver's synced slot rather than the wallclock — restart-time
|
|
48
|
+
// gaps before the archiver catches up are accepted and not back-filled. Falls back to the
|
|
49
|
+
// wallclock if the archiver isn't ready yet (cold start).
|
|
50
|
+
const syncedSlot = await this.l2BlockSource.getSyncedL2SlotNumber();
|
|
51
|
+
this.initialSlot = syncedSlot ?? this.epochCache.getSlotNow();
|
|
52
|
+
this.log.info(`Starting data-withholding watcher with initial slot ${this.initialSlot}`);
|
|
53
|
+
this.runningPromise.start();
|
|
54
|
+
}
|
|
55
|
+
stop() {
|
|
56
|
+
return this.runningPromise.stop();
|
|
57
|
+
}
|
|
58
|
+
updateConfig(config) {
|
|
59
|
+
this.config = merge(this.config, pick(config, ...DataWithholdingWatcherConfigKeys));
|
|
60
|
+
this.log.verbose('DataWithholdingWatcher config updated', this.config);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Runs every tick. Walks newly-eligible slots and probes their checkpoints for data
|
|
64
|
+
* availability; emits a DATA_WITHHOLDING slash for any checkpoint whose txs are missing.
|
|
65
|
+
*/ async work() {
|
|
66
|
+
if (this.initialSlot === undefined) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// tolerance is the number of full slots that must elapse after the checkpoint's slot
|
|
70
|
+
// before we declare its data missing. For checkpoint slot S, we therefore process S
|
|
71
|
+
// only once we are in slot `S + tolerance + 1` or later. Drive this off the archiver's
|
|
72
|
+
// synced slot rather than the wallclock so we don't make claims about slots we haven't
|
|
73
|
+
// fully ingested yet (archiver may lag behind L1).
|
|
74
|
+
const tolerance = this.config.slashDataWithholdingToleranceSlots;
|
|
75
|
+
const currentSlot = await this.l2BlockSource.getSyncedL2SlotNumber() ?? this.epochCache.getSlotNow();
|
|
76
|
+
if (currentSlot <= tolerance) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const targetSlot = SlotNumber(currentSlot - tolerance - 1);
|
|
80
|
+
if (targetSlot <= this.initialSlot) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const startSlot = this.lastCheckedSlot === undefined ? this.initialSlot : this.lastCheckedSlot;
|
|
84
|
+
for(let slot = SlotNumber(startSlot + 1); slot <= targetSlot; slot = SlotNumber(slot + 1)){
|
|
85
|
+
try {
|
|
86
|
+
await this.processSlot(slot);
|
|
87
|
+
} catch (err) {
|
|
88
|
+
this.log.error(`Error processing slot ${slot} for data-withholding check`, err, {
|
|
89
|
+
slot
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
this.lastCheckedSlot = slot;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/** Probes the checkpoint at the given slot, if any, and emits a slash on missing txs. */ async processSlot(slot) {
|
|
96
|
+
const published = await this.l2BlockSource.getCheckpoint({
|
|
97
|
+
slot
|
|
98
|
+
});
|
|
99
|
+
if (!published) {
|
|
100
|
+
this.log.trace(`No published checkpoint at slot ${slot}`, {
|
|
101
|
+
slot
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const checkpointNumber = published.checkpoint.number;
|
|
106
|
+
// Per-block tx-collection records (true | false | undefined) for every block in this
|
|
107
|
+
// published checkpoint. Captured by the validator's proposal handler at the moment of
|
|
108
|
+
// tx collection (i.e. by the *re-execution* deadline). Used as a positive short-circuit
|
|
109
|
+
// only: a `true` for every block means we know the data was available locally, so this
|
|
110
|
+
// checkpoint cannot be a data-withholding offense. A `false` does *not* trigger a slash
|
|
111
|
+
// on its own — the re-execution deadline is much earlier than the data-withholding
|
|
112
|
+
// tolerance window, so missing txs at that earlier deadline may still arrive in time.
|
|
113
|
+
// Anything other than all-true falls through to the mempool probe, which respects the
|
|
114
|
+
// tolerance window.
|
|
115
|
+
const collectionRecords = published.checkpoint.blocks.map((block, idx)=>this.reexecutionTracker.getTxsCollectedRecord(block.header.getSlot(), idx));
|
|
116
|
+
if (collectionRecords.every((r)=>r === true)) {
|
|
117
|
+
this.log.trace(`All blocks for checkpoint at slot ${slot} were collected locally; skipping`, {
|
|
118
|
+
slot,
|
|
119
|
+
checkpointNumber
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const txHashes = published.checkpoint.blocks.flatMap((block)=>block.body.txEffects.map((txEffect)=>txEffect.txHash));
|
|
124
|
+
if (txHashes.length === 0) {
|
|
125
|
+
this.log.trace(`Checkpoint at slot ${slot} has no txs`, {
|
|
126
|
+
slot
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const availability = await this.txProvider.hasTxs(txHashes);
|
|
131
|
+
const missingTxs = txHashes.filter((_, i)=>!availability[i]);
|
|
132
|
+
if (missingTxs.length === 0) {
|
|
133
|
+
this.log.trace(`All ${txHashes.length} txs available for checkpoint at slot ${slot}`, {
|
|
134
|
+
slot
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const attesters = await this.extractAttesters(published);
|
|
139
|
+
if (attesters.length === 0) {
|
|
140
|
+
this.log.warn(`Detected data withholding at slot ${slot} but no recoverable attesters`, {
|
|
141
|
+
slot,
|
|
142
|
+
checkpointNumber,
|
|
143
|
+
missingTxs: missingTxs.map((h)=>h.toString()),
|
|
144
|
+
records: collectionRecords
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
this.log.info(`Detected data withholding offense at slot ${slot}`, {
|
|
149
|
+
slot,
|
|
150
|
+
checkpointNumber,
|
|
151
|
+
amount: this.config.slashDataWithholdingPenalty,
|
|
152
|
+
offenseType: getOffenseTypeName(OffenseType.DATA_WITHHOLDING),
|
|
153
|
+
missingTxs: missingTxs.map((h)=>h.toString()),
|
|
154
|
+
records: collectionRecords,
|
|
155
|
+
attesters: attesters.map((a)=>a.toString())
|
|
156
|
+
});
|
|
157
|
+
const args = attesters.map((validator)=>({
|
|
158
|
+
validator,
|
|
159
|
+
amount: this.config.slashDataWithholdingPenalty,
|
|
160
|
+
offenseType: OffenseType.DATA_WITHHOLDING,
|
|
161
|
+
epochOrSlot: BigInt(slot)
|
|
162
|
+
}));
|
|
163
|
+
this.emit(WANT_TO_SLASH_EVENT, args);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Returns the union of:
|
|
167
|
+
* 1. attesters whose signatures landed in the published checkpoint on L1, and
|
|
168
|
+
* 2. attesters we observed signing the same proposal on p2p (the proposer publishes as
|
|
169
|
+
* soon as it has hit committee quorum, so honest peer attestations that arrive after
|
|
170
|
+
* that point are dropped — but they still vouched for the data and
|
|
171
|
+
* should be slashed for withholding it).
|
|
172
|
+
*
|
|
173
|
+
*
|
|
174
|
+
* Exposed as protected so tests can substitute a deterministic recovery without having
|
|
175
|
+
* to construct real secp256k1 signatures.
|
|
176
|
+
*/ async extractAttesters(published) {
|
|
177
|
+
const fromL1 = getAttestationInfoFromPublishedCheckpoint(published, this.signatureContext).filter((info)=>info.status === 'recovered-from-signature').map((info)=>info.address);
|
|
178
|
+
const slot = published.checkpoint.header.slotNumber;
|
|
179
|
+
const proposalPayloadHash = CheckpointProposalHash.fromBuffer(ConsensusPayload.fromCheckpoint(published.checkpoint, this.signatureContext).getPayloadHash());
|
|
180
|
+
const fromP2p = await this.p2p.getCheckpointAttestationsForSlot(slot, proposalPayloadHash).then((attestations)=>attestations.map((a)=>a.getSender()));
|
|
181
|
+
// Dedupe
|
|
182
|
+
const all = new Map();
|
|
183
|
+
for (const addr of compactArray([
|
|
184
|
+
...fromL1,
|
|
185
|
+
...fromP2p
|
|
186
|
+
])){
|
|
187
|
+
all.set(addr.toString(), addr);
|
|
188
|
+
}
|
|
189
|
+
return [
|
|
190
|
+
...all.values()
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/slasher",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.b8a057fa",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dest/index.js",
|
|
7
7
|
"./config": "./dest/config.js"
|
|
8
8
|
},
|
|
9
9
|
"inherits": [
|
|
10
|
-
"../package.common.json"
|
|
10
|
+
"../package.common.json",
|
|
11
|
+
"./package.local.json"
|
|
11
12
|
],
|
|
12
13
|
"scripts": {
|
|
13
|
-
"build": "yarn clean && tsc
|
|
14
|
-
"build:dev": "tsc
|
|
14
|
+
"build": "yarn clean && ../scripts/tsc.sh",
|
|
15
|
+
"build:dev": "../scripts/tsc.sh --watch",
|
|
15
16
|
"clean": "rm -rf ./dest .tsbuildinfo",
|
|
16
17
|
"bb": "node --no-warnings ./dest/bb/index.js",
|
|
17
|
-
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}"
|
|
18
|
+
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules ../node_modules/.bin/jest --passWithNoTests --maxWorkers=${JEST_MAX_WORKERS:-8}",
|
|
19
|
+
"generate": "./scripts/generate.sh"
|
|
18
20
|
},
|
|
19
21
|
"jest": {
|
|
20
22
|
"moduleNameMapper": {
|
|
@@ -54,24 +56,25 @@
|
|
|
54
56
|
]
|
|
55
57
|
},
|
|
56
58
|
"dependencies": {
|
|
57
|
-
"@aztec/epoch-cache": "0.0.1-commit.
|
|
58
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
59
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
60
|
-
"@aztec/kv-store": "0.0.1-commit.
|
|
61
|
-
"@aztec/l1-artifacts": "0.0.1-commit.
|
|
62
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
63
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
59
|
+
"@aztec/epoch-cache": "0.0.1-commit.b8a057fa",
|
|
60
|
+
"@aztec/ethereum": "0.0.1-commit.b8a057fa",
|
|
61
|
+
"@aztec/foundation": "0.0.1-commit.b8a057fa",
|
|
62
|
+
"@aztec/kv-store": "0.0.1-commit.b8a057fa",
|
|
63
|
+
"@aztec/l1-artifacts": "0.0.1-commit.b8a057fa",
|
|
64
|
+
"@aztec/stdlib": "0.0.1-commit.b8a057fa",
|
|
65
|
+
"@aztec/telemetry-client": "0.0.1-commit.b8a057fa",
|
|
64
66
|
"source-map-support": "^0.5.21",
|
|
65
67
|
"tslib": "^2.4.0",
|
|
66
|
-
"viem": "npm:@
|
|
67
|
-
"zod": "^
|
|
68
|
+
"viem": "npm:@aztec/viem@2.38.2",
|
|
69
|
+
"zod": "^4"
|
|
68
70
|
},
|
|
69
71
|
"devDependencies": {
|
|
70
|
-
"@aztec/aztec.js": "0.0.1-commit.
|
|
72
|
+
"@aztec/aztec.js": "0.0.1-commit.b8a057fa",
|
|
71
73
|
"@jest/globals": "^30.0.0",
|
|
72
74
|
"@types/jest": "^30.0.0",
|
|
73
75
|
"@types/node": "^22.15.17",
|
|
74
76
|
"@types/source-map-support": "^0.5.10",
|
|
77
|
+
"@typescript/native-preview": "7.0.0-dev.20260113.1",
|
|
75
78
|
"jest": "^30.0.0",
|
|
76
79
|
"jest-mock-extended": "^4.0.0",
|
|
77
80
|
"ts-node": "^10.9.1",
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { DefaultL1ContractsConfig } from '@aztec/ethereum';
|
|
2
1
|
import type { ConfigMappingsType } from '@aztec/foundation/config';
|
|
3
2
|
import {
|
|
4
3
|
bigintConfigHelper,
|
|
@@ -9,27 +8,35 @@ import {
|
|
|
9
8
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
10
9
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
11
10
|
|
|
11
|
+
import { slasherDefaultEnv } from './generated/slasher-defaults.js';
|
|
12
|
+
|
|
12
13
|
export type { SlasherConfig };
|
|
13
14
|
|
|
14
15
|
export const DefaultSlasherConfig: SlasherConfig = {
|
|
15
16
|
slashOverridePayload: undefined,
|
|
16
|
-
slashMinPenaltyPercentage: 0.5, // 50% of penalty
|
|
17
|
-
slashMaxPenaltyPercentage: 2.0, //2x of penalty
|
|
18
17
|
slashValidatorsAlways: [], // Empty by default
|
|
19
18
|
slashValidatorsNever: [], // Empty by default
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
slashInactivityTargetPercentage:
|
|
23
|
-
slashInactivityConsecutiveEpochThreshold:
|
|
24
|
-
slashBroadcastedInvalidBlockPenalty:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
slashDataWithholdingPenalty: BigInt(slasherDefaultEnv.SLASH_DATA_WITHHOLDING_PENALTY),
|
|
20
|
+
slashDataWithholdingToleranceSlots: slasherDefaultEnv.SLASH_DATA_WITHHOLDING_TOLERANCE_SLOTS,
|
|
21
|
+
slashInactivityTargetPercentage: slasherDefaultEnv.SLASH_INACTIVITY_TARGET_PERCENTAGE,
|
|
22
|
+
slashInactivityConsecutiveEpochThreshold: slasherDefaultEnv.SLASH_INACTIVITY_CONSECUTIVE_EPOCH_THRESHOLD,
|
|
23
|
+
slashBroadcastedInvalidBlockPenalty: BigInt(slasherDefaultEnv.SLASH_INVALID_BLOCK_PENALTY),
|
|
24
|
+
slashBroadcastedInvalidCheckpointProposalPenalty: BigInt(slasherDefaultEnv.SLASH_INVALID_CHECKPOINT_PROPOSAL_PENALTY),
|
|
25
|
+
slashDuplicateProposalPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_PROPOSAL_PENALTY),
|
|
26
|
+
slashDuplicateAttestationPenalty: BigInt(slasherDefaultEnv.SLASH_DUPLICATE_ATTESTATION_PENALTY),
|
|
27
|
+
slashInactivityPenalty: BigInt(slasherDefaultEnv.SLASH_INACTIVITY_PENALTY),
|
|
28
|
+
slashProposeInvalidAttestationsPenalty: BigInt(slasherDefaultEnv.SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY),
|
|
29
|
+
slashProposeDescendantOfCheckpointWithInvalidAttestationsPenalty: BigInt(
|
|
30
|
+
slasherDefaultEnv.SLASH_PROPOSE_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS_PENALTY,
|
|
31
|
+
),
|
|
32
|
+
slashAttestInvalidCheckpointProposalPenalty: BigInt(
|
|
33
|
+
slasherDefaultEnv.SLASH_ATTEST_INVALID_CHECKPOINT_PROPOSAL_PENALTY,
|
|
34
|
+
),
|
|
35
|
+
slashUnknownPenalty: BigInt(slasherDefaultEnv.SLASH_UNKNOWN_PENALTY),
|
|
36
|
+
slashOffenseExpirationRounds: slasherDefaultEnv.SLASH_OFFENSE_EXPIRATION_ROUNDS,
|
|
37
|
+
slashMaxPayloadSize: slasherDefaultEnv.SLASH_MAX_PAYLOAD_SIZE,
|
|
38
|
+
slashGracePeriodL2Slots: slasherDefaultEnv.SLASH_GRACE_PERIOD_L2_SLOTS,
|
|
39
|
+
slashExecuteRoundsLookBack: slasherDefaultEnv.SLASH_EXECUTE_ROUNDS_LOOK_BACK,
|
|
33
40
|
slashSelfAllowed: false,
|
|
34
41
|
};
|
|
35
42
|
|
|
@@ -37,19 +44,9 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
|
|
|
37
44
|
slashOverridePayload: {
|
|
38
45
|
env: 'SLASH_OVERRIDE_PAYLOAD',
|
|
39
46
|
description: 'An Ethereum address for a slash payload to vote for unconditionally.',
|
|
40
|
-
parseEnv: (val: string) =>
|
|
47
|
+
parseEnv: (val: string) => EthAddress.fromString(val),
|
|
41
48
|
defaultValue: DefaultSlasherConfig.slashOverridePayload,
|
|
42
49
|
},
|
|
43
|
-
slashMinPenaltyPercentage: {
|
|
44
|
-
env: 'SLASH_MIN_PENALTY_PERCENTAGE',
|
|
45
|
-
description: 'Minimum penalty percentage for slashing offenses (0.1 is 10%).',
|
|
46
|
-
...floatConfigHelper(DefaultSlasherConfig.slashMinPenaltyPercentage),
|
|
47
|
-
},
|
|
48
|
-
slashMaxPenaltyPercentage: {
|
|
49
|
-
env: 'SLASH_MAX_PENALTY_PERCENTAGE',
|
|
50
|
-
description: 'Maximum penalty percentage for slashing offenses (2.0 is 200%).',
|
|
51
|
-
...floatConfigHelper(DefaultSlasherConfig.slashMaxPenaltyPercentage),
|
|
52
|
-
},
|
|
53
50
|
slashValidatorsAlways: {
|
|
54
51
|
env: 'SLASH_VALIDATORS_ALWAYS',
|
|
55
52
|
description: 'Comma-separated list of validator addresses that should always be slashed.',
|
|
@@ -72,21 +69,38 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
|
|
|
72
69
|
.map(addr => EthAddress.fromString(addr)),
|
|
73
70
|
defaultValue: DefaultSlasherConfig.slashValidatorsNever,
|
|
74
71
|
},
|
|
75
|
-
slashPrunePenalty: {
|
|
76
|
-
env: 'SLASH_PRUNE_PENALTY',
|
|
77
|
-
description: 'Penalty amount for slashing validators of a valid pruned epoch (set to 0 to disable).',
|
|
78
|
-
...bigintConfigHelper(DefaultSlasherConfig.slashPrunePenalty),
|
|
79
|
-
},
|
|
80
72
|
slashDataWithholdingPenalty: {
|
|
81
73
|
env: 'SLASH_DATA_WITHHOLDING_PENALTY',
|
|
82
|
-
description: 'Penalty
|
|
74
|
+
description: 'Penalty for data withholding (0 records offenses without slash votes).',
|
|
83
75
|
...bigintConfigHelper(DefaultSlasherConfig.slashDataWithholdingPenalty),
|
|
84
76
|
},
|
|
77
|
+
slashDataWithholdingToleranceSlots: {
|
|
78
|
+
env: 'SLASH_DATA_WITHHOLDING_TOLERANCE_SLOTS',
|
|
79
|
+
description:
|
|
80
|
+
'Number of full L2 slots that must elapse after a checkpoint slot before declaring its txs missing and slashing its attesters for data withholding.',
|
|
81
|
+
...numberConfigHelper(DefaultSlasherConfig.slashDataWithholdingToleranceSlots),
|
|
82
|
+
},
|
|
85
83
|
slashBroadcastedInvalidBlockPenalty: {
|
|
86
84
|
env: 'SLASH_INVALID_BLOCK_PENALTY',
|
|
87
85
|
description: 'Penalty amount for slashing a validator for an invalid block proposed via p2p.',
|
|
88
86
|
...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidBlockPenalty),
|
|
89
87
|
},
|
|
88
|
+
slashBroadcastedInvalidCheckpointProposalPenalty: {
|
|
89
|
+
env: 'SLASH_INVALID_CHECKPOINT_PROPOSAL_PENALTY',
|
|
90
|
+
description: 'Penalty amount for slashing a validator for an invalid checkpoint proposal proposed via p2p.',
|
|
91
|
+
...bigintConfigHelper(DefaultSlasherConfig.slashBroadcastedInvalidCheckpointProposalPenalty),
|
|
92
|
+
},
|
|
93
|
+
slashDuplicateProposalPenalty: {
|
|
94
|
+
env: 'SLASH_DUPLICATE_PROPOSAL_PENALTY',
|
|
95
|
+
description: 'Penalty amount for slashing a validator for sending duplicate proposals.',
|
|
96
|
+
...bigintConfigHelper(DefaultSlasherConfig.slashDuplicateProposalPenalty),
|
|
97
|
+
},
|
|
98
|
+
slashDuplicateAttestationPenalty: {
|
|
99
|
+
env: 'SLASH_DUPLICATE_ATTESTATION_PENALTY',
|
|
100
|
+
description:
|
|
101
|
+
'Penalty amount for slashing a validator for signing attestations for different proposals at the same slot.',
|
|
102
|
+
...bigintConfigHelper(DefaultSlasherConfig.slashDuplicateAttestationPenalty),
|
|
103
|
+
},
|
|
90
104
|
slashInactivityTargetPercentage: {
|
|
91
105
|
env: 'SLASH_INACTIVITY_TARGET_PERCENTAGE',
|
|
92
106
|
description:
|
|
@@ -111,23 +125,28 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
|
|
|
111
125
|
},
|
|
112
126
|
slashInactivityPenalty: {
|
|
113
127
|
env: 'SLASH_INACTIVITY_PENALTY',
|
|
114
|
-
description: 'Penalty
|
|
128
|
+
description: 'Penalty for an inactive validator (0 records offenses without slash votes).',
|
|
115
129
|
...bigintConfigHelper(DefaultSlasherConfig.slashInactivityPenalty),
|
|
116
130
|
},
|
|
117
131
|
slashProposeInvalidAttestationsPenalty: {
|
|
118
132
|
env: 'SLASH_PROPOSE_INVALID_ATTESTATIONS_PENALTY',
|
|
119
|
-
description: 'Penalty
|
|
133
|
+
description: 'Penalty for proposing invalid attestations (0 records offenses without slash votes).',
|
|
120
134
|
...bigintConfigHelper(DefaultSlasherConfig.slashProposeInvalidAttestationsPenalty),
|
|
121
135
|
},
|
|
122
|
-
|
|
123
|
-
env: '
|
|
136
|
+
slashProposeDescendantOfCheckpointWithInvalidAttestationsPenalty: {
|
|
137
|
+
env: 'SLASH_PROPOSE_DESCENDANT_OF_CHECKPOINT_WITH_INVALID_ATTESTATIONS_PENALTY',
|
|
124
138
|
description:
|
|
125
|
-
'Penalty
|
|
126
|
-
...bigintConfigHelper(DefaultSlasherConfig.
|
|
139
|
+
'Penalty for publishing a checkpoint building on an invalid checkpoint (0 records offenses without slash votes).',
|
|
140
|
+
...bigintConfigHelper(DefaultSlasherConfig.slashProposeDescendantOfCheckpointWithInvalidAttestationsPenalty),
|
|
141
|
+
},
|
|
142
|
+
slashAttestInvalidCheckpointProposalPenalty: {
|
|
143
|
+
env: 'SLASH_ATTEST_INVALID_CHECKPOINT_PROPOSAL_PENALTY',
|
|
144
|
+
description: 'Penalty for attesting to an invalid checkpoint proposal (0 records offenses without slash votes).',
|
|
145
|
+
...bigintConfigHelper(DefaultSlasherConfig.slashAttestInvalidCheckpointProposalPenalty),
|
|
127
146
|
},
|
|
128
147
|
slashUnknownPenalty: {
|
|
129
148
|
env: 'SLASH_UNKNOWN_PENALTY',
|
|
130
|
-
description: 'Penalty
|
|
149
|
+
description: 'Penalty for an unknown offense (0 records offenses without slash votes).',
|
|
131
150
|
...bigintConfigHelper(DefaultSlasherConfig.slashUnknownPenalty),
|
|
132
151
|
},
|
|
133
152
|
slashOffenseExpirationRounds: {
|
|
@@ -141,7 +160,8 @@ export const slasherConfigMappings: ConfigMappingsType<SlasherConfig> = {
|
|
|
141
160
|
...numberConfigHelper(DefaultSlasherConfig.slashMaxPayloadSize),
|
|
142
161
|
},
|
|
143
162
|
slashGracePeriodL2Slots: {
|
|
144
|
-
description:
|
|
163
|
+
description:
|
|
164
|
+
'Number of L2 slots after the network upgrade during which slashing offenses are ignored. The upgrade time is determined from the CanonicalRollupUpdated event.',
|
|
145
165
|
env: 'SLASH_GRACE_PERIOD_L2_SLOTS',
|
|
146
166
|
...numberConfigHelper(DefaultSlasherConfig.slashGracePeriodL2Slots),
|
|
147
167
|
},
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
|
|
3
|
+
import type { L1ReaderConfig } from '@aztec/ethereum/l1-reader';
|
|
4
|
+
import type { ViemClient } from '@aztec/ethereum/types';
|
|
5
|
+
import { SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
6
|
import { unique } from '@aztec/foundation/collection';
|
|
5
7
|
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
8
|
import { createLogger } from '@aztec/foundation/log';
|
|
7
9
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
8
|
-
import type { DataStoreConfig } from '@aztec/kv-store/config';
|
|
9
10
|
import { createStore } from '@aztec/kv-store/lmdb-v2';
|
|
11
|
+
import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
|
|
10
12
|
import type { SlasherConfig } from '@aztec/stdlib/interfaces/server';
|
|
13
|
+
import type { DataStoreConfig } from '@aztec/stdlib/kv-store';
|
|
11
14
|
|
|
12
15
|
import { SlasherClientFacade } from '../slasher_client_facade.js';
|
|
13
16
|
import type { SlasherClientInterface } from '../slasher_client_interface.js';
|
|
@@ -17,7 +20,7 @@ import type { Watcher } from '../watcher.js';
|
|
|
17
20
|
/** Creates a slasher client facade that updates itself whenever the rollup slasher changes */
|
|
18
21
|
export async function createSlasherFacade(
|
|
19
22
|
config: SlasherConfig & DataStoreConfig & { ethereumSlotDuration: number },
|
|
20
|
-
l1Contracts: Pick<L1ReaderConfig
|
|
23
|
+
l1Contracts: Pick<L1ReaderConfig, 'rollupAddress' | 'registryAddress'>,
|
|
21
24
|
l1Client: ViemClient,
|
|
22
25
|
watchers: Watcher[],
|
|
23
26
|
dateProvider: DateProvider,
|
|
@@ -30,9 +33,35 @@ export async function createSlasherFacade(
|
|
|
30
33
|
throw new Error('Cannot initialize SlasherClient without a Rollup address');
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
const kvStore = await createStore('slasher', SCHEMA_VERSION, config,
|
|
36
|
+
const kvStore = await createStore('slasher', SCHEMA_VERSION, config, logger.getBindings());
|
|
34
37
|
const rollup = new RollupContract(l1Client, l1Contracts.rollupAddress);
|
|
35
38
|
|
|
39
|
+
// Compute and cache the L2 slot at which the rollup was registered as canonical
|
|
40
|
+
const settingsMap = kvStore.openMap<string, number>('slasher-settings');
|
|
41
|
+
const cacheKey = `registeredSlot:${l1Contracts.rollupAddress}`;
|
|
42
|
+
let rollupRegisteredAtL2Slot = (await settingsMap.getAsync(cacheKey)) as SlotNumber | undefined;
|
|
43
|
+
|
|
44
|
+
if (rollupRegisteredAtL2Slot === undefined) {
|
|
45
|
+
const registry = new RegistryContract(l1Client, l1Contracts.registryAddress);
|
|
46
|
+
const l1StartBlock = await rollup.getL1StartBlock();
|
|
47
|
+
const registrationTimestamp = await registry.getCanonicalRollupRegistrationTimestamp(
|
|
48
|
+
l1Contracts.rollupAddress,
|
|
49
|
+
l1StartBlock,
|
|
50
|
+
);
|
|
51
|
+
if (registrationTimestamp !== undefined) {
|
|
52
|
+
const l1GenesisTime = await rollup.getL1GenesisTime();
|
|
53
|
+
const slotDuration = await rollup.getSlotDuration();
|
|
54
|
+
rollupRegisteredAtL2Slot = getSlotAtTimestamp(registrationTimestamp, {
|
|
55
|
+
l1GenesisTime,
|
|
56
|
+
slotDuration: Number(slotDuration),
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
rollupRegisteredAtL2Slot = SlotNumber(0);
|
|
60
|
+
}
|
|
61
|
+
await settingsMap.set(cacheKey, rollupRegisteredAtL2Slot);
|
|
62
|
+
logger.info(`Canonical rollup registered at L2 slot ${rollupRegisteredAtL2Slot}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
36
65
|
const slashValidatorsNever = config.slashSelfAllowed
|
|
37
66
|
? config.slashValidatorsNever
|
|
38
67
|
: unique([...config.slashValidatorsNever, ...validatorAddresses].map(a => a.toString())).map(EthAddress.fromString);
|
|
@@ -42,11 +71,11 @@ export async function createSlasherFacade(
|
|
|
42
71
|
updatedConfig,
|
|
43
72
|
rollup,
|
|
44
73
|
l1Client,
|
|
45
|
-
l1Contracts.slashFactoryAddress,
|
|
46
74
|
watchers,
|
|
47
75
|
epochCache,
|
|
48
76
|
dateProvider,
|
|
49
77
|
kvStore,
|
|
78
|
+
rollupRegisteredAtL2Slot,
|
|
50
79
|
logger,
|
|
51
80
|
);
|
|
52
81
|
}
|