@lodestar/validator 1.50.0-dev.fef46df5ba → 1.50.0-rc.0
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/services/block.d.ts.map +1 -1
- package/lib/services/block.js +14 -3
- package/lib/services/block.js.map +1 -1
- package/lib/services/prepareBeaconProposer.d.ts +2 -1
- package/lib/services/prepareBeaconProposer.d.ts.map +1 -1
- package/lib/services/prepareBeaconProposer.js +5 -1
- package/lib/services/prepareBeaconProposer.js.map +1 -1
- package/lib/services/proposalPreferences.d.ts +47 -0
- package/lib/services/proposalPreferences.d.ts.map +1 -0
- package/lib/services/proposalPreferences.js +260 -0
- package/lib/services/proposalPreferences.js.map +1 -0
- package/lib/services/ptc.d.ts.map +1 -1
- package/lib/services/ptc.js +11 -2
- package/lib/services/ptc.js.map +1 -1
- package/lib/services/ptcDuties.js +2 -2
- package/lib/services/ptcDuties.js.map +1 -1
- package/lib/services/validatorStore.d.ts.map +1 -1
- package/lib/services/validatorStore.js +4 -9
- package/lib/services/validatorStore.js.map +1 -1
- package/lib/validator.d.ts.map +1 -1
- package/lib/validator.js +2 -4
- package/lib/validator.js.map +1 -1
- package/package.json +13 -13
- package/src/services/block.ts +18 -4
- package/src/services/prepareBeaconProposer.ts +5 -1
- package/src/services/proposalPreferences.ts +308 -0
- package/src/services/ptc.ts +12 -3
- package/src/services/ptcDuties.ts +2 -2
- package/src/services/validatorStore.ts +4 -8
- package/src/validator.ts +11 -4
- package/lib/services/builderPreferences.d.ts +0 -27
- package/lib/services/builderPreferences.d.ts.map +0 -1
- package/lib/services/builderPreferences.js +0 -127
- package/lib/services/builderPreferences.js.map +0 -1
- package/lib/services/proposerPreferences.d.ts +0 -31
- package/lib/services/proposerPreferences.d.ts.map +0 -1
- package/lib/services/proposerPreferences.js +0 -130
- package/lib/services/proposerPreferences.js.map +0 -1
- package/src/services/builderPreferences.ts +0 -142
- package/src/services/proposerPreferences.ts +0 -154
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { SLOTS_PER_EPOCH, isForkPostGloas } from "@lodestar/params";
|
|
2
|
-
import { computeEpochAtSlot } from "@lodestar/state-transition";
|
|
3
|
-
import { fromHex, toPubkeyHex } from "@lodestar/utils";
|
|
4
|
-
/**
|
|
5
|
-
* Submit a proposer's `SignedProposerPreferences` this many slots before the proposal slot.
|
|
6
|
-
*
|
|
7
|
-
* Earlier submission means more reorg-triggered resubmits (and gossip flood); later
|
|
8
|
-
* submission risks missing the bid-auction window for this proposal slot. The bid for
|
|
9
|
-
* slot S typically arrives at slot S-1, so we want preferences propagated to the network
|
|
10
|
-
* and consumed by builders before then. SLOTS_PER_EPOCH / 4 (8 slots @ 32 SPE, ~96s @ 12s
|
|
11
|
-
* slots) gives ample margin while bounding redundant resubmits.
|
|
12
|
-
*/
|
|
13
|
-
const SUBMIT_BEFORE_PROPOSAL_SLOTS = Math.floor(SLOTS_PER_EPOCH / 4);
|
|
14
|
-
/**
|
|
15
|
-
* Signs and submits `SignedProposerPreferences` for any local validator that will propose
|
|
16
|
-
* within the next `SUBMIT_BEFORE_PROPOSAL_SLOTS`. Re-submits automatically when the proposer
|
|
17
|
-
* dependent root for an epoch shifts (e.g. after a reorg) — detected by comparing the cached
|
|
18
|
-
* `dependentRoot` reported by `BlockDutiesService` against the one we last submitted under.
|
|
19
|
-
*
|
|
20
|
-
* Proposers should broadcast their preferences before the fork so the proposer preference caches
|
|
21
|
-
* of beacon nodes and builders are warm for the first Gloas slots. We start submitting
|
|
22
|
-
* as soon as a duty's proposal slot is in Gloas, which is up to `SUBMIT_BEFORE_PROPOSAL_SLOTS`
|
|
23
|
-
* before the fork, so only the first few Gloas slots are affected by this pre-fork submission.
|
|
24
|
-
*/
|
|
25
|
-
export class ProposerPreferencesService {
|
|
26
|
-
config;
|
|
27
|
-
logger;
|
|
28
|
-
api;
|
|
29
|
-
validatorStore;
|
|
30
|
-
blockDutiesService;
|
|
31
|
-
submitted = new Map();
|
|
32
|
-
activeScheduledGasLimit;
|
|
33
|
-
constructor(config, logger, api, clock, validatorStore, blockDutiesService, _metrics) {
|
|
34
|
-
this.config = config;
|
|
35
|
-
this.logger = logger;
|
|
36
|
-
this.api = api;
|
|
37
|
-
this.validatorStore = validatorStore;
|
|
38
|
-
this.blockDutiesService = blockDutiesService;
|
|
39
|
-
clock.runEverySlot(this.runProposerPreferencesTask);
|
|
40
|
-
clock.runEveryEpoch(this.runEveryEpochTask);
|
|
41
|
-
}
|
|
42
|
-
runProposerPreferencesTask = async (slot) => {
|
|
43
|
-
// Start running once the submission window (`slot + SUBMIT_BEFORE_PROPOSAL_SLOTS`) reaches
|
|
44
|
-
// Gloas, i.e. already in the epoch before the fork. This allows builders to prepare and
|
|
45
|
-
// submit bids for the first Gloas slots.
|
|
46
|
-
if (!isForkPostGloas(this.config.getForkName(slot + SUBMIT_BEFORE_PROPOSAL_SLOTS))) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const currentEpoch = computeEpochAtSlot(slot);
|
|
50
|
-
const batch = [];
|
|
51
|
-
// Track which `(submission, slot)` pairs are pending an API submission so we can mark
|
|
52
|
-
// them only after the network call succeeds. Marking before would silently drop a
|
|
53
|
-
// preference on transient API failure (no retry until dependent_root shifts).
|
|
54
|
-
const pending = [];
|
|
55
|
-
for (const epoch of [currentEpoch, currentEpoch + 1]) {
|
|
56
|
-
const dutiesAtEpoch = this.blockDutiesService.getProposersAtEpoch(epoch);
|
|
57
|
-
if (!dutiesAtEpoch)
|
|
58
|
-
continue;
|
|
59
|
-
// Reset submission tracking if the dependent root for this epoch has shifted
|
|
60
|
-
// (e.g. due to a reorg). Any previously-submitted preferences are now stale.
|
|
61
|
-
let submission = this.submitted.get(epoch);
|
|
62
|
-
if (submission === undefined || submission.dependentRoot !== dutiesAtEpoch.dependentRoot) {
|
|
63
|
-
if (submission !== undefined) {
|
|
64
|
-
this.logger.info("Proposer-shuffling dependent root shifted; resubmitting preferences", {
|
|
65
|
-
epoch,
|
|
66
|
-
priorDependentRoot: submission.dependentRoot,
|
|
67
|
-
dependentRoot: dutiesAtEpoch.dependentRoot,
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
submission = { dependentRoot: dutiesAtEpoch.dependentRoot, slots: new Set() };
|
|
71
|
-
this.submitted.set(epoch, submission);
|
|
72
|
-
}
|
|
73
|
-
const dependentRootBytes = fromHex(dutiesAtEpoch.dependentRoot);
|
|
74
|
-
for (const duty of dutiesAtEpoch.data) {
|
|
75
|
-
if (duty.slot <= slot)
|
|
76
|
-
continue;
|
|
77
|
-
if (duty.slot > slot + SUBMIT_BEFORE_PROPOSAL_SLOTS)
|
|
78
|
-
continue;
|
|
79
|
-
if (!isForkPostGloas(this.config.getForkName(duty.slot)))
|
|
80
|
-
continue;
|
|
81
|
-
if (submission.slots.has(duty.slot))
|
|
82
|
-
continue;
|
|
83
|
-
try {
|
|
84
|
-
const pubkeyHex = toPubkeyHex(duty.pubkey);
|
|
85
|
-
const signed = await this.validatorStore.signProposerPreferences(duty, dependentRootBytes, this.validatorStore.getFeeRecipient(pubkeyHex), this.validatorStore.getGasLimit(pubkeyHex, duty.slot, this.logger), slot);
|
|
86
|
-
batch.push(signed);
|
|
87
|
-
pending.push({ submission, slot: duty.slot });
|
|
88
|
-
}
|
|
89
|
-
catch (e) {
|
|
90
|
-
this.logger.error("Error signing proposer preferences", { slot: duty.slot, validatorIndex: duty.validatorIndex }, e);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
if (batch.length === 0) {
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
try {
|
|
98
|
-
await this.api.validator.submitProposerPreferences({ signedProposerPreferences: batch });
|
|
99
|
-
// Only mark as submitted after the API call succeeds; a thrown error leaves the
|
|
100
|
-
// slot eligible for retry on the next tick.
|
|
101
|
-
for (const { submission, slot: submittedSlot } of pending) {
|
|
102
|
-
submission.slots.add(submittedSlot);
|
|
103
|
-
}
|
|
104
|
-
this.logger.debug("Submitted signed proposer preferences", { count: batch.length });
|
|
105
|
-
}
|
|
106
|
-
catch (e) {
|
|
107
|
-
this.logger.error("Error submitting signed proposer preferences", { count: batch.length }, e);
|
|
108
|
-
}
|
|
109
|
-
};
|
|
110
|
-
runEveryEpochTask = async (epoch) => {
|
|
111
|
-
const scheduledGasLimit = this.config.getScheduledGasLimit(epoch);
|
|
112
|
-
if (scheduledGasLimit !== undefined && scheduledGasLimit !== this.activeScheduledGasLimit) {
|
|
113
|
-
const configuredDefaultGasLimit = this.validatorStore.getConfiguredDefaultGasLimit();
|
|
114
|
-
this.logger.info("Gas limit schedule active", {
|
|
115
|
-
epoch,
|
|
116
|
-
recommendedGasLimit: scheduledGasLimit,
|
|
117
|
-
effectiveDefaultGasLimit: configuredDefaultGasLimit ?? scheduledGasLimit,
|
|
118
|
-
defaultGasLimitSource: configuredDefaultGasLimit === undefined ? "schedule" : "operator",
|
|
119
|
-
});
|
|
120
|
-
this.activeScheduledGasLimit = scheduledGasLimit;
|
|
121
|
-
}
|
|
122
|
-
// Drop tracking for past epochs; only currentEpoch and currentEpoch + 1 are ever processed.
|
|
123
|
-
for (const trackedEpoch of this.submitted.keys()) {
|
|
124
|
-
if (trackedEpoch < epoch) {
|
|
125
|
-
this.submitted.delete(trackedEpoch);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
//# sourceMappingURL=proposerPreferences.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"proposerPreferences.js","sourceRoot":"","sources":["../../src/services/proposerPreferences.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import {ApiClient, routes} from "@lodestar/api";
|
|
2
|
-
import {ChainForkConfig} from "@lodestar/config";
|
|
3
|
-
import {SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
|
|
4
|
-
import {IClock, computeEpochAtSlot} from "@lodestar/state-transition";
|
|
5
|
-
import {Epoch, RootHex, Slot} from "@lodestar/types";
|
|
6
|
-
import {toPubkeyHex} from "@lodestar/utils";
|
|
7
|
-
import {Metrics} from "../metrics.js";
|
|
8
|
-
import {LoggerVc} from "../util/index.js";
|
|
9
|
-
import {BlockDutiesService} from "./blockDuties.js";
|
|
10
|
-
import {ValidatorStore} from "./validatorStore.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Submit a proposer's builder preferences this many slots before the proposal slot.
|
|
14
|
-
* Same pacing as `ProposerPreferencesService`, builders must have the preferences
|
|
15
|
-
* before the bid request arrives at proposal time.
|
|
16
|
-
*/
|
|
17
|
-
const SUBMIT_BEFORE_PROPOSAL_SLOTS = Math.floor(SLOTS_PER_EPOCH / 4);
|
|
18
|
-
|
|
19
|
-
/** Per-epoch tracking of preferences already submitted under the current dependent_root. */
|
|
20
|
-
type SubmittedAtEpoch = {dependentRoot: RootHex; slots: Set<Slot>};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Signs and submits builder preferences for any local validator that will propose within the
|
|
24
|
-
* next `SUBMIT_BEFORE_PROPOSAL_SLOTS` and has external builders configured. Signing the request
|
|
25
|
-
* auths here also pre-fills the auth cache used at proposal time.
|
|
26
|
-
*
|
|
27
|
-
* The beacon node submits each entry to the builder at the entry's url. Re-submits automatically
|
|
28
|
-
* when the proposer dependent root for an epoch shifts (e.g. after a reorg) since the proposer
|
|
29
|
-
* for a slot may have changed.
|
|
30
|
-
*/
|
|
31
|
-
export class BuilderPreferencesService {
|
|
32
|
-
private readonly submitted = new Map<Epoch, SubmittedAtEpoch>();
|
|
33
|
-
|
|
34
|
-
constructor(
|
|
35
|
-
private readonly config: ChainForkConfig,
|
|
36
|
-
private readonly logger: LoggerVc,
|
|
37
|
-
private readonly api: ApiClient,
|
|
38
|
-
clock: IClock,
|
|
39
|
-
private readonly validatorStore: ValidatorStore,
|
|
40
|
-
private readonly blockDutiesService: BlockDutiesService,
|
|
41
|
-
_metrics: Metrics | null
|
|
42
|
-
) {
|
|
43
|
-
clock.runEverySlot(this.runBuilderPreferencesTask);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
private runBuilderPreferencesTask = async (slot: Slot): Promise<void> => {
|
|
47
|
-
// Start running once the submission window (`slot + SUBMIT_BEFORE_PROPOSAL_SLOTS`) reaches
|
|
48
|
-
// Gloas, i.e. already in the epoch before the fork. This allows builders to prepare and
|
|
49
|
-
// submit bids for the first Gloas slots.
|
|
50
|
-
if (!isForkPostGloas(this.config.getForkName(slot + SUBMIT_BEFORE_PROPOSAL_SLOTS))) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const currentEpoch = computeEpochAtSlot(slot);
|
|
55
|
-
const entries: routes.validator.BuilderPreferencesEntry[] = [];
|
|
56
|
-
// Track which `(submission, slot)` pairs are pending an API submission so we can mark
|
|
57
|
-
// them only after the network call succeeds. Marking before would silently drop a
|
|
58
|
-
// preference on transient API failure (no retry until dependent_root shifts).
|
|
59
|
-
const pending: {submission: SubmittedAtEpoch; slot: Slot}[] = [];
|
|
60
|
-
|
|
61
|
-
for (const epoch of [currentEpoch, currentEpoch + 1]) {
|
|
62
|
-
const dutiesAtEpoch = this.blockDutiesService.getProposersAtEpoch(epoch);
|
|
63
|
-
if (!dutiesAtEpoch) continue;
|
|
64
|
-
|
|
65
|
-
// Reset submission tracking if the dependent root for this epoch has shifted
|
|
66
|
-
// (e.g. due to a reorg). The proposer for a slot may have changed.
|
|
67
|
-
let submission = this.submitted.get(epoch);
|
|
68
|
-
if (submission === undefined || submission.dependentRoot !== dutiesAtEpoch.dependentRoot) {
|
|
69
|
-
if (submission !== undefined) {
|
|
70
|
-
this.logger.info("Proposer-shuffling dependent root shifted; resubmitting builder preferences", {
|
|
71
|
-
epoch,
|
|
72
|
-
priorDependentRoot: submission.dependentRoot,
|
|
73
|
-
dependentRoot: dutiesAtEpoch.dependentRoot,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
submission = {dependentRoot: dutiesAtEpoch.dependentRoot, slots: new Set()};
|
|
77
|
-
this.submitted.set(epoch, submission);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
for (const duty of dutiesAtEpoch.data) {
|
|
81
|
-
if (duty.slot <= slot) continue;
|
|
82
|
-
if (duty.slot > slot + SUBMIT_BEFORE_PROPOSAL_SLOTS) continue;
|
|
83
|
-
if (!isForkPostGloas(this.config.getForkName(duty.slot))) continue;
|
|
84
|
-
if (submission.slots.has(duty.slot)) continue;
|
|
85
|
-
|
|
86
|
-
const pubkeyHex = toPubkeyHex(duty.pubkey);
|
|
87
|
-
const {selection} = this.validatorStore.getBuilderSelectionParams(pubkeyHex, duty.slot);
|
|
88
|
-
if (selection === routes.validator.BuilderSelection.ExecutionOnly) continue;
|
|
89
|
-
|
|
90
|
-
const builderEntries = this.validatorStore.getResolvedBuilderEntries(pubkeyHex);
|
|
91
|
-
if (builderEntries.length === 0) continue;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
// Collect entries per duty and only add them to the batch if signing
|
|
95
|
-
// succeeded for all builders, else the duty is retried on the next tick
|
|
96
|
-
const dutyEntries: routes.validator.BuilderPreferencesEntry[] = [];
|
|
97
|
-
for (const entry of builderEntries) {
|
|
98
|
-
const auth = await this.validatorStore.getBuilderRequestAuth(duty.pubkey, entry.authData, duty.slot, slot);
|
|
99
|
-
dutyEntries.push({
|
|
100
|
-
proposerPubkey: duty.pubkey,
|
|
101
|
-
url: new TextEncoder().encode(entry.url),
|
|
102
|
-
auth,
|
|
103
|
-
maxExecutionPayment: entry.maxExecutionPayment,
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
entries.push(...dutyEntries);
|
|
107
|
-
pending.push({submission, slot: duty.slot});
|
|
108
|
-
} catch (e) {
|
|
109
|
-
this.logger.error(
|
|
110
|
-
"Error signing builder preferences",
|
|
111
|
-
{slot: duty.slot, validatorIndex: duty.validatorIndex},
|
|
112
|
-
e as Error
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Prune tracking for past epochs
|
|
119
|
-
for (const epoch of this.submitted.keys()) {
|
|
120
|
-
if (epoch < currentEpoch) {
|
|
121
|
-
this.submitted.delete(epoch);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (entries.length === 0) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
(await this.api.validator.submitBuilderPreferences({builderPreferences: entries})).assertOk();
|
|
131
|
-
// Only mark as submitted after the API call succeeds; a thrown error, including per-entry
|
|
132
|
-
// failures reported by index, leaves all slots eligible for retry on the next tick.
|
|
133
|
-
// Re-submitting preferences a builder already accepted is harmless.
|
|
134
|
-
for (const {submission, slot: submittedSlot} of pending) {
|
|
135
|
-
submission.slots.add(submittedSlot);
|
|
136
|
-
}
|
|
137
|
-
this.logger.debug("Submitted builder preferences", {count: entries.length});
|
|
138
|
-
} catch (e) {
|
|
139
|
-
this.logger.warn("Error submitting builder preferences", {count: entries.length}, e as Error);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import {ApiClient} from "@lodestar/api";
|
|
2
|
-
import {ChainForkConfig} from "@lodestar/config";
|
|
3
|
-
import {SLOTS_PER_EPOCH, isForkPostGloas} from "@lodestar/params";
|
|
4
|
-
import {IClock, computeEpochAtSlot} from "@lodestar/state-transition";
|
|
5
|
-
import {Epoch, RootHex, Slot, gloas} from "@lodestar/types";
|
|
6
|
-
import {fromHex, toPubkeyHex} from "@lodestar/utils";
|
|
7
|
-
import {Metrics} from "../metrics.js";
|
|
8
|
-
import {LoggerVc} from "../util/index.js";
|
|
9
|
-
import {BlockDutiesService} from "./blockDuties.js";
|
|
10
|
-
import {ValidatorStore} from "./validatorStore.js";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Submit a proposer's `SignedProposerPreferences` this many slots before the proposal slot.
|
|
14
|
-
*
|
|
15
|
-
* Earlier submission means more reorg-triggered resubmits (and gossip flood); later
|
|
16
|
-
* submission risks missing the bid-auction window for this proposal slot. The bid for
|
|
17
|
-
* slot S typically arrives at slot S-1, so we want preferences propagated to the network
|
|
18
|
-
* and consumed by builders before then. SLOTS_PER_EPOCH / 4 (8 slots @ 32 SPE, ~96s @ 12s
|
|
19
|
-
* slots) gives ample margin while bounding redundant resubmits.
|
|
20
|
-
*/
|
|
21
|
-
const SUBMIT_BEFORE_PROPOSAL_SLOTS = Math.floor(SLOTS_PER_EPOCH / 4);
|
|
22
|
-
|
|
23
|
-
/** Per-epoch tracking of preferences already submitted under the current dependent_root. */
|
|
24
|
-
type SubmittedAtEpoch = {dependentRoot: RootHex; slots: Set<Slot>};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Signs and submits `SignedProposerPreferences` for any local validator that will propose
|
|
28
|
-
* within the next `SUBMIT_BEFORE_PROPOSAL_SLOTS`. Re-submits automatically when the proposer
|
|
29
|
-
* dependent root for an epoch shifts (e.g. after a reorg) — detected by comparing the cached
|
|
30
|
-
* `dependentRoot` reported by `BlockDutiesService` against the one we last submitted under.
|
|
31
|
-
*
|
|
32
|
-
* Proposers should broadcast their preferences before the fork so the proposer preference caches
|
|
33
|
-
* of beacon nodes and builders are warm for the first Gloas slots. We start submitting
|
|
34
|
-
* as soon as a duty's proposal slot is in Gloas, which is up to `SUBMIT_BEFORE_PROPOSAL_SLOTS`
|
|
35
|
-
* before the fork, so only the first few Gloas slots are affected by this pre-fork submission.
|
|
36
|
-
*/
|
|
37
|
-
export class ProposerPreferencesService {
|
|
38
|
-
private readonly submitted = new Map<Epoch, SubmittedAtEpoch>();
|
|
39
|
-
private activeScheduledGasLimit: number | undefined;
|
|
40
|
-
|
|
41
|
-
constructor(
|
|
42
|
-
private readonly config: ChainForkConfig,
|
|
43
|
-
private readonly logger: LoggerVc,
|
|
44
|
-
private readonly api: ApiClient,
|
|
45
|
-
clock: IClock,
|
|
46
|
-
private readonly validatorStore: ValidatorStore,
|
|
47
|
-
private readonly blockDutiesService: BlockDutiesService,
|
|
48
|
-
_metrics: Metrics | null
|
|
49
|
-
) {
|
|
50
|
-
clock.runEverySlot(this.runProposerPreferencesTask);
|
|
51
|
-
clock.runEveryEpoch(this.runEveryEpochTask);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
private runProposerPreferencesTask = async (slot: Slot): Promise<void> => {
|
|
55
|
-
// Start running once the submission window (`slot + SUBMIT_BEFORE_PROPOSAL_SLOTS`) reaches
|
|
56
|
-
// Gloas, i.e. already in the epoch before the fork. This allows builders to prepare and
|
|
57
|
-
// submit bids for the first Gloas slots.
|
|
58
|
-
if (!isForkPostGloas(this.config.getForkName(slot + SUBMIT_BEFORE_PROPOSAL_SLOTS))) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const currentEpoch = computeEpochAtSlot(slot);
|
|
63
|
-
const batch: gloas.SignedProposerPreferences[] = [];
|
|
64
|
-
// Track which `(submission, slot)` pairs are pending an API submission so we can mark
|
|
65
|
-
// them only after the network call succeeds. Marking before would silently drop a
|
|
66
|
-
// preference on transient API failure (no retry until dependent_root shifts).
|
|
67
|
-
const pending: {submission: SubmittedAtEpoch; slot: Slot}[] = [];
|
|
68
|
-
|
|
69
|
-
for (const epoch of [currentEpoch, currentEpoch + 1]) {
|
|
70
|
-
const dutiesAtEpoch = this.blockDutiesService.getProposersAtEpoch(epoch);
|
|
71
|
-
if (!dutiesAtEpoch) continue;
|
|
72
|
-
|
|
73
|
-
// Reset submission tracking if the dependent root for this epoch has shifted
|
|
74
|
-
// (e.g. due to a reorg). Any previously-submitted preferences are now stale.
|
|
75
|
-
let submission = this.submitted.get(epoch);
|
|
76
|
-
if (submission === undefined || submission.dependentRoot !== dutiesAtEpoch.dependentRoot) {
|
|
77
|
-
if (submission !== undefined) {
|
|
78
|
-
this.logger.info("Proposer-shuffling dependent root shifted; resubmitting preferences", {
|
|
79
|
-
epoch,
|
|
80
|
-
priorDependentRoot: submission.dependentRoot,
|
|
81
|
-
dependentRoot: dutiesAtEpoch.dependentRoot,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
submission = {dependentRoot: dutiesAtEpoch.dependentRoot, slots: new Set()};
|
|
85
|
-
this.submitted.set(epoch, submission);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const dependentRootBytes = fromHex(dutiesAtEpoch.dependentRoot);
|
|
89
|
-
|
|
90
|
-
for (const duty of dutiesAtEpoch.data) {
|
|
91
|
-
if (duty.slot <= slot) continue;
|
|
92
|
-
if (duty.slot > slot + SUBMIT_BEFORE_PROPOSAL_SLOTS) continue;
|
|
93
|
-
if (!isForkPostGloas(this.config.getForkName(duty.slot))) continue;
|
|
94
|
-
if (submission.slots.has(duty.slot)) continue;
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const pubkeyHex = toPubkeyHex(duty.pubkey);
|
|
98
|
-
const signed = await this.validatorStore.signProposerPreferences(
|
|
99
|
-
duty,
|
|
100
|
-
dependentRootBytes,
|
|
101
|
-
this.validatorStore.getFeeRecipient(pubkeyHex),
|
|
102
|
-
this.validatorStore.getGasLimit(pubkeyHex, duty.slot, this.logger),
|
|
103
|
-
slot
|
|
104
|
-
);
|
|
105
|
-
batch.push(signed);
|
|
106
|
-
pending.push({submission, slot: duty.slot});
|
|
107
|
-
} catch (e) {
|
|
108
|
-
this.logger.error(
|
|
109
|
-
"Error signing proposer preferences",
|
|
110
|
-
{slot: duty.slot, validatorIndex: duty.validatorIndex},
|
|
111
|
-
e as Error
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (batch.length === 0) {
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
await this.api.validator.submitProposerPreferences({signedProposerPreferences: batch});
|
|
123
|
-
// Only mark as submitted after the API call succeeds; a thrown error leaves the
|
|
124
|
-
// slot eligible for retry on the next tick.
|
|
125
|
-
for (const {submission, slot: submittedSlot} of pending) {
|
|
126
|
-
submission.slots.add(submittedSlot);
|
|
127
|
-
}
|
|
128
|
-
this.logger.debug("Submitted signed proposer preferences", {count: batch.length});
|
|
129
|
-
} catch (e) {
|
|
130
|
-
this.logger.error("Error submitting signed proposer preferences", {count: batch.length}, e as Error);
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
private runEveryEpochTask = async (epoch: Epoch): Promise<void> => {
|
|
135
|
-
const scheduledGasLimit = this.config.getScheduledGasLimit(epoch);
|
|
136
|
-
if (scheduledGasLimit !== undefined && scheduledGasLimit !== this.activeScheduledGasLimit) {
|
|
137
|
-
const configuredDefaultGasLimit = this.validatorStore.getConfiguredDefaultGasLimit();
|
|
138
|
-
this.logger.info("Gas limit schedule active", {
|
|
139
|
-
epoch,
|
|
140
|
-
recommendedGasLimit: scheduledGasLimit,
|
|
141
|
-
effectiveDefaultGasLimit: configuredDefaultGasLimit ?? scheduledGasLimit,
|
|
142
|
-
defaultGasLimitSource: configuredDefaultGasLimit === undefined ? "schedule" : "operator",
|
|
143
|
-
});
|
|
144
|
-
this.activeScheduledGasLimit = scheduledGasLimit;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Drop tracking for past epochs; only currentEpoch and currentEpoch + 1 are ever processed.
|
|
148
|
-
for (const trackedEpoch of this.submitted.keys()) {
|
|
149
|
-
if (trackedEpoch < epoch) {
|
|
150
|
-
this.submitted.delete(trackedEpoch);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
}
|