@lodestar/validator 1.43.0-dev.bc569affb9 → 1.43.0-dev.c5efeb6c90
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/metrics.d.ts +10 -0
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +37 -0
- package/lib/metrics.js.map +1 -1
- package/lib/services/block.d.ts +2 -1
- package/lib/services/block.d.ts.map +1 -1
- package/lib/services/block.js +4 -3
- package/lib/services/block.js.map +1 -1
- package/lib/services/blockDuties.d.ts +85 -7
- package/lib/services/blockDuties.d.ts.map +1 -1
- package/lib/services/blockDuties.js +186 -74
- package/lib/services/blockDuties.js.map +1 -1
- package/lib/services/chainHeaderTracker.d.ts +8 -2
- package/lib/services/chainHeaderTracker.d.ts.map +1 -1
- package/lib/services/chainHeaderTracker.js +23 -8
- package/lib/services/chainHeaderTracker.js.map +1 -1
- package/lib/services/emitter.d.ts +14 -1
- package/lib/services/emitter.d.ts.map +1 -1
- package/lib/services/emitter.js +22 -0
- package/lib/services/emitter.js.map +1 -1
- package/lib/services/proposerPreferences.d.ts +25 -0
- package/lib/services/proposerPreferences.d.ts.map +1 -0
- package/lib/services/proposerPreferences.js +101 -0
- package/lib/services/proposerPreferences.js.map +1 -0
- package/lib/services/ptc.d.ts +28 -0
- package/lib/services/ptc.d.ts.map +1 -0
- package/lib/services/ptc.js +89 -0
- package/lib/services/ptc.js.map +1 -0
- package/lib/services/ptcDuties.d.ts +31 -0
- package/lib/services/ptcDuties.d.ts.map +1 -0
- package/lib/services/ptcDuties.js +201 -0
- package/lib/services/ptcDuties.js.map +1 -0
- package/lib/services/validatorStore.d.ts +3 -0
- package/lib/services/validatorStore.d.ts.map +1 -1
- package/lib/services/validatorStore.js +54 -1
- package/lib/services/validatorStore.js.map +1 -1
- package/lib/util/externalSignerClient.d.ts +9 -1
- package/lib/util/externalSignerClient.d.ts.map +1 -1
- package/lib/util/externalSignerClient.js +8 -0
- package/lib/util/externalSignerClient.js.map +1 -1
- package/lib/util/params.js +3 -0
- package/lib/util/params.js.map +1 -1
- package/lib/validator.d.ts +4 -1
- package/lib/validator.d.ts.map +1 -1
- package/lib/validator.js +13 -3
- package/lib/validator.js.map +1 -1
- package/package.json +12 -12
- package/src/metrics.ts +46 -0
- package/src/services/block.ts +3 -9
- package/src/services/blockDuties.ts +212 -79
- package/src/services/chainHeaderTracker.ts +31 -7
- package/src/services/emitter.ts +31 -0
- package/src/services/proposerPreferences.ts +124 -0
- package/src/services/ptc.ts +131 -0
- package/src/services/ptcDuties.ts +246 -0
- package/src/services/validatorStore.ts +79 -0
- package/src/util/externalSignerClient.ts +13 -1
- package/src/util/params.ts +3 -0
- package/src/validator.ts +47 -5
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {ApiClient, routes} from "@lodestar/api";
|
|
2
|
+
import {ChainForkConfig} from "@lodestar/config";
|
|
3
|
+
import {isForkPostGloas} from "@lodestar/params";
|
|
4
|
+
import {Slot, gloas} from "@lodestar/types";
|
|
5
|
+
import {prettyBytes, sleep, toRootHex} from "@lodestar/utils";
|
|
6
|
+
import {Metrics} from "../metrics.js";
|
|
7
|
+
import {PubkeyHex} from "../types.js";
|
|
8
|
+
import {IClock, LoggerVc} from "../util/index.js";
|
|
9
|
+
import {ChainHeaderTracker} from "./chainHeaderTracker.js";
|
|
10
|
+
import {ValidatorEventEmitter} from "./emitter.js";
|
|
11
|
+
import {PtcDutiesService} from "./ptcDuties.js";
|
|
12
|
+
import {SyncingStatusTracker} from "./syncingStatusTracker.js";
|
|
13
|
+
import {ValidatorStore} from "./validatorStore.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Service that sets up and handles validator Payload Timeliness Committee duties.
|
|
17
|
+
*/
|
|
18
|
+
export class PtcService {
|
|
19
|
+
private readonly dutiesService: PtcDutiesService;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private readonly config: ChainForkConfig,
|
|
23
|
+
private readonly logger: LoggerVc,
|
|
24
|
+
private readonly api: ApiClient,
|
|
25
|
+
private readonly clock: IClock,
|
|
26
|
+
private readonly validatorStore: ValidatorStore,
|
|
27
|
+
private readonly emitter: ValidatorEventEmitter,
|
|
28
|
+
chainHeadTracker: ChainHeaderTracker,
|
|
29
|
+
syncingStatusTracker: SyncingStatusTracker,
|
|
30
|
+
private readonly metrics: Metrics | null
|
|
31
|
+
) {
|
|
32
|
+
this.dutiesService = new PtcDutiesService(
|
|
33
|
+
config,
|
|
34
|
+
logger,
|
|
35
|
+
api,
|
|
36
|
+
clock,
|
|
37
|
+
validatorStore,
|
|
38
|
+
chainHeadTracker,
|
|
39
|
+
syncingStatusTracker,
|
|
40
|
+
metrics
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
clock.runEverySlot(this.runPtcTasks);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
removeDutiesForKey(pubkey: PubkeyHex): void {
|
|
47
|
+
this.dutiesService.removeDutiesForKey(pubkey);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private runPtcTasks = async (slot: Slot, signal: AbortSignal): Promise<void> => {
|
|
51
|
+
const fork = this.config.getForkName(slot);
|
|
52
|
+
if (!isForkPostGloas(fork)) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const duties = this.dutiesService.getDutiesAtSlot(slot);
|
|
57
|
+
if (duties.length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const payloadAttestationDueMs = this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS);
|
|
62
|
+
await Promise.race([
|
|
63
|
+
sleep(payloadAttestationDueMs - this.clock.msFromSlot(slot), signal),
|
|
64
|
+
this.emitter.waitForExecutionPayloadAvailableSlot(slot),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
this.metrics?.ptcStepCallProducePayloadAttestation.observe(
|
|
68
|
+
this.clock.secFromSlot(slot) - payloadAttestationDueMs / 1000
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const payloadAttestationData = await this.producePayloadAttestationData(slot);
|
|
73
|
+
await this.signAndPublishPayloadAttestations(slot, payloadAttestationData, duties);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
this.logger.error("Error on PTC routine", {slot}, e as Error);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
private async producePayloadAttestationData(slot: Slot): Promise<gloas.PayloadAttestationData> {
|
|
80
|
+
return (await this.api.validator.producePayloadAttestationData({slot})).value();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private async signAndPublishPayloadAttestations(
|
|
84
|
+
slot: Slot,
|
|
85
|
+
payloadAttestationData: gloas.PayloadAttestationData,
|
|
86
|
+
duties: routes.validator.PtcDuty[]
|
|
87
|
+
): Promise<void> {
|
|
88
|
+
const payloadAttestationMessages: gloas.PayloadAttestationMessage[] = [];
|
|
89
|
+
const beaconBlockRootHex = toRootHex(payloadAttestationData.beaconBlockRoot);
|
|
90
|
+
|
|
91
|
+
await Promise.all(
|
|
92
|
+
duties.map(async (duty) => {
|
|
93
|
+
const logCtxValidator = {slot, validatorIndex: duty.validatorIndex, beaconBlockRoot: beaconBlockRootHex};
|
|
94
|
+
try {
|
|
95
|
+
payloadAttestationMessages.push(
|
|
96
|
+
await this.validatorStore.signPayloadAttestation(
|
|
97
|
+
duty,
|
|
98
|
+
payloadAttestationData,
|
|
99
|
+
this.clock.getCurrentSlot(),
|
|
100
|
+
this.logger
|
|
101
|
+
)
|
|
102
|
+
);
|
|
103
|
+
this.logger.debug("Signed payload attestation message", logCtxValidator);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
this.metrics?.ptcError.inc({error: "sign"});
|
|
106
|
+
this.logger.error("Error signing payload attestation message", logCtxValidator, e as Error);
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
this.metrics?.ptcStepCallPublishPayloadAttestation.observe(
|
|
112
|
+
this.clock.secFromSlot(slot) -
|
|
113
|
+
this.config.getSlotComponentDurationMs(this.config.PAYLOAD_ATTESTATION_DUE_BPS) / 1000
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (payloadAttestationMessages.length > 0) {
|
|
117
|
+
try {
|
|
118
|
+
(await this.api.beacon.submitPayloadAttestationMessages({payloadAttestationMessages})).assertOk();
|
|
119
|
+
this.logger.info("Published payload attestation messages", {
|
|
120
|
+
slot,
|
|
121
|
+
beaconBlockRoot: prettyBytes(beaconBlockRootHex),
|
|
122
|
+
count: payloadAttestationMessages.length,
|
|
123
|
+
});
|
|
124
|
+
this.metrics?.publishedPayloadAttestations.inc(payloadAttestationMessages.length);
|
|
125
|
+
} catch (e) {
|
|
126
|
+
this.metrics?.ptcError.inc({error: "publish"});
|
|
127
|
+
this.logger.error("Error publishing payload attestation messages", {slot}, e as Error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {ApiClient, routes} from "@lodestar/api";
|
|
2
|
+
import {ChainForkConfig} from "@lodestar/config";
|
|
3
|
+
import {SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
|
|
4
|
+
import {computeEpochAtSlot, isStartSlotOfEpoch} from "@lodestar/state-transition";
|
|
5
|
+
import {Epoch, RootHex, Slot, ValidatorIndex} from "@lodestar/types";
|
|
6
|
+
import {toPubkeyHex} from "@lodestar/utils";
|
|
7
|
+
import {Metrics} from "../metrics.js";
|
|
8
|
+
import {PubkeyHex} from "../types.js";
|
|
9
|
+
import {IClock, LoggerVc} from "../util/index.js";
|
|
10
|
+
import {ChainHeaderTracker, HeadEventData} from "./chainHeaderTracker.js";
|
|
11
|
+
import {SyncingStatusTracker} from "./syncingStatusTracker.js";
|
|
12
|
+
import {ValidatorStore} from "./validatorStore.js";
|
|
13
|
+
|
|
14
|
+
/** Only retain `HISTORICAL_DUTIES_EPOCHS` duties prior to the current epoch. */
|
|
15
|
+
const HISTORICAL_DUTIES_EPOCHS = 2;
|
|
16
|
+
|
|
17
|
+
type PtcDutiesAtEpoch = {dependentRoot: RootHex; dutiesByIndex: Map<ValidatorIndex, routes.validator.PtcDuty>};
|
|
18
|
+
|
|
19
|
+
export class PtcDutiesService {
|
|
20
|
+
/** Maps a validator index to its PTC duty for each epoch. */
|
|
21
|
+
private readonly dutiesByIndexByEpoch = new Map<Epoch, PtcDutiesAtEpoch>();
|
|
22
|
+
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly config: ChainForkConfig,
|
|
25
|
+
private readonly logger: LoggerVc,
|
|
26
|
+
private readonly api: ApiClient,
|
|
27
|
+
private readonly clock: IClock,
|
|
28
|
+
private readonly validatorStore: ValidatorStore,
|
|
29
|
+
chainHeadTracker: ChainHeaderTracker,
|
|
30
|
+
syncingStatusTracker: SyncingStatusTracker,
|
|
31
|
+
private readonly metrics: Metrics | null
|
|
32
|
+
) {
|
|
33
|
+
clock.runEveryEpoch(this.runDutiesTasks);
|
|
34
|
+
chainHeadTracker.runOnNewHead(this.onNewHead);
|
|
35
|
+
syncingStatusTracker.runOnResynced(async (slot) => {
|
|
36
|
+
// Skip on first slot of epoch since tasks are already scheduled.
|
|
37
|
+
if (!isStartSlotOfEpoch(slot)) {
|
|
38
|
+
return this.runDutiesTasks(computeEpochAtSlot(slot));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (metrics) {
|
|
43
|
+
metrics.ptcDutiesCount.addCollect(() => {
|
|
44
|
+
const currentSlot = this.clock.getCurrentSlot();
|
|
45
|
+
let duties = 0;
|
|
46
|
+
let nextDutySlot = null;
|
|
47
|
+
for (const [epoch, ptcDutiesAtEpoch] of this.dutiesByIndexByEpoch) {
|
|
48
|
+
duties += ptcDutiesAtEpoch.dutiesByIndex.size;
|
|
49
|
+
|
|
50
|
+
// Epochs are sorted, stop searching once a next duty slot is found.
|
|
51
|
+
if (epoch < this.clock.currentEpoch || nextDutySlot !== null) continue;
|
|
52
|
+
|
|
53
|
+
for (const duty of ptcDutiesAtEpoch.dutiesByIndex.values()) {
|
|
54
|
+
if (duty.slot > currentSlot && (nextDutySlot === null || duty.slot < nextDutySlot)) {
|
|
55
|
+
nextDutySlot = duty.slot;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
metrics.ptcDutiesCount.set(duties);
|
|
60
|
+
metrics.ptcDutiesEpochCount.set(this.dutiesByIndexByEpoch.size);
|
|
61
|
+
if (nextDutySlot !== null) metrics.ptcDutiesNextSlot.set(nextDutySlot);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
removeDutiesForKey(pubkey: PubkeyHex): void {
|
|
67
|
+
for (const [epoch, ptcDutiesAtEpoch] of this.dutiesByIndexByEpoch) {
|
|
68
|
+
for (const [validatorIndex, duty] of ptcDutiesAtEpoch.dutiesByIndex) {
|
|
69
|
+
if (toPubkeyHex(duty.pubkey) === pubkey) {
|
|
70
|
+
ptcDutiesAtEpoch.dutiesByIndex.delete(validatorIndex);
|
|
71
|
+
if (ptcDutiesAtEpoch.dutiesByIndex.size === 0) {
|
|
72
|
+
this.dutiesByIndexByEpoch.delete(epoch);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Returns all PTC duties for the given slot. */
|
|
80
|
+
getDutiesAtSlot(slot: Slot): routes.validator.PtcDuty[] {
|
|
81
|
+
const epoch = computeEpochAtSlot(slot);
|
|
82
|
+
const duties: routes.validator.PtcDuty[] = [];
|
|
83
|
+
const epochDuties = this.dutiesByIndexByEpoch.get(epoch);
|
|
84
|
+
if (epochDuties === undefined) {
|
|
85
|
+
return duties;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const duty of epochDuties.dutiesByIndex.values()) {
|
|
89
|
+
if (duty.slot === slot) {
|
|
90
|
+
duties.push(duty);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return duties;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private runDutiesTasks = async (epoch: Epoch): Promise<void> => {
|
|
98
|
+
const nextEpoch = epoch + 1;
|
|
99
|
+
if (!isForkPostGloas(this.config.getForkName(nextEpoch * SLOTS_PER_EPOCH))) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await Promise.all([
|
|
104
|
+
this.pollPtcDuties(epoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => {
|
|
105
|
+
this.logger.error("Error on poll PTC duties", {epoch}, e);
|
|
106
|
+
}),
|
|
107
|
+
|
|
108
|
+
this.validatorStore
|
|
109
|
+
.pollValidatorIndices()
|
|
110
|
+
.then((newIndices) => this.pollPtcDuties(epoch, newIndices))
|
|
111
|
+
.catch((e: Error) => {
|
|
112
|
+
this.logger.error("Error on poll indices and PTC duties", {epoch}, e);
|
|
113
|
+
}),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
this.pruneOldDuties(epoch);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
private async pollPtcDuties(currentEpoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
|
|
120
|
+
const nextEpoch = currentEpoch + 1;
|
|
121
|
+
|
|
122
|
+
if (indexArr.length === 0) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const epoch of [currentEpoch, nextEpoch]) {
|
|
127
|
+
await this.pollPtcDutiesForEpoch(epoch, indexArr).catch((e: Error) => {
|
|
128
|
+
this.logger.error("Failed to download PTC duties", {epoch}, e);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private async pollPtcDutiesForEpoch(epoch: Epoch, indexArr: ValidatorIndex[]): Promise<void> {
|
|
134
|
+
if (epoch < 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!isForkPostGloas(this.config.getForkName(epoch * SLOTS_PER_EPOCH))) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const res = await this.api.validator.getPtcDuties({epoch, indices: indexArr});
|
|
143
|
+
const ptcDuties = res.value();
|
|
144
|
+
const {dependentRoot} = res.meta();
|
|
145
|
+
const relevantDuties = ptcDuties.filter((duty) => {
|
|
146
|
+
const pubkeyHex = toPubkeyHex(duty.pubkey);
|
|
147
|
+
return this.validatorStore.hasVotingPubkey(pubkeyHex) && this.validatorStore.isDoppelgangerSafe(pubkeyHex);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
this.logger.debug("Downloaded PTC duties", {epoch, dependentRoot, count: relevantDuties.length});
|
|
151
|
+
|
|
152
|
+
const dutiesAtEpoch = this.dutiesByIndexByEpoch.get(epoch);
|
|
153
|
+
const priorDependentRoot = dutiesAtEpoch?.dependentRoot;
|
|
154
|
+
const dependentRootChanged = priorDependentRoot !== undefined && priorDependentRoot !== dependentRoot;
|
|
155
|
+
|
|
156
|
+
if (!priorDependentRoot || dependentRootChanged) {
|
|
157
|
+
const dutiesByIndex = new Map<ValidatorIndex, routes.validator.PtcDuty>();
|
|
158
|
+
for (const duty of relevantDuties) {
|
|
159
|
+
dutiesByIndex.set(duty.validatorIndex, duty);
|
|
160
|
+
}
|
|
161
|
+
this.dutiesByIndexByEpoch.set(epoch, {dependentRoot, dutiesByIndex});
|
|
162
|
+
|
|
163
|
+
if (priorDependentRoot && dependentRootChanged) {
|
|
164
|
+
this.metrics?.ptcDutiesReorg.inc();
|
|
165
|
+
this.logger.warn("PTC duties re-org. This may happen from time to time", {
|
|
166
|
+
priorDependentRoot,
|
|
167
|
+
dependentRoot,
|
|
168
|
+
epoch,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
const existingDuties = dutiesAtEpoch.dutiesByIndex;
|
|
173
|
+
const existingDutiesCount = existingDuties.size;
|
|
174
|
+
const discoveredNewDuties = relevantDuties.length > existingDutiesCount;
|
|
175
|
+
|
|
176
|
+
if (discoveredNewDuties) {
|
|
177
|
+
for (const duty of relevantDuties) {
|
|
178
|
+
if (!existingDuties.has(duty.validatorIndex)) {
|
|
179
|
+
existingDuties.set(duty.validatorIndex, duty);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.logger.debug("Discovered new PTC duties", {
|
|
184
|
+
epoch,
|
|
185
|
+
dependentRoot,
|
|
186
|
+
count: relevantDuties.length - existingDutiesCount,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private onNewHead = async ({
|
|
193
|
+
slot,
|
|
194
|
+
previousDutyDependentRoot,
|
|
195
|
+
currentDutyDependentRoot,
|
|
196
|
+
}: HeadEventData): Promise<void> => {
|
|
197
|
+
const currentEpoch = computeEpochAtSlot(slot);
|
|
198
|
+
const nextEpoch = currentEpoch + 1;
|
|
199
|
+
|
|
200
|
+
const nextEpochDependentRoot = this.dutiesByIndexByEpoch.get(nextEpoch)?.dependentRoot;
|
|
201
|
+
if (nextEpochDependentRoot && currentDutyDependentRoot !== nextEpochDependentRoot) {
|
|
202
|
+
this.logger.warn("Potential next epoch PTC duties reorg", {
|
|
203
|
+
slot,
|
|
204
|
+
dutyEpoch: nextEpoch,
|
|
205
|
+
priorDependentRoot: nextEpochDependentRoot,
|
|
206
|
+
newDependentRoot: currentDutyDependentRoot,
|
|
207
|
+
});
|
|
208
|
+
await this.handlePtcDutiesReorg(nextEpoch, slot, nextEpochDependentRoot, currentDutyDependentRoot);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const currentEpochDependentRoot = this.dutiesByIndexByEpoch.get(currentEpoch)?.dependentRoot;
|
|
212
|
+
if (currentEpochDependentRoot && currentEpochDependentRoot !== previousDutyDependentRoot) {
|
|
213
|
+
this.logger.warn("Potential current epoch PTC duties reorg", {
|
|
214
|
+
slot,
|
|
215
|
+
dutyEpoch: currentEpoch,
|
|
216
|
+
priorDependentRoot: currentEpochDependentRoot,
|
|
217
|
+
newDependentRoot: previousDutyDependentRoot,
|
|
218
|
+
});
|
|
219
|
+
await this.handlePtcDutiesReorg(currentEpoch, slot, currentEpochDependentRoot, previousDutyDependentRoot);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
private async handlePtcDutiesReorg(
|
|
224
|
+
dutyEpoch: Epoch,
|
|
225
|
+
slot: Slot,
|
|
226
|
+
oldDependentRoot: RootHex,
|
|
227
|
+
newDependentRoot: RootHex
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
this.metrics?.ptcDutiesReorg.inc();
|
|
230
|
+
const logContext = {dutyEpoch, slot, oldDependentRoot, newDependentRoot};
|
|
231
|
+
this.logger.debug("Redownload PTC duties", logContext);
|
|
232
|
+
|
|
233
|
+
await this.pollPtcDutiesForEpoch(dutyEpoch, this.validatorStore.getAllLocalIndices()).catch((e: Error) => {
|
|
234
|
+
this.logger.error("Failed to redownload PTC duties when reorg happens", logContext, e);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Run once per epoch to prune duties map. */
|
|
239
|
+
private pruneOldDuties(currentEpoch: Epoch): void {
|
|
240
|
+
for (const epoch of this.dutiesByIndexByEpoch.keys()) {
|
|
241
|
+
if (epoch + HISTORICAL_DUTIES_EPOCHS < currentEpoch) {
|
|
242
|
+
this.dutiesByIndexByEpoch.delete(epoch);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
DOMAIN_BEACON_BUILDER,
|
|
10
10
|
DOMAIN_BEACON_PROPOSER,
|
|
11
11
|
DOMAIN_CONTRIBUTION_AND_PROOF,
|
|
12
|
+
DOMAIN_PROPOSER_PREFERENCES,
|
|
13
|
+
DOMAIN_PTC_ATTESTER,
|
|
12
14
|
DOMAIN_RANDAO,
|
|
13
15
|
DOMAIN_SELECTION_PROOF,
|
|
14
16
|
DOMAIN_SYNC_COMMITTEE,
|
|
@@ -668,6 +670,77 @@ export class ValidatorStore {
|
|
|
668
670
|
};
|
|
669
671
|
}
|
|
670
672
|
|
|
673
|
+
async signPayloadAttestation(
|
|
674
|
+
duty: routes.validator.PtcDuty,
|
|
675
|
+
data: gloas.PayloadAttestationData,
|
|
676
|
+
currentSlot: Slot,
|
|
677
|
+
logger?: LoggerVc
|
|
678
|
+
): Promise<gloas.PayloadAttestationMessage> {
|
|
679
|
+
if (data.slot > currentSlot) {
|
|
680
|
+
throw Error(`Not signing payload attestation with slot ${data.slot} greater than current slot ${currentSlot}`);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
this.assertDoppelgangerSafe(duty.pubkey);
|
|
684
|
+
this.validatePtcDuty(duty, data);
|
|
685
|
+
|
|
686
|
+
const signingSlot = data.slot;
|
|
687
|
+
const domain = this.config.getDomain(signingSlot, DOMAIN_PTC_ATTESTER);
|
|
688
|
+
const signingRoot = computeSigningRoot(ssz.gloas.PayloadAttestationData, data, domain);
|
|
689
|
+
|
|
690
|
+
logger?.debug("Signing payload attestation message", {
|
|
691
|
+
slot: signingSlot,
|
|
692
|
+
beaconBlockRoot: toRootHex(data.beaconBlockRoot),
|
|
693
|
+
signingRoot: toRootHex(signingRoot),
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const signableMessage: SignableMessage = {
|
|
697
|
+
type: SignableMessageType.PAYLOAD_ATTESTATION,
|
|
698
|
+
data,
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
validatorIndex: duty.validatorIndex,
|
|
703
|
+
data,
|
|
704
|
+
signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async signProposerPreferences(
|
|
709
|
+
duty: routes.validator.ProposerDuty,
|
|
710
|
+
dependentRoot: Uint8Array,
|
|
711
|
+
feeRecipient: ExecutionAddress,
|
|
712
|
+
gasLimit: number,
|
|
713
|
+
currentSlot: Slot
|
|
714
|
+
): Promise<gloas.SignedProposerPreferences> {
|
|
715
|
+
if (duty.slot <= currentSlot) {
|
|
716
|
+
throw Error(`Not signing proposer preferences for past slot ${duty.slot} (current ${currentSlot})`);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
this.assertDoppelgangerSafe(duty.pubkey);
|
|
720
|
+
|
|
721
|
+
const message: gloas.ProposerPreferences = {
|
|
722
|
+
dependentRoot,
|
|
723
|
+
proposalSlot: duty.slot,
|
|
724
|
+
validatorIndex: duty.validatorIndex,
|
|
725
|
+
feeRecipient: fromHex(feeRecipient),
|
|
726
|
+
gasLimit,
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
const signingSlot = duty.slot;
|
|
730
|
+
const domain = this.config.getDomain(signingSlot, DOMAIN_PROPOSER_PREFERENCES);
|
|
731
|
+
const signingRoot = computeSigningRoot(ssz.gloas.ProposerPreferences, message, domain);
|
|
732
|
+
|
|
733
|
+
const signableMessage: SignableMessage = {
|
|
734
|
+
type: SignableMessageType.PROPOSER_PREFERENCES,
|
|
735
|
+
data: message,
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
return {
|
|
739
|
+
message,
|
|
740
|
+
signature: await this.getSignature(duty.pubkey, signingRoot, signingSlot, signableMessage),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
671
744
|
async signAttestationSelectionProof(pubkey: BLSPubkeyMaybeHex, slot: Slot): Promise<BLSSignature> {
|
|
672
745
|
const signingSlot = slot;
|
|
673
746
|
const domain = this.config.getDomain(slot, DOMAIN_SELECTION_PROOF);
|
|
@@ -852,6 +925,12 @@ export class ValidatorStore {
|
|
|
852
925
|
}
|
|
853
926
|
}
|
|
854
927
|
|
|
928
|
+
private validatePtcDuty(duty: routes.validator.PtcDuty, data: gloas.PayloadAttestationData): void {
|
|
929
|
+
if (duty.slot !== data.slot) {
|
|
930
|
+
throw Error(`Inconsistent PTC duties during signing: duty.slot ${duty.slot} != data.slot ${data.slot}`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
855
934
|
private assertDoppelgangerSafe(pubKey: PubkeyHex | BLSPubkey): void {
|
|
856
935
|
const pubkeyHex = typeof pubKey === "string" ? pubKey : toPubkeyHex(pubKey);
|
|
857
936
|
if (!this.isDoppelgangerSafe(pubkeyHex)) {
|
|
@@ -34,6 +34,8 @@ export enum SignableMessageType {
|
|
|
34
34
|
SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF = "SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF",
|
|
35
35
|
VALIDATOR_REGISTRATION = "VALIDATOR_REGISTRATION",
|
|
36
36
|
EXECUTION_PAYLOAD_ENVELOPE = "EXECUTION_PAYLOAD_ENVELOPE",
|
|
37
|
+
PAYLOAD_ATTESTATION = "PAYLOAD_ATTESTATION",
|
|
38
|
+
PROPOSER_PREFERENCES = "PROPOSER_PREFERENCES",
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const AggregationSlotType = new ContainerType({
|
|
@@ -83,7 +85,9 @@ export type SignableMessage =
|
|
|
83
85
|
| {type: SignableMessageType.SYNC_COMMITTEE_SELECTION_PROOF; data: ValueOf<typeof SyncAggregatorSelectionDataType>}
|
|
84
86
|
| {type: SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF; data: altair.ContributionAndProof}
|
|
85
87
|
| {type: SignableMessageType.VALIDATOR_REGISTRATION; data: ValidatorRegistrationV1}
|
|
86
|
-
| {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope}
|
|
88
|
+
| {type: SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE; data: gloas.ExecutionPayloadEnvelope}
|
|
89
|
+
| {type: SignableMessageType.PAYLOAD_ATTESTATION; data: gloas.PayloadAttestationData}
|
|
90
|
+
| {type: SignableMessageType.PROPOSER_PREFERENCES; data: gloas.ProposerPreferences};
|
|
87
91
|
|
|
88
92
|
const requiresForkInfo: Record<SignableMessageType, boolean> = {
|
|
89
93
|
[SignableMessageType.AGGREGATION_SLOT]: true,
|
|
@@ -99,6 +103,8 @@ const requiresForkInfo: Record<SignableMessageType, boolean> = {
|
|
|
99
103
|
[SignableMessageType.SYNC_COMMITTEE_CONTRIBUTION_AND_PROOF]: true,
|
|
100
104
|
[SignableMessageType.VALIDATOR_REGISTRATION]: false,
|
|
101
105
|
[SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE]: true,
|
|
106
|
+
[SignableMessageType.PAYLOAD_ATTESTATION]: true,
|
|
107
|
+
[SignableMessageType.PROPOSER_PREFERENCES]: true,
|
|
102
108
|
};
|
|
103
109
|
|
|
104
110
|
type Web3SignerSerializedRequest = {
|
|
@@ -273,6 +279,12 @@ function serializerSignableMessagePayload(config: BeaconConfig, payload: Signabl
|
|
|
273
279
|
|
|
274
280
|
case SignableMessageType.EXECUTION_PAYLOAD_ENVELOPE:
|
|
275
281
|
return {execution_payload_envelope: ssz.gloas.ExecutionPayloadEnvelope.toJson(payload.data)};
|
|
282
|
+
|
|
283
|
+
case SignableMessageType.PAYLOAD_ATTESTATION:
|
|
284
|
+
return {payload_attestation: ssz.gloas.PayloadAttestationData.toJson(payload.data)};
|
|
285
|
+
|
|
286
|
+
case SignableMessageType.PROPOSER_PREFERENCES:
|
|
287
|
+
return {proposer_preferences: ssz.gloas.ProposerPreferences.toJson(payload.data)};
|
|
276
288
|
}
|
|
277
289
|
}
|
|
278
290
|
|
package/src/util/params.ts
CHANGED
|
@@ -327,5 +327,8 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record<keyof ConfigWit
|
|
|
327
327
|
BUILDER_PENDING_WITHDRAWALS_LIMIT: gloasForkRelevant,
|
|
328
328
|
MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: gloasForkRelevant,
|
|
329
329
|
MIN_BUILDER_WITHDRAWABILITY_DELAY: gloasForkRelevant,
|
|
330
|
+
CHURN_LIMIT_QUOTIENT_GLOAS: gloasForkRelevant,
|
|
331
|
+
CONSOLIDATION_CHURN_LIMIT_QUOTIENT: gloasForkRelevant,
|
|
332
|
+
MAX_PER_EPOCH_ACTIVATION_CHURN_LIMIT_GLOAS: gloasForkRelevant,
|
|
330
333
|
};
|
|
331
334
|
}
|
package/src/validator.ts
CHANGED
|
@@ -9,12 +9,15 @@ import {Metrics} from "./metrics.js";
|
|
|
9
9
|
import {MetaDataRepository} from "./repositories/metaDataRepository.js";
|
|
10
10
|
import {AttestationService} from "./services/attestation.js";
|
|
11
11
|
import {BlockProposingService} from "./services/block.js";
|
|
12
|
+
import {BlockDutiesService} from "./services/blockDuties.js";
|
|
12
13
|
import {ChainHeaderTracker} from "./services/chainHeaderTracker.js";
|
|
13
14
|
import {DoppelgangerService} from "./services/doppelgangerService.js";
|
|
14
15
|
import {ValidatorEventEmitter} from "./services/emitter.js";
|
|
15
16
|
import {ExternalSignerOptions, pollExternalSignerPubkeys} from "./services/externalSignerSync.js";
|
|
16
17
|
import {IndicesService} from "./services/indices.js";
|
|
17
18
|
import {pollBuilderValidatorRegistration, pollPrepareBeaconProposer} from "./services/prepareBeaconProposer.js";
|
|
19
|
+
import {ProposerPreferencesService} from "./services/proposerPreferences.js";
|
|
20
|
+
import {PtcService} from "./services/ptc.js";
|
|
18
21
|
import {SyncCommitteeService} from "./services/syncCommittee.js";
|
|
19
22
|
import {SyncingStatusTracker} from "./services/syncingStatusTracker.js";
|
|
20
23
|
import {Signer, ValidatorProposerConfig, ValidatorStore, defaultOptions} from "./services/validatorStore.js";
|
|
@@ -30,6 +33,7 @@ export type ValidatorModules = {
|
|
|
30
33
|
slashingProtection: ISlashingProtection;
|
|
31
34
|
blockProposingService: BlockProposingService;
|
|
32
35
|
attestationService: AttestationService;
|
|
36
|
+
ptcService: PtcService;
|
|
33
37
|
syncCommitteeService: SyncCommitteeService;
|
|
34
38
|
config: BeaconConfig;
|
|
35
39
|
api: ApiClient;
|
|
@@ -84,6 +88,7 @@ export class Validator {
|
|
|
84
88
|
private readonly slashingProtection: ISlashingProtection;
|
|
85
89
|
private readonly blockProposingService: BlockProposingService;
|
|
86
90
|
private readonly attestationService: AttestationService;
|
|
91
|
+
private readonly ptcService: PtcService;
|
|
87
92
|
private readonly syncCommitteeService: SyncCommitteeService;
|
|
88
93
|
private readonly config: BeaconConfig;
|
|
89
94
|
private readonly api: ApiClient;
|
|
@@ -102,6 +107,7 @@ export class Validator {
|
|
|
102
107
|
slashingProtection,
|
|
103
108
|
blockProposingService,
|
|
104
109
|
attestationService,
|
|
110
|
+
ptcService,
|
|
105
111
|
syncCommitteeService,
|
|
106
112
|
config,
|
|
107
113
|
api,
|
|
@@ -118,6 +124,7 @@ export class Validator {
|
|
|
118
124
|
this.slashingProtection = slashingProtection;
|
|
119
125
|
this.blockProposingService = blockProposingService;
|
|
120
126
|
this.attestationService = attestationService;
|
|
127
|
+
this.ptcService = ptcService;
|
|
121
128
|
this.syncCommitteeService = syncCommitteeService;
|
|
122
129
|
this.config = config;
|
|
123
130
|
this.api = api;
|
|
@@ -225,13 +232,32 @@ export class Validator {
|
|
|
225
232
|
// We set infinity to prevent MaxListenersExceededWarning which get logged when listeners > 10
|
|
226
233
|
emitter.setMaxListeners(Infinity);
|
|
227
234
|
|
|
228
|
-
const chainHeaderTracker = new ChainHeaderTracker(logger, api, emitter);
|
|
235
|
+
const chainHeaderTracker = new ChainHeaderTracker(config, logger, api, emitter);
|
|
229
236
|
const syncingStatusTracker = new SyncingStatusTracker(logger, api, clock, metrics);
|
|
230
237
|
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
238
|
+
const blockDutiesService = new BlockDutiesService(
|
|
239
|
+
config,
|
|
240
|
+
loggerVc,
|
|
241
|
+
api,
|
|
242
|
+
clock,
|
|
243
|
+
validatorStore,
|
|
244
|
+
chainHeaderTracker,
|
|
245
|
+
metrics
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const blockProposingService = new BlockProposingService(
|
|
249
|
+
config,
|
|
250
|
+
loggerVc,
|
|
251
|
+
api,
|
|
252
|
+
clock,
|
|
253
|
+
validatorStore,
|
|
254
|
+
blockDutiesService,
|
|
255
|
+
metrics,
|
|
256
|
+
{
|
|
257
|
+
broadcastValidation: opts.broadcastValidation ?? defaultOptions.broadcastValidation,
|
|
258
|
+
blindedLocal: opts.blindedLocal ?? defaultOptions.blindedLocal,
|
|
259
|
+
}
|
|
260
|
+
);
|
|
235
261
|
|
|
236
262
|
const attestationService = new AttestationService(
|
|
237
263
|
loggerVc,
|
|
@@ -249,6 +275,18 @@ export class Validator {
|
|
|
249
275
|
}
|
|
250
276
|
);
|
|
251
277
|
|
|
278
|
+
const ptcService = new PtcService(
|
|
279
|
+
config,
|
|
280
|
+
loggerVc,
|
|
281
|
+
api,
|
|
282
|
+
clock,
|
|
283
|
+
validatorStore,
|
|
284
|
+
emitter,
|
|
285
|
+
chainHeaderTracker,
|
|
286
|
+
syncingStatusTracker,
|
|
287
|
+
metrics
|
|
288
|
+
);
|
|
289
|
+
|
|
252
290
|
const syncCommitteeService = new SyncCommitteeService(
|
|
253
291
|
config,
|
|
254
292
|
loggerVc,
|
|
@@ -265,6 +303,8 @@ export class Validator {
|
|
|
265
303
|
}
|
|
266
304
|
);
|
|
267
305
|
|
|
306
|
+
new ProposerPreferencesService(config, loggerVc, api, clock, validatorStore, blockDutiesService, metrics);
|
|
307
|
+
|
|
268
308
|
return new Validator({
|
|
269
309
|
opts,
|
|
270
310
|
genesis,
|
|
@@ -272,6 +312,7 @@ export class Validator {
|
|
|
272
312
|
slashingProtection,
|
|
273
313
|
blockProposingService,
|
|
274
314
|
attestationService,
|
|
315
|
+
ptcService,
|
|
275
316
|
syncCommitteeService,
|
|
276
317
|
config,
|
|
277
318
|
api,
|
|
@@ -338,6 +379,7 @@ export class Validator {
|
|
|
338
379
|
removeDutiesForKey(pubkey: PubkeyHex): void {
|
|
339
380
|
this.blockProposingService.removeDutiesForKey(pubkey);
|
|
340
381
|
this.attestationService.removeDutiesForKey(pubkey);
|
|
382
|
+
this.ptcService.removeDutiesForKey(pubkey);
|
|
341
383
|
this.syncCommitteeService.removeDutiesForKey(pubkey);
|
|
342
384
|
}
|
|
343
385
|
|