@lodestar/validator 1.35.0-dev.83de5b8dea → 1.35.0-dev.8689cc3545
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/lib/buckets.d.ts.map +1 -0
- package/lib/defaults.d.ts.map +1 -0
- package/lib/genesis.d.ts.map +1 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/metrics.d.ts.map +1 -0
- package/lib/repositories/index.d.ts.map +1 -0
- package/lib/repositories/metaDataRepository.d.ts.map +1 -0
- package/lib/services/attestation.d.ts.map +1 -0
- package/lib/services/attestationDuties.d.ts.map +1 -0
- package/lib/services/block.d.ts.map +1 -0
- package/lib/services/blockDuties.d.ts.map +1 -0
- package/lib/services/chainHeaderTracker.d.ts.map +1 -0
- package/lib/services/doppelgangerService.d.ts.map +1 -0
- package/lib/services/emitter.d.ts.map +1 -0
- package/lib/services/externalSignerSync.d.ts.map +1 -0
- package/lib/services/indices.d.ts.map +1 -0
- package/lib/services/prepareBeaconProposer.d.ts.map +1 -0
- package/lib/services/syncCommittee.d.ts.map +1 -0
- package/lib/services/syncCommitteeDuties.d.ts.map +1 -0
- package/lib/services/syncingStatusTracker.d.ts.map +1 -0
- package/lib/services/utils.d.ts.map +1 -0
- package/lib/services/validatorStore.d.ts.map +1 -0
- package/lib/slashingProtection/attestation/attestationByTargetRepository.d.ts.map +1 -0
- package/lib/slashingProtection/attestation/attestationLowerBoundRepository.d.ts.map +1 -0
- package/lib/slashingProtection/attestation/errors.d.ts.map +1 -0
- package/lib/slashingProtection/attestation/index.d.ts.map +1 -0
- package/lib/slashingProtection/block/blockBySlotRepository.d.ts.map +1 -0
- package/lib/slashingProtection/block/errors.d.ts.map +1 -0
- package/lib/slashingProtection/block/index.d.ts.map +1 -0
- package/lib/slashingProtection/index.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/errors.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/formats/completeV4.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/formats/index.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/formats/v5.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/index.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/parseInterchange.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/serializeInterchange.d.ts.map +1 -0
- package/lib/slashingProtection/interchange/types.d.ts.map +1 -0
- package/lib/slashingProtection/interface.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/distanceStoreRepository.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/errors.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/index.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/interface.d.ts.map +1 -0
- package/lib/slashingProtection/minMaxSurround/minMaxSurround.d.ts.map +1 -0
- package/lib/slashingProtection/types.d.ts.map +1 -0
- package/lib/slashingProtection/utils.d.ts.map +1 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/util/batch.d.ts.map +1 -0
- package/lib/util/clock.d.ts.map +1 -0
- package/lib/util/difference.d.ts.map +1 -0
- package/lib/util/externalSignerClient.d.ts.map +1 -0
- package/lib/util/format.d.ts.map +1 -0
- package/lib/util/index.d.ts.map +1 -0
- package/lib/util/logger.d.ts.map +1 -0
- package/lib/util/params.d.ts.map +1 -0
- package/lib/util/url.d.ts.map +1 -0
- package/lib/validator.d.ts.map +1 -0
- package/package.json +13 -15
- package/src/buckets.ts +30 -0
- package/src/defaults.ts +8 -0
- package/src/genesis.ts +19 -0
- package/src/index.ts +22 -0
- package/src/metrics.ts +417 -0
- package/src/repositories/index.ts +1 -0
- package/src/repositories/metaDataRepository.ts +42 -0
- package/src/services/attestation.ts +349 -0
- package/src/services/attestationDuties.ts +405 -0
- package/src/services/block.ts +261 -0
- package/src/services/blockDuties.ts +215 -0
- package/src/services/chainHeaderTracker.ts +89 -0
- package/src/services/doppelgangerService.ts +286 -0
- package/src/services/emitter.ts +43 -0
- package/src/services/externalSignerSync.ts +81 -0
- package/src/services/indices.ts +165 -0
- package/src/services/prepareBeaconProposer.ts +119 -0
- package/src/services/syncCommittee.ts +317 -0
- package/src/services/syncCommitteeDuties.ts +337 -0
- package/src/services/syncingStatusTracker.ts +74 -0
- package/src/services/utils.ts +58 -0
- package/src/services/validatorStore.ts +830 -0
- package/src/slashingProtection/attestation/attestationByTargetRepository.ts +77 -0
- package/src/slashingProtection/attestation/attestationLowerBoundRepository.ts +44 -0
- package/src/slashingProtection/attestation/errors.ts +66 -0
- package/src/slashingProtection/attestation/index.ts +171 -0
- package/src/slashingProtection/block/blockBySlotRepository.ts +78 -0
- package/src/slashingProtection/block/errors.ts +28 -0
- package/src/slashingProtection/block/index.ts +94 -0
- package/src/slashingProtection/index.ts +95 -0
- package/src/slashingProtection/interchange/errors.ts +15 -0
- package/src/slashingProtection/interchange/formats/completeV4.ts +125 -0
- package/src/slashingProtection/interchange/formats/index.ts +7 -0
- package/src/slashingProtection/interchange/formats/v5.ts +120 -0
- package/src/slashingProtection/interchange/index.ts +5 -0
- package/src/slashingProtection/interchange/parseInterchange.ts +55 -0
- package/src/slashingProtection/interchange/serializeInterchange.ts +35 -0
- package/src/slashingProtection/interchange/types.ts +18 -0
- package/src/slashingProtection/interface.ts +28 -0
- package/src/slashingProtection/minMaxSurround/distanceStoreRepository.ts +57 -0
- package/src/slashingProtection/minMaxSurround/errors.ts +27 -0
- package/src/slashingProtection/minMaxSurround/index.ts +4 -0
- package/src/slashingProtection/minMaxSurround/interface.ts +23 -0
- package/src/slashingProtection/minMaxSurround/minMaxSurround.ts +104 -0
- package/src/slashingProtection/types.ts +12 -0
- package/src/slashingProtection/utils.ts +42 -0
- package/src/types.ts +31 -0
- package/src/util/batch.ts +15 -0
- package/src/util/clock.ts +164 -0
- package/src/util/difference.ts +10 -0
- package/src/util/externalSignerClient.ts +277 -0
- package/src/util/format.ts +3 -0
- package/src/util/index.ts +6 -0
- package/src/util/logger.ts +51 -0
- package/src/util/params.ts +313 -0
- package/src/util/url.ts +16 -0
- package/src/validator.ts +418 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {SecretKey} from "@chainsafe/blst";
|
|
2
|
+
import {DatabaseController} from "@lodestar/db";
|
|
3
|
+
import {BLSPubkey} from "@lodestar/types";
|
|
4
|
+
|
|
5
|
+
export type GenesisInfo = {
|
|
6
|
+
startTime: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type BLSKeypair = {
|
|
10
|
+
publicKey: BLSPubkey;
|
|
11
|
+
secretKey: SecretKey;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The validator's BLS public key, uniquely identifying them. _48-bytes, hex encoded with 0x prefix, case insensitive._
|
|
16
|
+
* ```
|
|
17
|
+
* "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export type PubkeyHex = string;
|
|
21
|
+
|
|
22
|
+
export type LodestarValidatorDatabaseController = Pick<
|
|
23
|
+
DatabaseController<Uint8Array, Uint8Array>,
|
|
24
|
+
"close" | "setMetrics" | "values" | "batchPut" | "keys" | "get" | "put"
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Callback to request a parent process to shudown.
|
|
29
|
+
* This could be an AbortController, but sending a message upwards is very useful to log a reason for shudown
|
|
30
|
+
*/
|
|
31
|
+
export type ProcessShutdownCallback = (err: Error) => void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert array of items into array of batched item arrays
|
|
3
|
+
*/
|
|
4
|
+
export function batchItems<T>(items: T[], opts: {batchSize: number; maxBatches?: number}): T[][] {
|
|
5
|
+
const batches: T[][] = [];
|
|
6
|
+
const maxBatches = opts.maxBatches ?? Math.ceil(items.length / opts.batchSize);
|
|
7
|
+
|
|
8
|
+
for (let i = 0; i < maxBatches; i++) {
|
|
9
|
+
const batch = items.slice(opts.batchSize * i, opts.batchSize * (i + 1));
|
|
10
|
+
if (batch.length === 0) break;
|
|
11
|
+
batches.push(batch);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return batches;
|
|
15
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {ChainForkConfig} from "@lodestar/config";
|
|
2
|
+
import {GENESIS_SLOT, SLOTS_PER_EPOCH} from "@lodestar/params";
|
|
3
|
+
import {computeEpochAtSlot, getCurrentSlot} from "@lodestar/state-transition";
|
|
4
|
+
import {Epoch, Slot, TimeSeconds} from "@lodestar/types";
|
|
5
|
+
import {ErrorAborted, Logger, isErrorAborted, sleep} from "@lodestar/utils";
|
|
6
|
+
|
|
7
|
+
type RunEveryFn = (slot: Slot, signal: AbortSignal) => Promise<void>;
|
|
8
|
+
|
|
9
|
+
export interface IClock {
|
|
10
|
+
readonly genesisTime: number;
|
|
11
|
+
readonly secondsPerSlot: number;
|
|
12
|
+
|
|
13
|
+
readonly currentEpoch: number;
|
|
14
|
+
|
|
15
|
+
start(signal: AbortSignal): void;
|
|
16
|
+
runEverySlot(fn: (slot: Slot, signal: AbortSignal) => Promise<void>): void;
|
|
17
|
+
runEveryEpoch(fn: (epoch: Epoch, signal: AbortSignal) => Promise<void>): void;
|
|
18
|
+
msToSlot(slot: Slot): number;
|
|
19
|
+
secFromSlot(slot: Slot): number;
|
|
20
|
+
getCurrentSlot(): Slot;
|
|
21
|
+
getCurrentEpoch(): Epoch;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export enum TimeItem {
|
|
25
|
+
Slot,
|
|
26
|
+
Epoch,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class Clock implements IClock {
|
|
30
|
+
readonly genesisTime: number;
|
|
31
|
+
readonly secondsPerSlot: number;
|
|
32
|
+
private readonly config: ChainForkConfig;
|
|
33
|
+
private readonly logger: Logger;
|
|
34
|
+
private readonly fns: {timeItem: TimeItem; fn: RunEveryFn}[] = [];
|
|
35
|
+
|
|
36
|
+
constructor(config: ChainForkConfig, logger: Logger, opts: {genesisTime: number}) {
|
|
37
|
+
this.genesisTime = opts.genesisTime;
|
|
38
|
+
this.secondsPerSlot = config.SECONDS_PER_SLOT;
|
|
39
|
+
this.config = config;
|
|
40
|
+
this.logger = logger;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
get currentEpoch(): Epoch {
|
|
44
|
+
return computeEpochAtSlot(getCurrentSlot(this.config, this.genesisTime));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
start(signal: AbortSignal): void {
|
|
48
|
+
for (const {timeItem, fn} of this.fns) {
|
|
49
|
+
this.runAtMostEvery(timeItem, signal, fn).catch((e: Error) => {
|
|
50
|
+
if (!isErrorAborted(e)) {
|
|
51
|
+
this.logger.error("runAtMostEvery", {}, e);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getCurrentSlot(): Slot {
|
|
58
|
+
return getCurrentSlot(this.config, this.genesisTime);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getCurrentEpoch(): Epoch {
|
|
62
|
+
return computeEpochAtSlot(getCurrentSlot(this.config, this.genesisTime));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
runEverySlot(fn: RunEveryFn): void {
|
|
66
|
+
this.fns.push({timeItem: TimeItem.Slot, fn});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
runEveryEpoch(fn: RunEveryFn): void {
|
|
70
|
+
this.fns.push({timeItem: TimeItem.Epoch, fn});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Milliseconds from now to a specific slot */
|
|
74
|
+
msToSlot(slot: Slot): number {
|
|
75
|
+
const timeAt = this.genesisTime + this.config.SECONDS_PER_SLOT * slot;
|
|
76
|
+
return timeAt * 1000 - Date.now();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Seconds elapsed from a specific slot to now */
|
|
80
|
+
secFromSlot(slot: Slot): number {
|
|
81
|
+
return Date.now() / 1000 - (this.genesisTime + this.config.SECONDS_PER_SLOT * slot);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* If a task happens to take more than one slot to run, we might skip a slot. This is unfortunate,
|
|
86
|
+
* however the alternative is to *always* process every slot, which has the chance of creating a
|
|
87
|
+
* theoretically unlimited backlog of tasks. It was a conscious decision to choose to drop tasks
|
|
88
|
+
* on an overloaded/latent system rather than overload it even more.
|
|
89
|
+
*/
|
|
90
|
+
private async runAtMostEvery(timeItem: TimeItem, signal: AbortSignal, fn: RunEveryFn): Promise<void> {
|
|
91
|
+
// Run immediately first
|
|
92
|
+
let slot = getCurrentSlot(this.config, this.genesisTime);
|
|
93
|
+
let slotOrEpoch = timeItem === TimeItem.Slot ? slot : computeEpochAtSlot(slot);
|
|
94
|
+
while (!signal.aborted) {
|
|
95
|
+
// Must catch fn() to ensure `sleep()` is awaited both for resolve and reject
|
|
96
|
+
await fn(slotOrEpoch, signal).catch((e: Error) => {
|
|
97
|
+
if (!isErrorAborted(e)) this.logger.error("Error on runEvery fn", {}, e);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
await sleep(this.timeUntilNext(timeItem), signal);
|
|
102
|
+
// calling getCurrentSlot here may not be correct when we're close to the next slot
|
|
103
|
+
// it's safe to call getCurrentSlotAround after we sleep
|
|
104
|
+
const nextSlot = getCurrentSlotAround(this.config, this.genesisTime);
|
|
105
|
+
|
|
106
|
+
if (timeItem === TimeItem.Slot) {
|
|
107
|
+
if (nextSlot > slot + 1) {
|
|
108
|
+
// It's not very likely that we skip more than one slot as HTTP timeout is set
|
|
109
|
+
// to SECONDS_PER_SLOT so we will fail task before skipping another slot.
|
|
110
|
+
this.logger.warn("Skipped slot due to task taking more than one slot to run", {
|
|
111
|
+
skippedSlot: slot + 1,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
slotOrEpoch = nextSlot;
|
|
115
|
+
} else {
|
|
116
|
+
slotOrEpoch = computeEpochAtSlot(nextSlot);
|
|
117
|
+
}
|
|
118
|
+
slot = nextSlot;
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e instanceof ErrorAborted) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
throw e;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private timeUntilNext(timeItem: TimeItem): number {
|
|
129
|
+
const milliSecondsPerSlot = this.config.SECONDS_PER_SLOT * 1000;
|
|
130
|
+
const msFromGenesis = Date.now() - this.genesisTime * 1000;
|
|
131
|
+
|
|
132
|
+
if (timeItem === TimeItem.Slot) {
|
|
133
|
+
if (msFromGenesis >= 0) {
|
|
134
|
+
return milliSecondsPerSlot - (msFromGenesis % milliSecondsPerSlot);
|
|
135
|
+
}
|
|
136
|
+
return Math.abs(msFromGenesis) % milliSecondsPerSlot;
|
|
137
|
+
}
|
|
138
|
+
const milliSecondsPerEpoch = SLOTS_PER_EPOCH * milliSecondsPerSlot;
|
|
139
|
+
if (msFromGenesis >= 0) {
|
|
140
|
+
return milliSecondsPerEpoch - (msFromGenesis % milliSecondsPerEpoch);
|
|
141
|
+
}
|
|
142
|
+
return Math.abs(msFromGenesis) % milliSecondsPerEpoch;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Same to the spec but we use Math.round instead of Math.floor.
|
|
148
|
+
*/
|
|
149
|
+
export function getCurrentSlotAround(config: ChainForkConfig, genesisTime: TimeSeconds): Slot {
|
|
150
|
+
const diffInSeconds = Date.now() / 1000 - genesisTime;
|
|
151
|
+
const slotsSinceGenesis = Math.round(diffInSeconds / config.SECONDS_PER_SLOT);
|
|
152
|
+
return GENESIS_SLOT + slotsSinceGenesis;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// function useEventStream() {
|
|
156
|
+
// this.stream = this.events.getEventStream([BeaconEventType.BLOCK, BeaconEventType.HEAD, BeaconEventType.CHAIN_REORG]);
|
|
157
|
+
// pipeToEmitter(this.stream, this).catch((e: Error) => {
|
|
158
|
+
// this.logger.error("Error on stream pipe", {}, e);
|
|
159
|
+
// });
|
|
160
|
+
|
|
161
|
+
// // On stop
|
|
162
|
+
// this.stream.stop();
|
|
163
|
+
// this.stream = null;
|
|
164
|
+
// }
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {Root} from "@lodestar/types";
|
|
2
|
+
import {toHex} from "@lodestar/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Return items included in `next` but not in `prev`
|
|
6
|
+
*/
|
|
7
|
+
export function differenceHex<T extends Uint8Array | Root>(prev: T[], next: T[]): T[] {
|
|
8
|
+
const existing = new Set(prev.map((item) => toHex(item)));
|
|
9
|
+
return next.filter((item) => !existing.has(toHex(item)));
|
|
10
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import {ContainerType, ValueOf} from "@chainsafe/ssz";
|
|
2
|
+
import {BeaconConfig} from "@lodestar/config";
|
|
3
|
+
import {ForkPreBellatrix, ForkSeq} from "@lodestar/params";
|
|
4
|
+
import {blindedOrFullBlockToHeader, computeEpochAtSlot} from "@lodestar/state-transition";
|
|
5
|
+
import {
|
|
6
|
+
AggregateAndProof,
|
|
7
|
+
BeaconBlock,
|
|
8
|
+
BlindedBeaconBlock,
|
|
9
|
+
Epoch,
|
|
10
|
+
Root,
|
|
11
|
+
RootHex,
|
|
12
|
+
Slot,
|
|
13
|
+
altair,
|
|
14
|
+
capella,
|
|
15
|
+
phase0,
|
|
16
|
+
ssz,
|
|
17
|
+
sszTypesFor,
|
|
18
|
+
} from "@lodestar/types";
|
|
19
|
+
import {ValidatorRegistrationV1} from "@lodestar/types/bellatrix";
|
|
20
|
+
import {fetch, toHex, toRootHex} from "@lodestar/utils";
|
|
21
|
+
import {PubkeyHex} from "../types.js";
|
|
22
|
+
|
|
23
|
+
export enum SignableMessageType {
|
|
24
|
+
AGGREGATION_SLOT = "AGGREGATION_SLOT",
|
|
25
|
+
AGGREGATE_AND_PROOF = "AGGREGATE_AND_PROOF",
|
|
26
|
+
AGGREGATE_AND_PROOF_V2 = "AGGREGATE_AND_PROOF_V2",
|
|
27
|
+
ATTESTATION = "ATTESTATION",
|
|
28
|
+
BLOCK_V2 = "BLOCK_V2",
|
|
29
|
+
DEPOSIT = "DEPOSIT",
|
|
30
|
+
RANDAO_REVEAL = "RANDAO_REVEAL",
|
|
31
|
+
VOLUNTARY_EXIT = "VOLUNTARY_EXIT",
|
|
32
|
+
SYNC_COMMITTEE_MESSAGE = "SYNC_COMMITTEE_MESSAGE",
|
|
33
|
+
SYNC_COMMITTEE_SELECTION_PROOF = "SYNC_COMMITTEE_SELECTION_PROOF",
|
|
34
|
+
SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF = "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF",
|
|
35
|
+
VALIDATOR_REGISTRATION = "VALIDATOR_REGISTRATION",
|
|
36
|
+
BLS_TO_EXECUTION_CHANGE = "BLS_TO_EXECUTION_CHANGE",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const AggregationSlotType = new ContainerType({
|
|
40
|
+
slot: ssz.Slot,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const DepositType = new ContainerType(
|
|
44
|
+
{
|
|
45
|
+
pubkey: ssz.BLSPubkey,
|
|
46
|
+
withdrawalCredentials: ssz.Bytes32,
|
|
47
|
+
amount: ssz.UintNum64,
|
|
48
|
+
genesisForkVersion: ssz.Bytes4,
|
|
49
|
+
},
|
|
50
|
+
{jsonCase: "eth2"}
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const RandaoRevealType = new ContainerType({
|
|
54
|
+
epoch: ssz.Epoch,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const SyncCommitteeMessageType = new ContainerType(
|
|
58
|
+
{
|
|
59
|
+
beaconBlockRoot: ssz.Root,
|
|
60
|
+
slot: ssz.Slot,
|
|
61
|
+
},
|
|
62
|
+
{jsonCase: "eth2"}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const SyncAggregatorSelectionDataType = new ContainerType(
|
|
66
|
+
{
|
|
67
|
+
slot: ssz.Slot,
|
|
68
|
+
subcommitteeIndex: ssz.SubcommitteeIndex,
|
|
69
|
+
},
|
|
70
|
+
{jsonCase: "eth2"}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
export type SignableMessage =
|
|
74
|
+
| {type: SignableMessageType.AGGREGATION_SLOT; data: {slot: Slot}}
|
|
75
|
+
| {type: SignableMessageType.AGGREGATE_AND_PROOF; data: phase0.AggregateAndProof}
|
|
76
|
+
| {type: SignableMessageType.AGGREGATE_AND_PROOF_V2; data: AggregateAndProof}
|
|
77
|
+
| {type: SignableMessageType.ATTESTATION; data: phase0.AttestationData}
|
|
78
|
+
| {type: SignableMessageType.BLOCK_V2; data: BeaconBlock<ForkPreBellatrix> | BlindedBeaconBlock}
|
|
79
|
+
| {type: SignableMessageType.DEPOSIT; data: ValueOf<typeof DepositType>}
|
|
80
|
+
| {type: SignableMessageType.RANDAO_REVEAL; data: {epoch: Epoch}}
|
|
81
|
+
| {type: SignableMessageType.VOLUNTARY_EXIT; data: phase0.VoluntaryExit}
|
|
82
|
+
| {type: SignableMessageType.SYNC_COMMITTEE_MESSAGE; data: ValueOf<typeof SyncCommitteeMessageType>}
|
|
83
|
+
| {type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF; data: ValueOf<typeof SyncAggregatorSelectionDataType>}
|
|
84
|
+
| {type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF; data: altair.ContributionAndProof}
|
|
85
|
+
| {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1}
|
|
86
|
+
| {type: SignableMessageType.BLS_TO_EXECUTION_CHANGE; data: capella.BLSToExecutionChange};
|
|
87
|
+
|
|
88
|
+
const requiresForkInfo: Record<SignableMessageType, boolean> = {
|
|
89
|
+
[SignableMessageType.AGGREGATION_SLOT]: true,
|
|
90
|
+
[SignableMessageType.AGGREGATE_AND_PROOF]: true,
|
|
91
|
+
[SignableMessageType.AGGREGATE_AND_PROOF_V2]: true,
|
|
92
|
+
[SignableMessageType.ATTESTATION]: true,
|
|
93
|
+
[SignableMessageType.BLOCK_V2]: true,
|
|
94
|
+
[SignableMessageType.DEPOSIT]: false,
|
|
95
|
+
[SignableMessageType.RANDAO_REVEAL]: true,
|
|
96
|
+
[SignableMessageType.VOLUNTARY_EXIT]: true,
|
|
97
|
+
[SignableMessageType.SYNC_COMMITTEE_MESSAGE]: true,
|
|
98
|
+
[SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF]: true,
|
|
99
|
+
[SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF]: true,
|
|
100
|
+
[SignableMessageType.VALIDATOR_REGISTRATION]: false,
|
|
101
|
+
[SignableMessageType.BLS_TO_EXECUTION_CHANGE]: true,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type Web3SignerSerializedRequest = {
|
|
105
|
+
type: SignableMessageType;
|
|
106
|
+
fork_info?: {
|
|
107
|
+
fork: {
|
|
108
|
+
previous_version: RootHex;
|
|
109
|
+
current_version: RootHex;
|
|
110
|
+
epoch: string;
|
|
111
|
+
};
|
|
112
|
+
genesis_validators_root: RootHex;
|
|
113
|
+
};
|
|
114
|
+
signingRoot: RootHex;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
enum MediaType {
|
|
118
|
+
json = "application/json",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Return public keys from the server.
|
|
123
|
+
*/
|
|
124
|
+
export async function externalSignerGetKeys(externalSignerUrl: string): Promise<string[]> {
|
|
125
|
+
const res = await fetch(`${externalSignerUrl}/api/v1/eth2/publicKeys`, {
|
|
126
|
+
method: "GET",
|
|
127
|
+
headers: {Accept: MediaType.json},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return handleExternalSignerResponse<string[]>(res);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Return signature in bytes. Assumption that the pubkey has it's corresponding secret key in the keystore of an external signer.
|
|
135
|
+
*/
|
|
136
|
+
export async function externalSignerPostSignature(
|
|
137
|
+
config: BeaconConfig,
|
|
138
|
+
externalSignerUrl: string,
|
|
139
|
+
pubkeyHex: PubkeyHex,
|
|
140
|
+
signingRoot: Root,
|
|
141
|
+
signingSlot: Slot,
|
|
142
|
+
signableMessage: SignableMessage
|
|
143
|
+
): Promise<string> {
|
|
144
|
+
const requestObj = serializerSignableMessagePayload(config, signableMessage) as Web3SignerSerializedRequest;
|
|
145
|
+
|
|
146
|
+
requestObj.type = signableMessage.type;
|
|
147
|
+
requestObj.signingRoot = toRootHex(signingRoot);
|
|
148
|
+
|
|
149
|
+
if (requiresForkInfo[signableMessage.type]) {
|
|
150
|
+
const forkInfo = config.getForkInfo(signingSlot);
|
|
151
|
+
requestObj.fork_info = {
|
|
152
|
+
fork: {
|
|
153
|
+
previous_version: toHex(forkInfo.prevVersion),
|
|
154
|
+
current_version: toHex(forkInfo.version),
|
|
155
|
+
epoch: String(computeEpochAtSlot(signingSlot)),
|
|
156
|
+
},
|
|
157
|
+
genesis_validators_root: toRootHex(config.genesisValidatorsRoot),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const res = await fetch(`${externalSignerUrl}/api/v1/eth2/sign/${pubkeyHex}`, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
Accept: MediaType.json,
|
|
165
|
+
"Content-Type": MediaType.json,
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify(requestObj),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const data = await handleExternalSignerResponse<{signature: string}>(res);
|
|
171
|
+
return data.signature;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Return upcheck status from server.
|
|
176
|
+
*/
|
|
177
|
+
export async function externalSignerUpCheck(remoteUrl: string): Promise<boolean> {
|
|
178
|
+
const res = await fetch(`${remoteUrl}/upcheck`, {
|
|
179
|
+
method: "GET",
|
|
180
|
+
headers: {Accept: MediaType.json},
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const data = await handleExternalSignerResponse<{status: string}>(res);
|
|
184
|
+
return data.status === "OK";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function handleExternalSignerResponse<T>(res: Response): Promise<T> {
|
|
188
|
+
if (!res.ok) {
|
|
189
|
+
const errBody = await res.text();
|
|
190
|
+
throw Error(errBody || res.statusText);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const contentType = res.headers.get("content-type");
|
|
194
|
+
if (contentType === null) {
|
|
195
|
+
throw Error("No Content-Type header found in response");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
199
|
+
if (mediaType !== MediaType.json) {
|
|
200
|
+
throw Error(`Unsupported response media type: ${mediaType}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
return (await res.json()) as T;
|
|
205
|
+
} catch (e) {
|
|
206
|
+
throw Error(`Invalid json response: ${(e as Error).message}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function serializerSignableMessagePayload(config: BeaconConfig, payload: SignableMessage): Record<string, unknown> {
|
|
211
|
+
switch (payload.type) {
|
|
212
|
+
case SignableMessageType.AGGREGATION_SLOT:
|
|
213
|
+
return {aggregation_slot: AggregationSlotType.toJson(payload.data)};
|
|
214
|
+
|
|
215
|
+
case SignableMessageType.AGGREGATE_AND_PROOF:
|
|
216
|
+
return {aggregate_and_proof: ssz.phase0.AggregateAndProof.toJson(payload.data)};
|
|
217
|
+
|
|
218
|
+
case SignableMessageType.AGGREGATE_AND_PROOF_V2: {
|
|
219
|
+
const fork = config.getForkName(payload.data.aggregate.data.slot);
|
|
220
|
+
return {
|
|
221
|
+
aggregate_and_proof: {
|
|
222
|
+
version: fork.toUpperCase(),
|
|
223
|
+
data: sszTypesFor(fork).AggregateAndProof.toJson(payload.data),
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case SignableMessageType.ATTESTATION:
|
|
229
|
+
return {attestation: ssz.phase0.AttestationData.toJson(payload.data)};
|
|
230
|
+
|
|
231
|
+
// Note: `type: BLOCK` not implemented
|
|
232
|
+
case SignableMessageType.BLOCK_V2: {
|
|
233
|
+
const fork = config.getForkInfo(payload.data.slot);
|
|
234
|
+
// web3signer requires capitalized names: PHASE0, ALTAIR, etc
|
|
235
|
+
const version = fork.name.toUpperCase();
|
|
236
|
+
if (fork.seq >= ForkSeq.bellatrix) {
|
|
237
|
+
return {
|
|
238
|
+
beacon_block: {
|
|
239
|
+
version,
|
|
240
|
+
block_header: ssz.phase0.BeaconBlockHeader.toJson(blindedOrFullBlockToHeader(config, payload.data)),
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
beacon_block: {
|
|
247
|
+
version,
|
|
248
|
+
block: config.getForkTypes(payload.data.slot).BeaconBlock.toJson(payload.data),
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case SignableMessageType.DEPOSIT:
|
|
254
|
+
return {deposit: DepositType.toJson(payload.data)};
|
|
255
|
+
|
|
256
|
+
case SignableMessageType.RANDAO_REVEAL:
|
|
257
|
+
return {randao_reveal: RandaoRevealType.toJson(payload.data)};
|
|
258
|
+
|
|
259
|
+
case SignableMessageType.VOLUNTARY_EXIT:
|
|
260
|
+
return {voluntary_exit: ssz.phase0.VoluntaryExit.toJson(payload.data)};
|
|
261
|
+
|
|
262
|
+
case SignableMessageType.SYNC_COMMITTEE_MESSAGE:
|
|
263
|
+
return {sync_committee_message: SyncCommitteeMessageType.toJson(payload.data)};
|
|
264
|
+
|
|
265
|
+
case SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF:
|
|
266
|
+
return {sync_aggregator_selection_data: SyncAggregatorSelectionDataType.toJson(payload.data)};
|
|
267
|
+
|
|
268
|
+
case SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF:
|
|
269
|
+
return {contribution_and_proof: ssz.altair.ContributionAndProof.toJson(payload.data)};
|
|
270
|
+
|
|
271
|
+
case SignableMessageType.VALIDATOR_REGISTRATION:
|
|
272
|
+
return {validator_registration: ssz.bellatrix.ValidatorRegistrationV1.toJson(payload.data)};
|
|
273
|
+
|
|
274
|
+
case SignableMessageType.BLS_TO_EXECUTION_CHANGE:
|
|
275
|
+
return {BLS_TO_EXECUTION_CHANGE: ssz.capella.BLSToExecutionChange.toJson(payload.data)};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {ApiError} from "@lodestar/api";
|
|
2
|
+
import {LogData, Logger, isErrorAborted} from "@lodestar/utils";
|
|
3
|
+
import {IClock} from "./clock.js";
|
|
4
|
+
|
|
5
|
+
export type LoggerVc = Logger & {
|
|
6
|
+
isSyncing(e: Error): void;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function getLoggerVc(logger: Logger, clock: IClock): LoggerVc {
|
|
10
|
+
let hasLogged = false;
|
|
11
|
+
|
|
12
|
+
clock.runEverySlot(async () => {
|
|
13
|
+
if (hasLogged) hasLogged = false;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
error(message: string, context?: LogData, e?: Error) {
|
|
18
|
+
if (e) {
|
|
19
|
+
// Returns true if it's an network error with code 503 = Node is syncing
|
|
20
|
+
// https://github.com/ethereum/beacon-APIs/blob/e68a954e1b6f6eb5421abf4532c171ce301c6b2e/types/http.yaml#L62
|
|
21
|
+
if (e instanceof ApiError && e.status === 503) {
|
|
22
|
+
this.isSyncing(e);
|
|
23
|
+
}
|
|
24
|
+
// Only log if arg `e` is not an instance of `ErrorAborted`
|
|
25
|
+
else if (!isErrorAborted(e)) {
|
|
26
|
+
logger.error(message, context, e);
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
logger.error(message, context, e);
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// error: logger.error.bind(logger),
|
|
34
|
+
warn: logger.warn.bind(logger),
|
|
35
|
+
info: logger.info.bind(logger),
|
|
36
|
+
verbose: logger.verbose.bind(logger),
|
|
37
|
+
debug: logger.debug.bind(logger),
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Throttle "node is syncing" errors to not pollute the console too much.
|
|
41
|
+
* Logs once per slot at most.
|
|
42
|
+
*/
|
|
43
|
+
isSyncing(e: Error) {
|
|
44
|
+
if (!hasLogged) {
|
|
45
|
+
hasLogged = true;
|
|
46
|
+
// Log the full error message, in case the server returns 503 for some unknown reason
|
|
47
|
+
logger.warn(`Node is syncing - ${e.message}`);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|