@lodestar/validator 1.35.0-dev.e9dd48f165 → 1.35.0-dev.fcf8d024ea

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.
Files changed (173) hide show
  1. package/lib/buckets.d.ts.map +1 -0
  2. package/lib/defaults.d.ts.map +1 -0
  3. package/lib/genesis.d.ts.map +1 -0
  4. package/lib/index.d.ts +7 -7
  5. package/lib/index.d.ts.map +1 -0
  6. package/lib/index.js +5 -5
  7. package/lib/index.js.map +1 -1
  8. package/lib/metrics.d.ts.map +1 -0
  9. package/lib/metrics.js +14 -14
  10. package/lib/metrics.js.map +1 -1
  11. package/lib/repositories/index.d.ts.map +1 -0
  12. package/lib/repositories/metaDataRepository.d.ts.map +1 -0
  13. package/lib/repositories/metaDataRepository.js +4 -3
  14. package/lib/repositories/metaDataRepository.js.map +1 -1
  15. package/lib/services/attestation.d.ts.map +1 -0
  16. package/lib/services/attestation.js +77 -60
  17. package/lib/services/attestation.js.map +1 -1
  18. package/lib/services/attestationDuties.d.ts.map +1 -0
  19. package/lib/services/attestationDuties.js +105 -98
  20. package/lib/services/attestationDuties.js.map +1 -1
  21. package/lib/services/block.d.ts.map +1 -0
  22. package/lib/services/block.js +64 -56
  23. package/lib/services/block.js.map +1 -1
  24. package/lib/services/blockDuties.d.ts +2 -2
  25. package/lib/services/blockDuties.d.ts.map +1 -0
  26. package/lib/services/blockDuties.js +35 -26
  27. package/lib/services/blockDuties.js.map +1 -1
  28. package/lib/services/chainHeaderTracker.d.ts.map +1 -0
  29. package/lib/services/chainHeaderTracker.js +30 -27
  30. package/lib/services/chainHeaderTracker.js.map +1 -1
  31. package/lib/services/doppelgangerService.d.ts.map +1 -0
  32. package/lib/services/doppelgangerService.js +52 -45
  33. package/lib/services/doppelgangerService.js.map +1 -1
  34. package/lib/services/emitter.d.ts +1 -1
  35. package/lib/services/emitter.d.ts.map +1 -0
  36. package/lib/services/externalSignerSync.d.ts.map +1 -0
  37. package/lib/services/externalSignerSync.js.map +1 -1
  38. package/lib/services/indices.d.ts.map +1 -0
  39. package/lib/services/indices.js +8 -5
  40. package/lib/services/indices.js.map +1 -1
  41. package/lib/services/prepareBeaconProposer.d.ts.map +1 -0
  42. package/lib/services/prepareBeaconProposer.js.map +1 -1
  43. package/lib/services/syncCommittee.d.ts.map +1 -0
  44. package/lib/services/syncCommittee.js +80 -61
  45. package/lib/services/syncCommittee.js.map +1 -1
  46. package/lib/services/syncCommitteeDuties.d.ts.map +1 -0
  47. package/lib/services/syncCommitteeDuties.js +28 -23
  48. package/lib/services/syncCommitteeDuties.js.map +1 -1
  49. package/lib/services/syncingStatusTracker.d.ts.map +1 -0
  50. package/lib/services/syncingStatusTracker.js +32 -27
  51. package/lib/services/syncingStatusTracker.js.map +1 -1
  52. package/lib/services/utils.d.ts.map +1 -0
  53. package/lib/services/validatorStore.d.ts.map +1 -0
  54. package/lib/services/validatorStore.js +9 -3
  55. package/lib/services/validatorStore.js.map +1 -1
  56. package/lib/slashingProtection/attestation/attestationByTargetRepository.d.ts.map +1 -0
  57. package/lib/slashingProtection/attestation/attestationByTargetRepository.js +7 -3
  58. package/lib/slashingProtection/attestation/attestationByTargetRepository.js.map +1 -1
  59. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.d.ts.map +1 -0
  60. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.js +5 -3
  61. package/lib/slashingProtection/attestation/attestationLowerBoundRepository.js.map +1 -1
  62. package/lib/slashingProtection/attestation/errors.d.ts.map +1 -0
  63. package/lib/slashingProtection/attestation/index.d.ts.map +1 -0
  64. package/lib/slashingProtection/attestation/index.js +3 -0
  65. package/lib/slashingProtection/attestation/index.js.map +1 -1
  66. package/lib/slashingProtection/block/blockBySlotRepository.d.ts.map +1 -0
  67. package/lib/slashingProtection/block/blockBySlotRepository.js +7 -3
  68. package/lib/slashingProtection/block/blockBySlotRepository.js.map +1 -1
  69. package/lib/slashingProtection/block/errors.d.ts.map +1 -0
  70. package/lib/slashingProtection/block/index.d.ts.map +1 -0
  71. package/lib/slashingProtection/block/index.js +1 -0
  72. package/lib/slashingProtection/block/index.js.map +1 -1
  73. package/lib/slashingProtection/index.d.ts +1 -1
  74. package/lib/slashingProtection/index.d.ts.map +1 -0
  75. package/lib/slashingProtection/index.js +3 -0
  76. package/lib/slashingProtection/index.js.map +1 -1
  77. package/lib/slashingProtection/interchange/errors.d.ts.map +1 -0
  78. package/lib/slashingProtection/interchange/formats/completeV4.d.ts.map +1 -0
  79. package/lib/slashingProtection/interchange/formats/index.d.ts.map +1 -0
  80. package/lib/slashingProtection/interchange/formats/v5.d.ts.map +1 -0
  81. package/lib/slashingProtection/interchange/index.d.ts.map +1 -0
  82. package/lib/slashingProtection/interchange/parseInterchange.d.ts.map +1 -0
  83. package/lib/slashingProtection/interchange/serializeInterchange.d.ts.map +1 -0
  84. package/lib/slashingProtection/interchange/types.d.ts.map +1 -0
  85. package/lib/slashingProtection/interface.d.ts.map +1 -0
  86. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.d.ts.map +1 -0
  87. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.js +8 -0
  88. package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.js.map +1 -1
  89. package/lib/slashingProtection/minMaxSurround/errors.d.ts.map +1 -0
  90. package/lib/slashingProtection/minMaxSurround/index.d.ts.map +1 -0
  91. package/lib/slashingProtection/minMaxSurround/interface.d.ts.map +1 -0
  92. package/lib/slashingProtection/minMaxSurround/minMaxSurround.d.ts.map +1 -0
  93. package/lib/slashingProtection/minMaxSurround/minMaxSurround.js +2 -0
  94. package/lib/slashingProtection/minMaxSurround/minMaxSurround.js.map +1 -1
  95. package/lib/slashingProtection/types.d.ts.map +1 -0
  96. package/lib/slashingProtection/utils.d.ts +1 -1
  97. package/lib/slashingProtection/utils.d.ts.map +1 -0
  98. package/lib/types.d.ts.map +1 -0
  99. package/lib/util/batch.d.ts.map +1 -0
  100. package/lib/util/clock.d.ts +3 -0
  101. package/lib/util/clock.d.ts.map +1 -0
  102. package/lib/util/clock.js +9 -1
  103. package/lib/util/clock.js.map +1 -1
  104. package/lib/util/difference.d.ts.map +1 -0
  105. package/lib/util/externalSignerClient.d.ts.map +1 -0
  106. package/lib/util/format.d.ts.map +1 -0
  107. package/lib/util/index.d.ts.map +1 -0
  108. package/lib/util/logger.d.ts.map +1 -0
  109. package/lib/util/params.d.ts.map +1 -0
  110. package/lib/util/params.js +18 -2
  111. package/lib/util/params.js.map +1 -1
  112. package/lib/util/url.d.ts.map +1 -0
  113. package/lib/validator.d.ts.map +1 -0
  114. package/lib/validator.js +15 -0
  115. package/lib/validator.js.map +1 -1
  116. package/package.json +19 -16
  117. package/src/buckets.ts +30 -0
  118. package/src/defaults.ts +8 -0
  119. package/src/genesis.ts +19 -0
  120. package/src/index.ts +22 -0
  121. package/src/metrics.ts +417 -0
  122. package/src/repositories/index.ts +1 -0
  123. package/src/repositories/metaDataRepository.ts +42 -0
  124. package/src/services/attestation.ts +362 -0
  125. package/src/services/attestationDuties.ts +406 -0
  126. package/src/services/block.ts +261 -0
  127. package/src/services/blockDuties.ts +217 -0
  128. package/src/services/chainHeaderTracker.ts +89 -0
  129. package/src/services/doppelgangerService.ts +286 -0
  130. package/src/services/emitter.ts +43 -0
  131. package/src/services/externalSignerSync.ts +81 -0
  132. package/src/services/indices.ts +165 -0
  133. package/src/services/prepareBeaconProposer.ts +119 -0
  134. package/src/services/syncCommittee.ts +338 -0
  135. package/src/services/syncCommitteeDuties.ts +337 -0
  136. package/src/services/syncingStatusTracker.ts +74 -0
  137. package/src/services/utils.ts +58 -0
  138. package/src/services/validatorStore.ts +830 -0
  139. package/src/slashingProtection/attestation/attestationByTargetRepository.ts +77 -0
  140. package/src/slashingProtection/attestation/attestationLowerBoundRepository.ts +44 -0
  141. package/src/slashingProtection/attestation/errors.ts +66 -0
  142. package/src/slashingProtection/attestation/index.ts +171 -0
  143. package/src/slashingProtection/block/blockBySlotRepository.ts +78 -0
  144. package/src/slashingProtection/block/errors.ts +28 -0
  145. package/src/slashingProtection/block/index.ts +94 -0
  146. package/src/slashingProtection/index.ts +95 -0
  147. package/src/slashingProtection/interchange/errors.ts +15 -0
  148. package/src/slashingProtection/interchange/formats/completeV4.ts +125 -0
  149. package/src/slashingProtection/interchange/formats/index.ts +7 -0
  150. package/src/slashingProtection/interchange/formats/v5.ts +120 -0
  151. package/src/slashingProtection/interchange/index.ts +5 -0
  152. package/src/slashingProtection/interchange/parseInterchange.ts +55 -0
  153. package/src/slashingProtection/interchange/serializeInterchange.ts +35 -0
  154. package/src/slashingProtection/interchange/types.ts +18 -0
  155. package/src/slashingProtection/interface.ts +28 -0
  156. package/src/slashingProtection/minMaxSurround/distanceStoreRepository.ts +57 -0
  157. package/src/slashingProtection/minMaxSurround/errors.ts +27 -0
  158. package/src/slashingProtection/minMaxSurround/index.ts +4 -0
  159. package/src/slashingProtection/minMaxSurround/interface.ts +23 -0
  160. package/src/slashingProtection/minMaxSurround/minMaxSurround.ts +104 -0
  161. package/src/slashingProtection/types.ts +12 -0
  162. package/src/slashingProtection/utils.ts +42 -0
  163. package/src/types.ts +31 -0
  164. package/src/util/batch.ts +15 -0
  165. package/src/util/clock.ts +170 -0
  166. package/src/util/difference.ts +10 -0
  167. package/src/util/externalSignerClient.ts +277 -0
  168. package/src/util/format.ts +3 -0
  169. package/src/util/index.ts +6 -0
  170. package/src/util/logger.ts +51 -0
  171. package/src/util/params.ts +320 -0
  172. package/src/util/url.ts +16 -0
  173. package/src/validator.ts +418 -0
@@ -0,0 +1,77 @@
1
+ import {ContainerType, Type} from "@chainsafe/ssz";
2
+ import {DB_PREFIX_LENGTH, DbReqOpts, encodeKey, uintLen} from "@lodestar/db";
3
+ import {BLSPubkey, Epoch, ssz} from "@lodestar/types";
4
+ import {bytesToInt, intToBytes} from "@lodestar/utils";
5
+ import {Bucket, getBucketNameByValue} from "../../buckets.js";
6
+ import {LodestarValidatorDatabaseController} from "../../types.js";
7
+ import {SlashingProtectionAttestation} from "../types.js";
8
+ import {blsPubkeyLen, uniqueVectorArr} from "../utils.js";
9
+
10
+ /**
11
+ * Manages validator db storage of attestations.
12
+ * Entries in the db are indexed by an encoded key which combines the validator's public key and the
13
+ * attestation's target epoch.
14
+ */
15
+ export class AttestationByTargetRepository {
16
+ protected type: Type<SlashingProtectionAttestation>;
17
+ protected bucket = Bucket.slashingProtectionAttestationByTarget;
18
+
19
+ private readonly bucketId = getBucketNameByValue(this.bucket);
20
+ private readonly dbReqOpts: DbReqOpts = {bucketId: this.bucketId};
21
+ private readonly minKey: Uint8Array;
22
+ private readonly maxKey: Uint8Array;
23
+
24
+ constructor(protected db: LodestarValidatorDatabaseController) {
25
+ this.type = new ContainerType({
26
+ sourceEpoch: ssz.Epoch,
27
+ targetEpoch: ssz.Epoch,
28
+ signingRoot: ssz.Root,
29
+ }); // casing doesn't matter
30
+ this.minKey = encodeKey(this.bucket, Buffer.alloc(0));
31
+ this.maxKey = encodeKey(this.bucket + 1, Buffer.alloc(0));
32
+ }
33
+
34
+ async getAll(pubkey: BLSPubkey, limit?: number): Promise<SlashingProtectionAttestation[]> {
35
+ const attestations = await this.db.values({
36
+ limit,
37
+ gte: this.encodeKey(pubkey, 0),
38
+ lt: this.encodeKey(pubkey, Number.MAX_SAFE_INTEGER),
39
+ bucketId: this.bucketId,
40
+ });
41
+ return attestations.map((attestation) => this.type.deserialize(attestation));
42
+ }
43
+
44
+ async get(pubkey: BLSPubkey, targetEpoch: Epoch): Promise<SlashingProtectionAttestation | null> {
45
+ const att = await this.db.get(this.encodeKey(pubkey, targetEpoch), this.dbReqOpts);
46
+ return att && this.type.deserialize(att);
47
+ }
48
+
49
+ async set(pubkey: BLSPubkey, atts: SlashingProtectionAttestation[]): Promise<void> {
50
+ await this.db.batchPut(
51
+ atts.map((att) => ({
52
+ key: this.encodeKey(pubkey, att.targetEpoch),
53
+ value: this.type.serialize(att),
54
+ })),
55
+ this.dbReqOpts
56
+ );
57
+ }
58
+
59
+ async listPubkeys(): Promise<BLSPubkey[]> {
60
+ const keys = await this.db.keys({gte: this.minKey, lt: this.maxKey, bucketId: this.bucketId});
61
+ return uniqueVectorArr(keys.map((key) => this.decodeKey(key).pubkey));
62
+ }
63
+
64
+ private encodeKey(pubkey: BLSPubkey, targetEpoch: Epoch): Uint8Array {
65
+ return encodeKey(this.bucket, Buffer.concat([pubkey, intToBytes(BigInt(targetEpoch), uintLen, "be")]));
66
+ }
67
+
68
+ private decodeKey(key: Uint8Array): {pubkey: BLSPubkey; targetEpoch: Epoch} {
69
+ return {
70
+ pubkey: key.slice(DB_PREFIX_LENGTH, DB_PREFIX_LENGTH + blsPubkeyLen),
71
+ targetEpoch: bytesToInt(
72
+ key.slice(DB_PREFIX_LENGTH + blsPubkeyLen, DB_PREFIX_LENGTH + blsPubkeyLen + uintLen),
73
+ "be"
74
+ ),
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,44 @@
1
+ import {ContainerType, Type} from "@chainsafe/ssz";
2
+ import {DbReqOpts, encodeKey} from "@lodestar/db";
3
+ import {BLSPubkey, Epoch, ssz} from "@lodestar/types";
4
+ import {Bucket, getBucketNameByValue} from "../../buckets.js";
5
+ import {LodestarValidatorDatabaseController} from "../../types.js";
6
+
7
+ // Only used locally here
8
+ export interface SlashingProtectionLowerBound {
9
+ minSourceEpoch: Epoch;
10
+ minTargetEpoch: Epoch;
11
+ }
12
+
13
+ /**
14
+ * Manages validator db storage of the minimum source and target epochs required of a validator
15
+ * attestation.
16
+ */
17
+ export class AttestationLowerBoundRepository {
18
+ protected type: Type<SlashingProtectionLowerBound>;
19
+ protected bucket = Bucket.slashingProtectionAttestationLowerBound;
20
+
21
+ private readonly bucketId = getBucketNameByValue(this.bucket);
22
+ private readonly dbReqOpts: DbReqOpts = {bucketId: this.bucketId};
23
+
24
+ constructor(protected db: LodestarValidatorDatabaseController) {
25
+ this.type = new ContainerType({
26
+ minSourceEpoch: ssz.Epoch,
27
+ minTargetEpoch: ssz.Epoch,
28
+ }); // casing doesn't matter
29
+ this.dbReqOpts = {bucketId: this.bucketId};
30
+ }
31
+
32
+ async get(pubkey: BLSPubkey): Promise<SlashingProtectionLowerBound | null> {
33
+ const att = await this.db.get(this.encodeKey(pubkey), this.dbReqOpts);
34
+ return att && this.type.deserialize(att);
35
+ }
36
+
37
+ async set(pubkey: BLSPubkey, value: SlashingProtectionLowerBound): Promise<void> {
38
+ await this.db.put(this.encodeKey(pubkey), this.type.serialize(value), this.dbReqOpts);
39
+ }
40
+
41
+ private encodeKey(pubkey: BLSPubkey): Uint8Array {
42
+ return encodeKey(this.bucket, pubkey);
43
+ }
44
+ }
@@ -0,0 +1,66 @@
1
+ import {Epoch} from "@lodestar/types";
2
+ import {LodestarError} from "@lodestar/utils";
3
+ import {SlashingProtectionAttestation} from "../types.js";
4
+
5
+ export enum InvalidAttestationErrorCode {
6
+ /**
7
+ * The attestation has the same target epoch as an attestation from the DB
8
+ */
9
+ DOUBLE_VOTE = "ERR_INVALID_ATTESTATION_DOUBLE_VOTE",
10
+ /**
11
+ * The attestation surrounds an existing attestation from the database `prev`
12
+ */
13
+ NEW_SURROUNDS_PREV = "ERR_INVALID_ATTESTATION_NEW_SURROUNDS_PREV",
14
+ /**
15
+ * The attestation is surrounded by an existing attestation from the database `prev`
16
+ */
17
+ PREV_SURROUNDS_NEW = "ERR_INVALID_ATTESTATION_PREV_SURROUNDS_NEW",
18
+ /**
19
+ * The attestation is invalid because its source epoch is greater than its target epoch
20
+ */
21
+ SOURCE_EXCEEDS_TARGET = "ERR_INVALID_ATTESTATION_SOURCE_EXCEEDS_TARGET",
22
+ /**
23
+ * The attestation is invalid because its source epoch is less than the lower bound on source
24
+ * epochs for this validator.
25
+ */
26
+ SOURCE_LESS_THAN_LOWER_BOUND = "ERR_INVALID_ATTESTATION_SOURCE_LESS_THAN_LOWER_BOUND",
27
+ /**
28
+ * The attestation is invalid because its target epoch is less than or equal to the lower
29
+ * bound on target epochs for this validator.
30
+ */
31
+ TARGET_LESS_THAN_OR_EQ_LOWER_BOUND = "ERR_INVALID_ATTESTATION_TARGET_LESS_THAN_OR_EQ_LOWER_BOUND",
32
+ }
33
+
34
+ type InvalidAttestationErrorType =
35
+ | {
36
+ code: InvalidAttestationErrorCode.DOUBLE_VOTE;
37
+ attestation: SlashingProtectionAttestation;
38
+ prev: SlashingProtectionAttestation;
39
+ }
40
+ | {
41
+ code: InvalidAttestationErrorCode.NEW_SURROUNDS_PREV;
42
+ attestation: SlashingProtectionAttestation;
43
+ // Since using min-max surround, the actual attestation may not be available
44
+ prev: SlashingProtectionAttestation | null;
45
+ }
46
+ | {
47
+ code: InvalidAttestationErrorCode.PREV_SURROUNDS_NEW;
48
+ attestation: SlashingProtectionAttestation;
49
+ // Since using min-max surround, the actual attestation may not be available
50
+ prev: SlashingProtectionAttestation | null;
51
+ }
52
+ | {
53
+ code: InvalidAttestationErrorCode.SOURCE_EXCEEDS_TARGET;
54
+ }
55
+ | {
56
+ code: InvalidAttestationErrorCode.SOURCE_LESS_THAN_LOWER_BOUND;
57
+ sourceEpoch: Epoch;
58
+ minSourceEpoch: Epoch;
59
+ }
60
+ | {
61
+ code: InvalidAttestationErrorCode.TARGET_LESS_THAN_OR_EQ_LOWER_BOUND;
62
+ targetEpoch: Epoch;
63
+ minTargetEpoch: Epoch;
64
+ };
65
+
66
+ export class InvalidAttestationError extends LodestarError<InvalidAttestationErrorType> {}
@@ -0,0 +1,171 @@
1
+ import {BLSPubkey, Epoch} from "@lodestar/types";
2
+ import {MinMaxSurround, SurroundAttestationError, SurroundAttestationErrorCode} from "../minMaxSurround/index.js";
3
+ import {SlashingProtectionAttestation} from "../types.js";
4
+ import {isEqualNonZeroRoot, minEpoch} from "../utils.js";
5
+ import {AttestationByTargetRepository} from "./attestationByTargetRepository.js";
6
+ import {AttestationLowerBoundRepository} from "./attestationLowerBoundRepository.js";
7
+ import {InvalidAttestationError, InvalidAttestationErrorCode} from "./errors.js";
8
+ export {
9
+ AttestationByTargetRepository,
10
+ AttestationLowerBoundRepository,
11
+ InvalidAttestationError,
12
+ InvalidAttestationErrorCode,
13
+ };
14
+
15
+ enum SafeStatus {
16
+ SAME_DATA = "SAFE_STATUS_SAME_DATA",
17
+ OK = "SAFE_STATUS_OK",
18
+ }
19
+
20
+ export class SlashingProtectionAttestationService {
21
+ private attestationByTarget: AttestationByTargetRepository;
22
+ private attestationLowerBound: AttestationLowerBoundRepository;
23
+ private minMaxSurround: MinMaxSurround;
24
+
25
+ constructor(
26
+ signedAttestationDb: AttestationByTargetRepository,
27
+ attestationLowerBound: AttestationLowerBoundRepository,
28
+ minMaxSurround: MinMaxSurround
29
+ ) {
30
+ this.attestationByTarget = signedAttestationDb;
31
+ this.attestationLowerBound = attestationLowerBound;
32
+ this.minMaxSurround = minMaxSurround;
33
+ }
34
+
35
+ /**
36
+ * Check an attestation for slash safety, and if it is safe, record it in the database
37
+ * This is the safe, externally-callable interface for checking attestations
38
+ */
39
+ async checkAndInsertAttestation(pubKey: BLSPubkey, attestation: SlashingProtectionAttestation): Promise<void> {
40
+ const safeStatus = await this.checkAttestation(pubKey, attestation);
41
+
42
+ if (safeStatus !== SafeStatus.SAME_DATA) {
43
+ await this.insertAttestation(pubKey, attestation);
44
+ }
45
+
46
+ // TODO: Implement safe clean-up of stored attestations
47
+ }
48
+
49
+ /**
50
+ * Check an attestation from `pubKey` for slash safety.
51
+ */
52
+ async checkAttestation(pubKey: BLSPubkey, attestation: SlashingProtectionAttestation): Promise<SafeStatus> {
53
+ // Although it's not required to avoid slashing, we disallow attestations
54
+ // which are obviously invalid by virtue of their source epoch exceeding their target.
55
+ if (attestation.sourceEpoch > attestation.targetEpoch) {
56
+ throw new InvalidAttestationError({code: InvalidAttestationErrorCode.SOURCE_EXCEEDS_TARGET});
57
+ }
58
+
59
+ // Check for a double vote. Namely, an existing attestation with the same target epoch,
60
+ // and a different signing root.
61
+ const sameTargetAtt = await this.attestationByTarget.get(pubKey, attestation.targetEpoch);
62
+ if (sameTargetAtt) {
63
+ // Interchange format allows for attestations without signing_root, then assume root is equal
64
+ if (isEqualNonZeroRoot(sameTargetAtt.signingRoot, attestation.signingRoot)) {
65
+ return SafeStatus.SAME_DATA;
66
+ }
67
+ throw new InvalidAttestationError({
68
+ code: InvalidAttestationErrorCode.DOUBLE_VOTE,
69
+ attestation: attestation,
70
+ prev: sameTargetAtt,
71
+ });
72
+ }
73
+
74
+ // Check for a surround vote
75
+ try {
76
+ await this.minMaxSurround.assertNoSurround(pubKey, attestation);
77
+ } catch (e) {
78
+ if (e instanceof SurroundAttestationError) {
79
+ const prev = await this.attestationByTarget.get(pubKey, e.type.attestation2Target).catch(() => null);
80
+ switch (e.type.code) {
81
+ case SurroundAttestationErrorCode.IS_SURROUNDING:
82
+ throw new InvalidAttestationError({
83
+ code: InvalidAttestationErrorCode.NEW_SURROUNDS_PREV,
84
+ attestation,
85
+ prev,
86
+ });
87
+ case SurroundAttestationErrorCode.IS_SURROUNDED:
88
+ throw new InvalidAttestationError({
89
+ code: InvalidAttestationErrorCode.PREV_SURROUNDS_NEW,
90
+ attestation,
91
+ prev,
92
+ });
93
+ }
94
+ }
95
+ throw e;
96
+ }
97
+
98
+ // Refuse to sign any attestation with:
99
+ // - source.epoch < min(att.source_epoch for att in data.signed_attestations if att.pubkey == attester_pubkey), OR
100
+ // - target_epoch <= min(att.target_epoch for att in data.signed_attestations if att.pubkey == attester_pubkey)
101
+ // (spec v4, Slashing Protection Database Interchange Format)
102
+ const attestationLowerBound = await this.attestationLowerBound.get(pubKey);
103
+ if (attestationLowerBound) {
104
+ const {minSourceEpoch, minTargetEpoch} = attestationLowerBound;
105
+ if (attestation.sourceEpoch < minSourceEpoch) {
106
+ throw new InvalidAttestationError({
107
+ code: InvalidAttestationErrorCode.SOURCE_LESS_THAN_LOWER_BOUND,
108
+ sourceEpoch: attestation.sourceEpoch,
109
+ minSourceEpoch,
110
+ });
111
+ }
112
+
113
+ if (attestation.targetEpoch <= minTargetEpoch) {
114
+ throw new InvalidAttestationError({
115
+ code: InvalidAttestationErrorCode.TARGET_LESS_THAN_OR_EQ_LOWER_BOUND,
116
+ targetEpoch: attestation.targetEpoch,
117
+ minTargetEpoch,
118
+ });
119
+ }
120
+ }
121
+
122
+ return SafeStatus.OK;
123
+ }
124
+
125
+ /**
126
+ * Insert an attestation into the slashing database
127
+ * This should *only* be called in the same (exclusive) transaction as `checkAttestation`
128
+ * so that the check isn't invalidated by a concurrent mutation
129
+ */
130
+ async insertAttestation(pubKey: BLSPubkey, attestation: SlashingProtectionAttestation): Promise<void> {
131
+ await this.attestationByTarget.set(pubKey, [attestation]);
132
+ await this.minMaxSurround.insertAttestation(pubKey, attestation);
133
+ }
134
+
135
+ /**
136
+ * Retrieve an attestation from the slashing protection database for a given `pubkey` and `epoch`
137
+ */
138
+ async getAttestationForEpoch(pubkey: BLSPubkey, epoch: Epoch): Promise<SlashingProtectionAttestation | null> {
139
+ return this.attestationByTarget.get(pubkey, epoch);
140
+ }
141
+
142
+ /**
143
+ * Interchange import / export functionality
144
+ */
145
+ async importAttestations(pubkey: BLSPubkey, attestations: SlashingProtectionAttestation[]): Promise<void> {
146
+ await this.attestationByTarget.set(pubkey, attestations);
147
+
148
+ // Pre-compute spans for all attestations
149
+ for (const attestation of attestations) {
150
+ await this.minMaxSurround.insertAttestation(pubkey, attestation);
151
+ }
152
+
153
+ // Pre-compute and store lower-bound
154
+ const minSourceEpoch = minEpoch(attestations.map((attestation) => attestation.sourceEpoch));
155
+ const minTargetEpoch = minEpoch(attestations.map((attestation) => attestation.targetEpoch));
156
+ if (minSourceEpoch != null && minTargetEpoch != null) {
157
+ await this.attestationLowerBound.set(pubkey, {minSourceEpoch, minTargetEpoch});
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Interchange import / export functionality
163
+ */
164
+ async exportAttestations(pubkey: BLSPubkey): Promise<SlashingProtectionAttestation[]> {
165
+ return this.attestationByTarget.getAll(pubkey);
166
+ }
167
+
168
+ async listPubkeys(): Promise<BLSPubkey[]> {
169
+ return this.attestationByTarget.listPubkeys();
170
+ }
171
+ }
@@ -0,0 +1,78 @@
1
+ import {ContainerType, Type} from "@chainsafe/ssz";
2
+ import {DB_PREFIX_LENGTH, DbReqOpts, encodeKey, uintLen} from "@lodestar/db";
3
+ import {BLSPubkey, Slot, ssz} from "@lodestar/types";
4
+ import {bytesToInt, intToBytes} from "@lodestar/utils";
5
+ import {Bucket, getBucketNameByValue} from "../../buckets.js";
6
+ import {LodestarValidatorDatabaseController} from "../../types.js";
7
+ import {SlashingProtectionBlock} from "../types.js";
8
+ import {blsPubkeyLen, uniqueVectorArr} from "../utils.js";
9
+
10
+ /**
11
+ * Manages validator db storage of blocks.
12
+ * Entries in the db are indexed by an encoded key which combines the validator's public key and the
13
+ * block's slot.
14
+ */
15
+ export class BlockBySlotRepository {
16
+ protected type: Type<SlashingProtectionBlock>;
17
+ protected bucket = Bucket.slashingProtectionBlockBySlot;
18
+
19
+ private readonly bucketId = getBucketNameByValue(this.bucket);
20
+ private readonly dbReqOpts: DbReqOpts = {bucketId: this.bucketId};
21
+ private readonly minKey: Uint8Array;
22
+ private readonly maxKey: Uint8Array;
23
+
24
+ constructor(protected db: LodestarValidatorDatabaseController) {
25
+ this.type = new ContainerType({
26
+ slot: ssz.Slot,
27
+ signingRoot: ssz.Root,
28
+ }); // casing doesn't matter
29
+ this.minKey = encodeKey(this.bucket, Buffer.alloc(0));
30
+ this.maxKey = encodeKey(this.bucket + 1, Buffer.alloc(0));
31
+ }
32
+
33
+ async getAll(pubkey: BLSPubkey, limit?: number): Promise<SlashingProtectionBlock[]> {
34
+ const blocks = await this.db.values({
35
+ limit,
36
+ gte: this.encodeKey(pubkey, 0),
37
+ lt: this.encodeKey(pubkey, Number.MAX_SAFE_INTEGER),
38
+ bucketId: this.bucketId,
39
+ });
40
+ return blocks.map((block) => this.type.deserialize(block));
41
+ }
42
+
43
+ async getFirst(pubkey: BLSPubkey): Promise<SlashingProtectionBlock | null> {
44
+ const blocks = await this.getAll(pubkey, 1);
45
+ return blocks[0] ?? null;
46
+ }
47
+
48
+ async get(pubkey: BLSPubkey, slot: Slot): Promise<SlashingProtectionBlock | null> {
49
+ const block = await this.db.get(this.encodeKey(pubkey, slot), this.dbReqOpts);
50
+ return block && this.type.deserialize(block);
51
+ }
52
+
53
+ async set(pubkey: BLSPubkey, blocks: SlashingProtectionBlock[]): Promise<void> {
54
+ await this.db.batchPut(
55
+ blocks.map((block) => ({
56
+ key: this.encodeKey(pubkey, block.slot),
57
+ value: this.type.serialize(block),
58
+ })),
59
+ this.dbReqOpts
60
+ );
61
+ }
62
+
63
+ async listPubkeys(): Promise<BLSPubkey[]> {
64
+ const keys = await this.db.keys({gte: this.minKey, lt: this.maxKey, bucketId: this.bucketId});
65
+ return uniqueVectorArr(keys.map((key) => this.decodeKey(key).pubkey));
66
+ }
67
+
68
+ private encodeKey(pubkey: BLSPubkey, slot: Slot): Uint8Array {
69
+ return encodeKey(this.bucket, Buffer.concat([pubkey, intToBytes(BigInt(slot), uintLen, "be")]));
70
+ }
71
+
72
+ private decodeKey(key: Uint8Array): {pubkey: BLSPubkey; slot: Slot} {
73
+ return {
74
+ pubkey: key.slice(DB_PREFIX_LENGTH, DB_PREFIX_LENGTH + blsPubkeyLen),
75
+ slot: bytesToInt(key.slice(DB_PREFIX_LENGTH, DB_PREFIX_LENGTH + uintLen), "be"),
76
+ };
77
+ }
78
+ }
@@ -0,0 +1,28 @@
1
+ import {Slot} from "@lodestar/types";
2
+ import {LodestarError} from "@lodestar/utils";
3
+ import {SlashingProtectionBlock} from "../types.js";
4
+
5
+ export enum InvalidBlockErrorCode {
6
+ /**
7
+ * The block has the same slot as a block from the DB
8
+ */
9
+ DOUBLE_BLOCK_PROPOSAL = "ERR_INVALID_BLOCK_DOUBLE_BLOCK_PROPOSAL",
10
+ /**
11
+ * The block is invalid because its slot is less than the lower bound slot for this validator.
12
+ */
13
+ SLOT_LESS_THAN_LOWER_BOUND = "ERR_INVALID_BLOCK_SLOT_LESS_THAN_LOWER_BOUND",
14
+ }
15
+
16
+ type InvalidBlockErrorType =
17
+ | {
18
+ code: InvalidBlockErrorCode.DOUBLE_BLOCK_PROPOSAL;
19
+ block: SlashingProtectionBlock;
20
+ block2: SlashingProtectionBlock;
21
+ }
22
+ | {
23
+ code: InvalidBlockErrorCode.SLOT_LESS_THAN_LOWER_BOUND;
24
+ slot: Slot;
25
+ minSlot: Slot;
26
+ };
27
+
28
+ export class InvalidBlockError extends LodestarError<InvalidBlockErrorType> {}
@@ -0,0 +1,94 @@
1
+ import {BLSPubkey} from "@lodestar/types";
2
+ import {SlashingProtectionBlock} from "../types.js";
3
+ import {isEqualNonZeroRoot} from "../utils.js";
4
+ import {BlockBySlotRepository} from "./blockBySlotRepository.js";
5
+ import {InvalidBlockError, InvalidBlockErrorCode} from "./errors.js";
6
+ export {BlockBySlotRepository, InvalidBlockError, InvalidBlockErrorCode};
7
+
8
+ enum SafeStatus {
9
+ SAME_DATA = "SAFE_STATUS_SAME_DATA",
10
+ OK = "SAFE_STATUS_OK",
11
+ }
12
+
13
+ export class SlashingProtectionBlockService {
14
+ private blockBySlot: BlockBySlotRepository;
15
+
16
+ constructor(blockBySlot: BlockBySlotRepository) {
17
+ this.blockBySlot = blockBySlot;
18
+ }
19
+
20
+ /**
21
+ * Check a block proposal for slash safety, and if it is safe, record it in the database.
22
+ * This is the safe, externally-callable interface for checking block proposals.
23
+ */
24
+ async checkAndInsertBlockProposal(pubkey: BLSPubkey, block: SlashingProtectionBlock): Promise<void> {
25
+ const safeStatus = await this.checkBlockProposal(pubkey, block);
26
+
27
+ if (safeStatus !== SafeStatus.SAME_DATA) {
28
+ await this.insertBlockProposal(pubkey, block);
29
+ }
30
+
31
+ // TODO: Implement safe clean-up of stored blocks
32
+ }
33
+
34
+ /**
35
+ * Check a block proposal from `pubKey` for slash safety.
36
+ */
37
+ async checkBlockProposal(pubkey: BLSPubkey, block: SlashingProtectionBlock): Promise<SafeStatus> {
38
+ // Double proposal
39
+ const sameSlotBlock = await this.blockBySlot.get(pubkey, block.slot);
40
+ if (sameSlotBlock && block.slot === sameSlotBlock.slot) {
41
+ // Interchange format allows for blocks without signing_root, then assume root is equal
42
+ if (isEqualNonZeroRoot(sameSlotBlock.signingRoot, block.signingRoot)) {
43
+ return SafeStatus.SAME_DATA;
44
+ }
45
+
46
+ throw new InvalidBlockError({
47
+ code: InvalidBlockErrorCode.DOUBLE_BLOCK_PROPOSAL,
48
+ block,
49
+ block2: sameSlotBlock,
50
+ });
51
+ }
52
+
53
+ // Refuse to sign any block with slot <= min(b.slot for b in data.signed_blocks if b.pubkey == proposer_pubkey),
54
+ // except if it is a repeat signing as determined by the signing_root.
55
+ // (spec v4, Slashing Protection Database Interchange Format)
56
+ const minBlock = await this.blockBySlot.getFirst(pubkey);
57
+ if (minBlock && block.slot <= minBlock.slot) {
58
+ throw new InvalidBlockError({
59
+ code: InvalidBlockErrorCode.SLOT_LESS_THAN_LOWER_BOUND,
60
+ slot: block.slot,
61
+ minSlot: minBlock.slot,
62
+ });
63
+ }
64
+
65
+ return SafeStatus.OK;
66
+ }
67
+
68
+ /**
69
+ * Insert a block proposal into the slashing database
70
+ * This should *only* be called in the same (exclusive) transaction as `checkBlockProposal`
71
+ * so that the check isn't invalidated by a concurrent mutation
72
+ */
73
+ async insertBlockProposal(pubkey: BLSPubkey, block: SlashingProtectionBlock): Promise<void> {
74
+ await this.blockBySlot.set(pubkey, [block]);
75
+ }
76
+
77
+ /**
78
+ * Interchange import / export functionality
79
+ */
80
+ async importBlocks(pubkey: BLSPubkey, blocks: SlashingProtectionBlock[]): Promise<void> {
81
+ await this.blockBySlot.set(pubkey, blocks);
82
+ }
83
+
84
+ /**
85
+ * Interchange import / export functionality
86
+ */
87
+ async exportBlocks(pubkey: BLSPubkey): Promise<SlashingProtectionBlock[]> {
88
+ return this.blockBySlot.getAll(pubkey);
89
+ }
90
+
91
+ async listPubkeys(): Promise<BLSPubkey[]> {
92
+ return this.blockBySlot.listPubkeys();
93
+ }
94
+ }
@@ -0,0 +1,95 @@
1
+ import {BLSPubkey, Epoch, Root} from "@lodestar/types";
2
+ import {Logger, toPubkeyHex} from "@lodestar/utils";
3
+ import {uniqueVectorArr} from "../slashingProtection/utils.js";
4
+ import {LodestarValidatorDatabaseController} from "../types.js";
5
+ import {
6
+ AttestationByTargetRepository,
7
+ AttestationLowerBoundRepository,
8
+ SlashingProtectionAttestationService,
9
+ } from "./attestation/index.js";
10
+ import {BlockBySlotRepository, SlashingProtectionBlockService} from "./block/index.js";
11
+ import {
12
+ Interchange,
13
+ InterchangeFormatVersion,
14
+ InterchangeLodestar,
15
+ parseInterchange,
16
+ serializeInterchange,
17
+ } from "./interchange/index.js";
18
+ import {ISlashingProtection} from "./interface.js";
19
+ import {DistanceStoreRepository, MinMaxSurround} from "./minMaxSurround/index.js";
20
+ import {SlashingProtectionAttestation, SlashingProtectionBlock} from "./types.js";
21
+
22
+ export {InvalidAttestationError, InvalidAttestationErrorCode} from "./attestation/index.js";
23
+ export {InvalidBlockError, InvalidBlockErrorCode} from "./block/index.js";
24
+ export type {Interchange, InterchangeFormat} from "./interchange/index.js";
25
+ export {InterchangeError, InterchangeErrorErrorCode} from "./interchange/index.js";
26
+ export type {ISlashingProtection, InterchangeFormatVersion, SlashingProtectionBlock, SlashingProtectionAttestation};
27
+ /**
28
+ * Handles slashing protection for validator proposer and attester duties as well as slashing protection
29
+ * during a validator interchange import/export process.
30
+ */
31
+ export class SlashingProtection implements ISlashingProtection {
32
+ private blockService: SlashingProtectionBlockService;
33
+ private attestationService: SlashingProtectionAttestationService;
34
+
35
+ constructor(protected db: LodestarValidatorDatabaseController) {
36
+ const blockBySlotRepository = new BlockBySlotRepository(db);
37
+ const attestationByTargetRepository = new AttestationByTargetRepository(db);
38
+ const attestationLowerBoundRepository = new AttestationLowerBoundRepository(db);
39
+ const distanceStoreRepository = new DistanceStoreRepository(db);
40
+ const minMaxSurround = new MinMaxSurround(distanceStoreRepository);
41
+
42
+ this.blockService = new SlashingProtectionBlockService(blockBySlotRepository);
43
+ this.attestationService = new SlashingProtectionAttestationService(
44
+ attestationByTargetRepository,
45
+ attestationLowerBoundRepository,
46
+ minMaxSurround
47
+ );
48
+ }
49
+
50
+ async checkAndInsertBlockProposal(pubKey: BLSPubkey, block: SlashingProtectionBlock): Promise<void> {
51
+ await this.blockService.checkAndInsertBlockProposal(pubKey, block);
52
+ }
53
+
54
+ async checkAndInsertAttestation(pubKey: BLSPubkey, attestation: SlashingProtectionAttestation): Promise<void> {
55
+ await this.attestationService.checkAndInsertAttestation(pubKey, attestation);
56
+ }
57
+
58
+ async hasAttestedInEpoch(pubKey: BLSPubkey, epoch: Epoch): Promise<boolean> {
59
+ return (await this.attestationService.getAttestationForEpoch(pubKey, epoch)) !== null;
60
+ }
61
+
62
+ async importInterchange(interchange: Interchange, genesisValidatorsRoot: Root, logger?: Logger): Promise<void> {
63
+ const {data} = parseInterchange(interchange, genesisValidatorsRoot);
64
+ for (const validator of data) {
65
+ logger?.info("Importing slashing protection", {pubkey: toPubkeyHex(validator.pubkey)});
66
+ await this.blockService.importBlocks(validator.pubkey, validator.signedBlocks);
67
+ await this.attestationService.importAttestations(validator.pubkey, validator.signedAttestations);
68
+ }
69
+ }
70
+
71
+ async exportInterchange(
72
+ genesisValidatorsRoot: Root,
73
+ pubkeys: BLSPubkey[],
74
+ formatVersion: InterchangeFormatVersion,
75
+ logger?: Logger
76
+ ): Promise<Interchange> {
77
+ const validatorData: InterchangeLodestar["data"] = [];
78
+ for (const pubkey of pubkeys) {
79
+ logger?.info("Exporting slashing protection", {pubkey: toPubkeyHex(pubkey)});
80
+ validatorData.push({
81
+ pubkey,
82
+ signedBlocks: await this.blockService.exportBlocks(pubkey),
83
+ signedAttestations: await this.attestationService.exportAttestations(pubkey),
84
+ });
85
+ }
86
+ logger?.verbose("Serializing Interchange");
87
+ return serializeInterchange({data: validatorData, genesisValidatorsRoot}, formatVersion);
88
+ }
89
+
90
+ async listPubkeys(): Promise<BLSPubkey[]> {
91
+ const pubkeysAtt = await this.attestationService.listPubkeys();
92
+ const pubkeysBlk = await this.blockService.listPubkeys();
93
+ return uniqueVectorArr([...pubkeysAtt, ...pubkeysBlk]);
94
+ }
95
+ }
@@ -0,0 +1,15 @@
1
+ import {Root} from "@lodestar/types";
2
+ import {LodestarError} from "@lodestar/utils";
3
+
4
+ export enum InterchangeErrorErrorCode {
5
+ UNSUPPORTED_FORMAT = "ERR_INTERCHANGE_UNSUPPORTED_FORMAT",
6
+ UNSUPPORTED_VERSION = "ERR_INTERCHANGE_UNSUPPORTED_VERSION",
7
+ GENESIS_VALIDATOR_MISMATCH = "ERR_INTERCHANGE_GENESIS_VALIDATOR_MISMATCH",
8
+ }
9
+
10
+ type InterchangeErrorErrorType =
11
+ | {code: InterchangeErrorErrorCode.UNSUPPORTED_FORMAT; format: string}
12
+ | {code: InterchangeErrorErrorCode.UNSUPPORTED_VERSION; version: string}
13
+ | {code: InterchangeErrorErrorCode.GENESIS_VALIDATOR_MISMATCH; root: Root; expectedRoot: Root};
14
+
15
+ export class InterchangeError extends LodestarError<InterchangeErrorErrorType> {}