-
-
Notifications
You must be signed in to change notification settings - Fork 277
/
block.ts
185 lines (163 loc) Β· 7.39 KB
/
block.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import {BLSPubkey, Slot, BLSSignature, allForks, bellatrix} from "@lodestar/types";
import {IChainForkConfig} from "@lodestar/config";
import {ForkName} from "@lodestar/params";
import {extendError, prettyBytes} from "@lodestar/utils";
import {toHexString} from "@chainsafe/ssz";
import {Api} from "@lodestar/api";
import {IClock, ILoggerVc} from "../util/index.js";
import {PubkeyHex} from "../types.js";
import {Metrics} from "../metrics.js";
import {ValidatorStore} from "./validatorStore.js";
import {BlockDutiesService, GENESIS_SLOT} from "./blockDuties.js";
type BlockProposingServiceOpts = {
graffiti?: string;
strictFeeRecipientCheck?: boolean;
builder: {enabled?: boolean};
};
/**
* Service that sets up and handles validator block proposal duties.
*/
export class BlockProposingService {
private readonly dutiesService: BlockDutiesService;
constructor(
private readonly config: IChainForkConfig,
private readonly logger: ILoggerVc,
private readonly api: Api,
private readonly clock: IClock,
private readonly validatorStore: ValidatorStore,
private readonly metrics: Metrics | null,
private readonly opts: BlockProposingServiceOpts
) {
this.dutiesService = new BlockDutiesService(
logger,
api,
clock,
validatorStore,
metrics,
this.notifyBlockProductionFn
);
}
removeDutiesForKey(pubkey: PubkeyHex): void {
this.dutiesService.removeDutiesForKey(pubkey);
}
/**
* `BlockDutiesService` must call this fn to trigger block creation
* This function may run more than once at a time, rationale in `BlockDutiesService.pollBeaconProposers`
*/
private notifyBlockProductionFn = (slot: Slot, proposers: BLSPubkey[]): void => {
if (slot <= GENESIS_SLOT) {
this.logger.debug("Not producing block before or at genesis slot");
return;
}
if (proposers.length > 1) {
this.logger.warn("Multiple block proposers", {slot, count: proposers.length});
}
Promise.all(proposers.map((pubkey) => this.createAndPublishBlock(pubkey, slot))).catch((e: Error) => {
this.logger.error("Error on block duties", {slot}, e);
});
};
/** Produce a block at the given slot for pubkey */
private async createAndPublishBlock(pubkey: BLSPubkey, slot: Slot): Promise<void> {
const pubkeyHex = toHexString(pubkey);
const logCtx = {slot, validator: prettyBytes(pubkeyHex)};
// Wrap with try catch here to re-use `logCtx`
try {
const randaoReveal = await this.validatorStore.signRandao(pubkey, slot);
const graffiti = this.opts.graffiti || "";
const debugLogCtx = {...logCtx, validator: pubkeyHex};
this.logger.debug("Producing block", debugLogCtx);
this.metrics?.proposerStepCallProduceBlock.observe(this.clock.secFromSlot(slot));
const expectedFeeRecipient = this.validatorStore.getFeeRecipient(pubkeyHex);
const block = await this.produceBlockWrapper(slot, randaoReveal, graffiti, expectedFeeRecipient).catch(
(e: Error) => {
this.metrics?.blockProposingErrors.inc({error: "produce"});
throw extendError(e, "Failed to produce block");
}
);
this.logger.debug("Produced block", {...debugLogCtx, ...block.debugLogCtx});
this.metrics?.blocksProduced.inc();
const signedBlock = await this.validatorStore.signBlock(pubkey, block.data, slot);
this.metrics?.proposerStepCallPublishBlock.observe(this.clock.secFromSlot(slot));
await this.publishBlockWrapper(signedBlock).catch((e: Error) => {
this.metrics?.blockProposingErrors.inc({error: "publish"});
throw extendError(e, "Failed to publish block");
});
this.logger.info("Published block", {...logCtx, graffiti, ...block.debugLogCtx});
this.metrics?.blocksPublished.inc();
} catch (e) {
this.logger.error("Error proposing block", logCtx, e as Error);
}
}
private publishBlockWrapper = async (signedBlock: allForks.FullOrBlindedSignedBeaconBlock): Promise<void> => {
return (signedBlock.message.body as bellatrix.BlindedBeaconBlockBody).executionPayloadHeader !== undefined
? this.api.beacon.publishBlindedBlock(signedBlock as bellatrix.SignedBlindedBeaconBlock)
: this.api.beacon.publishBlock(signedBlock as allForks.SignedBeaconBlock);
};
private produceBlockWrapper = async (
slot: Slot,
randaoReveal: BLSSignature,
graffiti: string,
expectedFeeRecipient: string
): Promise<{data: allForks.FullOrBlindedBeaconBlock} & {debugLogCtx: Record<string, string>}> => {
const blindedBlockPromise = this.opts.builder.enabled
? this.api.validator.produceBlindedBlock(slot, randaoReveal, graffiti).catch((e: Error) => {
this.logger.error("Failed to produce builder block", {}, e as Error);
return null;
})
: null;
const fullBlockPromise = this.produceBlock(slot, randaoReveal, graffiti).catch((e: Error) => {
this.logger.error("Failed to produce builder block", {}, e as Error);
return null;
});
await Promise.all([blindedBlockPromise, fullBlockPromise]);
const blindedBlock = await blindedBlockPromise;
const fullBlock = await fullBlockPromise;
// A metric on the choice between blindedBlock and normal block can be applied
if (blindedBlock) {
const debugLogCtx = {source: "builder"};
return {...blindedBlock, debugLogCtx};
} else {
const debugLogCtx = {source: "engine"};
if (!fullBlock) {
throw Error("Failed to produce engine or builder block");
}
const blockFeeRecipient = (fullBlock.data as bellatrix.BeaconBlock).body.executionPayload?.feeRecipient;
const feeRecipient = blockFeeRecipient !== undefined ? toHexString(blockFeeRecipient) : undefined;
if (feeRecipient !== undefined) {
// In Mev Builder, the feeRecipient could differ and rewards to the feeRecipeint
// might be included in the block transactions as indicated by the BuilderBid
// Address this appropriately in the Mev boost PR
//
// Even for engine, there could be divergence of feeRecipient the argument being
// that the bn <> engine setup has implied trust and are user-agents of the same entity.
// A better approach would be to have engine also provide something akin to BuilderBid
//
// The following conversation in the interop R&D channel can provide some context
// https://discord.com/channels/595666850260713488/892088344438255616/978374892678426695
//
// For now providing a strick check flag to enable disable this
if (feeRecipient !== expectedFeeRecipient && this.opts.strictFeeRecipientCheck) {
throw Error(`Invalid feeRecipient=${feeRecipient}, expected=${expectedFeeRecipient}`);
}
Object.assign(debugLogCtx, {feeRecipient});
}
return {...fullBlock, debugLogCtx};
// throw Error("random")
}
};
/** Wrapper around the API's different methods for producing blocks across forks */
private produceBlock: Api["validator"]["produceBlock"] = async (
slot,
randaoReveal,
graffiti
): Promise<{data: allForks.BeaconBlock}> => {
switch (this.config.getForkName(slot)) {
case ForkName.phase0:
return this.api.validator.produceBlock(slot, randaoReveal, graffiti);
// All subsequent forks are expected to use v2 too
case ForkName.altair:
default:
return this.api.validator.produceBlockV2(slot, randaoReveal, graffiti);
}
};
}