-
-
Notifications
You must be signed in to change notification settings - Fork 479
Expand file tree
/
Copy pathindex.ts
More file actions
192 lines (171 loc) 路 7.83 KB
/
Copy pathindex.ts
File metadata and controls
192 lines (171 loc) 路 7.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import {BLSPubkey, Epoch} from "@lodestar/types";
import {MinMaxSurround, SurroundAttestationError, SurroundAttestationErrorCode} from "../minMaxSurround/index.js";
import {SlashingProtectionAttestation} from "../types.js";
import {isEqualNonZeroRoot, minEpoch} from "../utils.js";
import {AttestationByTargetRepository} from "./attestationByTargetRepository.js";
import {AttestationLowerBoundRepository} from "./attestationLowerBoundRepository.js";
import {InvalidAttestationError, InvalidAttestationErrorCode} from "./errors.js";
export {
AttestationByTargetRepository,
AttestationLowerBoundRepository,
InvalidAttestationError,
InvalidAttestationErrorCode,
};
enum SafeStatus {
SAME_DATA = "SAFE_STATUS_SAME_DATA",
OK = "SAFE_STATUS_OK",
}
export class SlashingProtectionAttestationService {
private attestationByTarget: AttestationByTargetRepository;
private attestationLowerBound: AttestationLowerBoundRepository;
private minMaxSurround: MinMaxSurround;
constructor(
signedAttestationDb: AttestationByTargetRepository,
attestationLowerBound: AttestationLowerBoundRepository,
minMaxSurround: MinMaxSurround
) {
this.attestationByTarget = signedAttestationDb;
this.attestationLowerBound = attestationLowerBound;
this.minMaxSurround = minMaxSurround;
}
/**
* Check an attestation for slash safety, and if it is safe, record it in the database
* This is the safe, externally-callable interface for checking attestations
*/
async checkAndInsertAttestation(pubKey: BLSPubkey, attestation: SlashingProtectionAttestation): Promise<void> {
const safeStatus = await this.checkAttestation(pubKey, attestation);
if (safeStatus !== SafeStatus.SAME_DATA) {
await this.insertAttestation(pubKey, attestation);
}
// TODO: Implement safe clean-up of stored attestations
}
/**
* Check an attestation from `pubKey` for slash safety.
*/
async checkAttestation(pubKey: BLSPubkey, attestation: SlashingProtectionAttestation): Promise<SafeStatus> {
// Although it's not required to avoid slashing, we disallow attestations
// which are obviously invalid by virtue of their source epoch exceeding their target.
if (attestation.sourceEpoch > attestation.targetEpoch) {
throw new InvalidAttestationError({code: InvalidAttestationErrorCode.SOURCE_EXCEEDS_TARGET});
}
const latestAtt = await this.attestationByTarget.getLatest(pubKey);
// Check for a double vote. Namely, an existing attestation with the same target epoch,
// and a different signing root. No db read needed if the latest attestation has a lower target epoch.
let sameTargetAtt: SlashingProtectionAttestation | null = null;
if (latestAtt && latestAtt.targetEpoch === attestation.targetEpoch) {
sameTargetAtt = latestAtt;
} else if (latestAtt && latestAtt.targetEpoch > attestation.targetEpoch) {
sameTargetAtt = await this.attestationByTarget.get(pubKey, attestation.targetEpoch);
}
if (sameTargetAtt) {
// Interchange format allows for attestations without signing_root, then assume root is equal
if (isEqualNonZeroRoot(sameTargetAtt.signingRoot, attestation.signingRoot)) {
return SafeStatus.SAME_DATA;
}
throw new InvalidAttestationError({
code: InvalidAttestationErrorCode.DOUBLE_VOTE,
attestation: attestation,
prev: sameTargetAtt,
});
}
// Min-span entries only exist within the lookback window below each recorded source epoch, a surround vote
// with an older source epoch is undetectable by min-max surround and must be rejected outright. Recorded
// attestations are surround-free, so the latest one has the highest source epoch and the widest window.
if (latestAtt) {
const minSourceEpoch = this.minMaxSurround.minSpanCoverageStart(latestAtt.sourceEpoch);
if (attestation.sourceEpoch < minSourceEpoch) {
throw new InvalidAttestationError({
code: InvalidAttestationErrorCode.SOURCE_BELOW_MIN_SPAN_LOOKBACK,
sourceEpoch: attestation.sourceEpoch,
minSourceEpoch,
});
}
}
// Check for a surround vote
try {
await this.minMaxSurround.assertNoSurround(pubKey, attestation);
} catch (e) {
if (e instanceof SurroundAttestationError) {
const prev = await this.attestationByTarget.get(pubKey, e.type.attestation2Target).catch(() => null);
switch (e.type.code) {
case SurroundAttestationErrorCode.IS_SURROUNDING:
throw new InvalidAttestationError({
code: InvalidAttestationErrorCode.NEW_SURROUNDS_PREV,
attestation,
prev,
});
case SurroundAttestationErrorCode.IS_SURROUNDED:
throw new InvalidAttestationError({
code: InvalidAttestationErrorCode.PREV_SURROUNDS_NEW,
attestation,
prev,
});
}
}
throw e;
}
// Refuse to sign any attestation with:
// - source.epoch < min(att.source_epoch for att in data.signed_attestations if att.pubkey == attester_pubkey), OR
// - target_epoch <= min(att.target_epoch for att in data.signed_attestations if att.pubkey == attester_pubkey)
// (spec v4, Slashing Protection Database Interchange Format)
const attestationLowerBound = await this.attestationLowerBound.get(pubKey);
if (attestationLowerBound) {
const {minSourceEpoch, minTargetEpoch} = attestationLowerBound;
if (attestation.sourceEpoch < minSourceEpoch) {
throw new InvalidAttestationError({
code: InvalidAttestationErrorCode.SOURCE_LESS_THAN_LOWER_BOUND,
sourceEpoch: attestation.sourceEpoch,
minSourceEpoch,
});
}
if (attestation.targetEpoch <= minTargetEpoch) {
throw new InvalidAttestationError({
code: InvalidAttestationErrorCode.TARGET_LESS_THAN_OR_EQ_LOWER_BOUND,
targetEpoch: attestation.targetEpoch,
minTargetEpoch,
});
}
}
return SafeStatus.OK;
}
/**
* Insert an attestation into the slashing database
* This should *only* be called in the same (exclusive) transaction as `checkAttestation`
* so that the check isn't invalidated by a concurrent mutation
*/
async insertAttestation(pubKey: BLSPubkey, attestation: SlashingProtectionAttestation): Promise<void> {
await this.attestationByTarget.set(pubKey, [attestation]);
await this.minMaxSurround.insertAttestation(pubKey, attestation);
}
/**
* Retrieve an attestation from the slashing protection database for a given `pubkey` and `epoch`
*/
async getAttestationForEpoch(pubkey: BLSPubkey, epoch: Epoch): Promise<SlashingProtectionAttestation | null> {
return this.attestationByTarget.get(pubkey, epoch);
}
/**
* Interchange import / export functionality
*/
async importAttestations(pubkey: BLSPubkey, attestations: SlashingProtectionAttestation[]): Promise<void> {
// Pre-compute spans for all attestations, before storing them so a rejected interchange leaves none behind
for (const attestation of attestations) {
await this.minMaxSurround.insertAttestation(pubkey, attestation);
}
await this.attestationByTarget.set(pubkey, attestations);
// Pre-compute and store lower-bound
const minSourceEpoch = minEpoch(attestations.map((attestation) => attestation.sourceEpoch));
const minTargetEpoch = minEpoch(attestations.map((attestation) => attestation.targetEpoch));
if (minSourceEpoch != null && minTargetEpoch != null) {
await this.attestationLowerBound.set(pubkey, {minSourceEpoch, minTargetEpoch});
}
}
/**
* Interchange import / export functionality
*/
async exportAttestations(pubkey: BLSPubkey): Promise<SlashingProtectionAttestation[]> {
return this.attestationByTarget.getAll(pubkey);
}
async listPubkeys(): Promise<BLSPubkey[]> {
return this.attestationByTarget.listPubkeys();
}
}