@lodestar/beacon-node 1.48.0-dev.540d803f91 → 1.48.0-dev.566a28844f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/lib/api/impl/validator/index.d.ts.map +1 -1
  2. package/lib/api/impl/validator/index.js +39 -39
  3. package/lib/api/impl/validator/index.js.map +1 -1
  4. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  5. package/lib/chain/blocks/importBlock.js +0 -13
  6. package/lib/chain/blocks/importBlock.js.map +1 -1
  7. package/lib/chain/chain.d.ts +5 -0
  8. package/lib/chain/chain.d.ts.map +1 -1
  9. package/lib/chain/chain.js +67 -38
  10. package/lib/chain/chain.js.map +1 -1
  11. package/lib/chain/emitter.d.ts +1 -1
  12. package/lib/chain/emitter.js +1 -1
  13. package/lib/chain/errors/blockError.d.ts +3 -5
  14. package/lib/chain/errors/blockError.d.ts.map +1 -1
  15. package/lib/chain/errors/blockError.js +2 -2
  16. package/lib/chain/errors/blockError.js.map +1 -1
  17. package/lib/chain/errors/executionPayloadEnvelope.d.ts +0 -11
  18. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  19. package/lib/chain/errors/executionPayloadEnvelope.js +0 -2
  20. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  21. package/lib/chain/prepareNextSlot.js +13 -13
  22. package/lib/chain/prepareNextSlot.js.map +1 -1
  23. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  24. package/lib/chain/produceBlock/produceBlockBody.js +46 -64
  25. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  26. package/lib/chain/validation/block.d.ts.map +1 -1
  27. package/lib/chain/validation/block.js +9 -50
  28. package/lib/chain/validation/block.js.map +1 -1
  29. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  30. package/lib/chain/validation/executionPayloadEnvelope.js +0 -29
  31. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  32. package/package.json +15 -15
  33. package/src/api/impl/validator/index.ts +44 -43
  34. package/src/chain/blocks/importBlock.ts +0 -13
  35. package/src/chain/chain.ts +74 -46
  36. package/src/chain/emitter.ts +1 -1
  37. package/src/chain/errors/blockError.ts +3 -3
  38. package/src/chain/errors/executionPayloadEnvelope.ts +0 -9
  39. package/src/chain/prepareNextSlot.ts +16 -16
  40. package/src/chain/produceBlock/produceBlockBody.ts +50 -71
  41. package/src/chain/validation/block.ts +9 -66
  42. package/src/chain/validation/executionPayloadEnvelope.ts +0 -37
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "bugs": {
12
12
  "url": "https://github.com/ChainSafe/lodestar/issues"
13
13
  },
14
- "version": "1.48.0-dev.540d803f91",
14
+ "version": "1.48.0-dev.566a28844f",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -109,7 +109,7 @@
109
109
  "@chainsafe/persistent-merkle-tree": "^1.3.1",
110
110
  "@chainsafe/prometheus-gc-stats": "^1.0.0",
111
111
  "@chainsafe/snappy-wasm": "^0.5.0",
112
- "@chainsafe/ssz": "^1.6.3",
112
+ "@chainsafe/ssz": "^1.7.0",
113
113
  "@chainsafe/threads": "^1.11.3",
114
114
  "@crate-crypto/node-eth-kzg": "0.9.1",
115
115
  "@fastify/bearer-auth": "^10.0.1",
@@ -126,17 +126,17 @@
126
126
  "@libp2p/peer-id": "^6.0.15",
127
127
  "@libp2p/prometheus-metrics": "^5.0.28",
128
128
  "@libp2p/tcp": "^11.0.27",
129
- "@lodestar/api": "^1.48.0-dev.540d803f91",
130
- "@lodestar/config": "^1.48.0-dev.540d803f91",
131
- "@lodestar/db": "^1.48.0-dev.540d803f91",
132
- "@lodestar/fork-choice": "^1.48.0-dev.540d803f91",
133
- "@lodestar/logger": "^1.48.0-dev.540d803f91",
134
- "@lodestar/params": "^1.48.0-dev.540d803f91",
135
- "@lodestar/reqresp": "^1.48.0-dev.540d803f91",
136
- "@lodestar/state-transition": "^1.48.0-dev.540d803f91",
137
- "@lodestar/types": "^1.48.0-dev.540d803f91",
138
- "@lodestar/utils": "^1.48.0-dev.540d803f91",
139
- "@lodestar/validator": "^1.48.0-dev.540d803f91",
129
+ "@lodestar/api": "^1.48.0-dev.566a28844f",
130
+ "@lodestar/config": "^1.48.0-dev.566a28844f",
131
+ "@lodestar/db": "^1.48.0-dev.566a28844f",
132
+ "@lodestar/fork-choice": "^1.48.0-dev.566a28844f",
133
+ "@lodestar/logger": "^1.48.0-dev.566a28844f",
134
+ "@lodestar/params": "^1.48.0-dev.566a28844f",
135
+ "@lodestar/reqresp": "^1.48.0-dev.566a28844f",
136
+ "@lodestar/state-transition": "^1.48.0-dev.566a28844f",
137
+ "@lodestar/types": "^1.48.0-dev.566a28844f",
138
+ "@lodestar/utils": "^1.48.0-dev.566a28844f",
139
+ "@lodestar/validator": "^1.48.0-dev.566a28844f",
140
140
  "@multiformats/multiaddr": "^13.0.1",
141
141
  "datastore-core": "^12.0.1",
142
142
  "datastore-level": "^13.0.1",
@@ -158,7 +158,7 @@
158
158
  "@libp2p/interface-internal": "^3.1.12",
159
159
  "@libp2p/logger": "^6.2.13",
160
160
  "@libp2p/utils": "^7.4.0",
161
- "@lodestar/spec-test-util": "^1.48.0-dev.540d803f91",
161
+ "@lodestar/spec-test-util": "^1.48.0-dev.566a28844f",
162
162
  "@types/js-yaml": "^4.0.5",
163
163
  "@types/qs": "^6.9.7",
164
164
  "@types/tmp": "^0.2.3",
@@ -175,5 +175,5 @@
175
175
  "beacon",
176
176
  "blockchain"
177
177
  ],
178
- "gitHead": "eed55ce6324307520467e8ac73b52a7e2941e99a"
178
+ "gitHead": "08c7d0fcfa372d47cf32431e16aa854793a2c124"
179
179
  }
@@ -133,40 +133,37 @@ type BidCandidate = {
133
133
  };
134
134
 
135
135
  /**
136
- * Return the best bid by boosted total payment. A candidate with the max boost factor is
137
- * preferred over any other regardless of value, ties prefer a builder api bid over the
138
- * p2p bid, and the earlier received bid between builder api bids.
136
+ * Ranking value of a bid, the boosted total payment scaled by 100 as `boostFactor` is a percentage.
137
+ * Kept scaled so bids that differ by less than a percent are not truncated into a tie.
139
138
  */
140
- function selectBestBid(candidates: BidCandidate[]): BidCandidate | null {
141
- const boostedValue = ({totalGwei, boostFactor}: BidCandidate): bigint => boostFactor * totalGwei;
142
- let best: BidCandidate | null = null;
143
- for (const candidate of candidates) {
144
- if (best === null) {
145
- best = candidate;
146
- continue;
147
- }
148
- // Preserve max boost preference before comparing bid values
149
- const candidateIsMaxBoost = candidate.boostFactor === MAX_BUILDER_BOOST_FACTOR;
150
- const bestIsMaxBoost = best.boostFactor === MAX_BUILDER_BOOST_FACTOR;
151
- if (candidateIsMaxBoost !== bestIsMaxBoost) {
152
- if (candidateIsMaxBoost) {
153
- best = candidate;
154
- }
155
- continue;
156
- }
157
- const candidateValue = boostedValue(candidate);
158
- const bestValue = boostedValue(best);
159
- if (candidateValue > bestValue) {
160
- best = candidate;
161
- } else if (
162
- candidateValue === bestValue &&
163
- // A tie prefers a builder api bid over the p2p bid, and the earlier received bid otherwise
164
- (best.url === undefined || (candidate.url !== undefined && candidate.receivedMs < best.receivedMs))
165
- ) {
166
- best = candidate;
167
- }
139
+ function getBoostedTotalScaled({totalGwei, boostFactor}: BidCandidate): bigint {
140
+ return boostFactor * totalGwei;
141
+ }
142
+
143
+ /** Order bid candidates by preference, best first, used to select a bid and to log the ranking */
144
+ function compareBidCandidates(a: BidCandidate, b: BidCandidate): number {
145
+ // Preserve max boost preference before comparing bid values
146
+ const aIsMaxBoost = a.boostFactor === MAX_BUILDER_BOOST_FACTOR;
147
+ const bIsMaxBoost = b.boostFactor === MAX_BUILDER_BOOST_FACTOR;
148
+ if (aIsMaxBoost !== bIsMaxBoost) {
149
+ return aIsMaxBoost ? -1 : 1;
168
150
  }
169
- return best;
151
+
152
+ const aBoostedTotal = getBoostedTotalScaled(a);
153
+ const bBoostedTotal = getBoostedTotalScaled(b);
154
+ if (aBoostedTotal !== bBoostedTotal) {
155
+ return aBoostedTotal > bBoostedTotal ? -1 : 1;
156
+ }
157
+
158
+ // A tie prefers a builder api bid over the p2p bid
159
+ const aIsBuilderApi = a.url !== undefined;
160
+ const bIsBuilderApi = b.url !== undefined;
161
+ if (aIsBuilderApi !== bIsBuilderApi) {
162
+ return aIsBuilderApi ? -1 : 1;
163
+ }
164
+
165
+ // and the earlier received bid between builder api bids
166
+ return a.receivedMs - b.receivedMs;
170
167
  }
171
168
 
172
169
  type ProduceBlockContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Wei} & {
@@ -1049,20 +1046,24 @@ export function getValidatorApi(
1049
1046
  });
1050
1047
  }
1051
1048
 
1052
- const best = selectBestBid(candidates);
1053
- if (candidates.length > 0) {
1054
- logger.debug("Ranked builder bid candidates", {
1049
+ if (candidates.length === 0) {
1050
+ return null;
1051
+ }
1052
+
1053
+ const rankedCandidates = candidates.toSorted(compareBidCandidates);
1054
+ for (const [index, candidate] of rankedCandidates.entries()) {
1055
+ logger.debug("Builder bid candidate", {
1055
1056
  slot,
1056
- candidates: candidates
1057
- .map(
1058
- (candidate) =>
1059
- `${candidate.url ?? "p2p"}:total=${prettyGweiToEth(candidate.totalGwei)}:boost=${candidate.boostFactor}:received=${candidate.receivedMs}ms`
1060
- )
1061
- .join(","),
1062
- bidSource: best?.url ?? "p2p",
1057
+ rank: index + 1,
1058
+ source: candidate.url !== undefined ? toPrintableUrl(candidate.url) : "p2p",
1059
+ builder: candidate.signedBid.message.builderIndex,
1060
+ total: prettyGweiToEth(candidate.totalGwei),
1061
+ boost: candidate.boostFactor,
1062
+ boosted: prettyGweiToEth(getBoostedTotalScaled(candidate) / 100n),
1063
+ receivedMs: candidate.receivedMs,
1063
1064
  });
1064
1065
  }
1065
- return best;
1066
+ return rankedCandidates[0];
1066
1067
  })();
1067
1068
 
1068
1069
  const commonBlockBodyPromise = chain.produceCommonBlockBody({
@@ -524,19 +524,6 @@ export async function importBlock(
524
524
  this.metrics?.previousJustifiedEpoch.set(checkpointState.previousJustifiedCheckpoint.epoch);
525
525
  this.metrics?.currentJustifiedEpoch.set(justifiedCheckpoint.epoch);
526
526
  }
527
- const finalizedCheckpoint = checkpointState.finalizedCheckpoint;
528
- const finalizedEpoch = finalizedCheckpoint.epoch;
529
- const preFinalizedEpoch = parentBlockSummary.finalizedEpoch;
530
- if (finalizedEpoch > preFinalizedEpoch) {
531
- this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
532
- block: toRootHex(finalizedCheckpoint.root),
533
- epoch: finalizedCheckpoint.epoch,
534
- state: toRootHex(checkpointState.hashTreeRoot()),
535
- executionOptimistic: false,
536
- });
537
- this.logger.verbose("Checkpoint finalized", toCheckpointHex(finalizedCheckpoint));
538
- this.metrics?.finalizedEpoch.set(finalizedCheckpoint.epoch);
539
- }
540
527
  }
541
528
  }
542
529
 
@@ -1271,59 +1271,65 @@ export class BeaconChain implements IBeaconChain {
1271
1271
  try {
1272
1272
  const prevHead = this.forkChoice.getHead();
1273
1273
  const head = this.forkChoice.updateAndGetHead({mode: UpdateHeadOpt.GetCanonicalHead}).head;
1274
+ this.emitHeadEvents(prevHead, head);
1275
+ return head;
1276
+ } catch (e) {
1277
+ this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1278
+ throw e;
1279
+ } finally {
1280
+ timer?.();
1281
+ }
1282
+ }
1274
1283
 
1275
- const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1276
-
1277
- if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1278
- return head;
1279
- }
1284
+ /**
1285
+ * Emit `head` and `head_v2` when the fork choice head differs between two observations. Every caller that
1286
+ * can move the head, `updateAndGetHead()` and `updateTime()`, must capture the head before and pass both.
1287
+ */
1288
+ private emitHeadEvents(prevHead: ProtoBlock, head: ProtoBlock): void {
1289
+ const headRootChanged = head.blockRoot !== prevHead.blockRoot;
1280
1290
 
1281
- try {
1282
- const previousDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.previous);
1283
- const currentDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.current);
1284
- const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
1285
- const executionOptimistic = isOptimisticBlock(head);
1286
-
1287
- if (headRootChanged) {
1288
- this.emitter.emit(routes.events.EventType.head, {
1289
- block: head.blockRoot,
1290
- epochTransition,
1291
- slot: head.slot,
1292
- state: head.stateRoot,
1293
- previousDutyDependentRoot,
1294
- currentDutyDependentRoot,
1295
- executionOptimistic,
1296
- });
1297
- }
1291
+ if (!headRootChanged && prevHead.payloadStatus === head.payloadStatus) {
1292
+ return;
1293
+ }
1298
1294
 
1299
- this.emitter.emit(routes.events.EventType.headV2, {
1300
- version: this.config.getForkName(head.slot),
1301
- data: {
1302
- slot: head.slot,
1303
- block: head.blockRoot,
1304
- state: head.stateRoot,
1305
- payloadStatus: head.payloadStatus === PayloadStatus.FULL ? "full" : "empty",
1306
- epochTransition,
1307
- currentEpochDependentRoot: previousDutyDependentRoot,
1308
- nextEpochDependentRoot: currentDutyDependentRoot,
1309
- executionOptimistic,
1310
- },
1295
+ try {
1296
+ const previousDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.previous);
1297
+ const currentDutyDependentRoot = this.forkChoice.getDependentRoot(head, EpochDifference.current);
1298
+ const epochTransition = computeStartSlotAtEpoch(computeEpochAtSlot(head.slot)) === head.slot;
1299
+ const executionOptimistic = isOptimisticBlock(head);
1300
+
1301
+ if (headRootChanged) {
1302
+ this.emitter.emit(routes.events.EventType.head, {
1303
+ block: head.blockRoot,
1304
+ epochTransition,
1305
+ slot: head.slot,
1306
+ state: head.stateRoot,
1307
+ previousDutyDependentRoot,
1308
+ currentDutyDependentRoot,
1309
+ executionOptimistic,
1311
1310
  });
1312
- } catch (e) {
1313
- // getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
1314
- this.logger.debug(
1315
- "Error emitting head/head_v2 event",
1316
- {slot: head.slot, root: head.blockRoot, headRootChanged},
1317
- e as Error
1318
- );
1319
1311
  }
1320
1312
 
1321
- return head;
1313
+ this.emitter.emit(routes.events.EventType.headV2, {
1314
+ version: this.config.getForkName(head.slot),
1315
+ data: {
1316
+ slot: head.slot,
1317
+ block: head.blockRoot,
1318
+ state: head.stateRoot,
1319
+ payloadStatus: head.payloadStatus === PayloadStatus.FULL ? "full" : "empty",
1320
+ epochTransition,
1321
+ currentEpochDependentRoot: previousDutyDependentRoot,
1322
+ nextEpochDependentRoot: currentDutyDependentRoot,
1323
+ executionOptimistic,
1324
+ },
1325
+ });
1322
1326
  } catch (e) {
1323
- this.metrics?.forkChoice.errors.inc({entrypoint: UpdateHeadOpt.GetCanonicalHead});
1324
- throw e;
1325
- } finally {
1326
- timer?.();
1327
+ // getDependentRoot() may fail with error: "No block for root" as we can see in holesky non-finality issue
1328
+ this.logger.debug(
1329
+ "Error emitting head/head_v2 event",
1330
+ {slot: head.slot, root: head.blockRoot, headRootChanged},
1331
+ e as Error
1332
+ );
1327
1333
  }
1328
1334
  }
1329
1335
 
@@ -1612,7 +1618,10 @@ export class BeaconChain implements IBeaconChain {
1612
1618
  this.processShutdownCallback(this.forkChoice.irrecoverableError);
1613
1619
  }
1614
1620
 
1621
+ // updateTime() recomputes the head on epoch-boundary checkpoint pull-up and under fast confirmation
1622
+ const prevHead = this.forkChoice.getHead();
1615
1623
  this.forkChoice.updateTime(slot);
1624
+ this.emitHeadEvents(prevHead, this.forkChoice.getHead());
1616
1625
  this.metrics?.clockSlot.set(slot);
1617
1626
 
1618
1627
  this.attestationPool.prune(slot);
@@ -1670,6 +1679,25 @@ export class BeaconChain implements IBeaconChain {
1670
1679
 
1671
1680
  private async onForkChoiceFinalized(this: BeaconChain, cp: CheckpointWithHex): Promise<void> {
1672
1681
  this.logger.verbose("Fork choice finalized", {epoch: cp.epoch, root: cp.rootHex});
1682
+ this.metrics?.finalizedEpoch.set(cp.epoch);
1683
+ const finalizedBlock = this.forkChoice.getBlockHexDefaultStatus(cp.rootHex);
1684
+ if (finalizedBlock) {
1685
+ // The callback runs synchronously inside fork choice checkpoint updates, defer writing to subscribers
1686
+ callInNextEventLoop(() => {
1687
+ this.emitter.emit(routes.events.EventType.finalizedCheckpoint, {
1688
+ block: cp.rootHex,
1689
+ epoch: cp.epoch,
1690
+ state: finalizedBlock.stateRoot,
1691
+ executionOptimistic: isOptimisticBlock(finalizedBlock),
1692
+ });
1693
+ });
1694
+ } else {
1695
+ this.logger.debug("Finalized block not found in fork choice, skip finalized_checkpoint event", {
1696
+ epoch: cp.epoch,
1697
+ root: cp.rootHex,
1698
+ });
1699
+ }
1700
+
1673
1701
  const finalizedSlot = computeStartSlotAtEpoch(cp.epoch);
1674
1702
  this.seenBlockProposers.prune(finalizedSlot);
1675
1703
 
@@ -33,7 +33,7 @@ export enum ChainEvent {
33
33
  /**
34
34
  * This event signals that the fork choice store has been updated.
35
35
  *
36
- * This event is guaranteed to be triggered whenever the fork choice justified checkpoint is updated. This is in response to a newly processed block.
36
+ * This event is guaranteed to be triggered whenever the fork choice finalized checkpoint is updated. This is either in response to a newly processed block or a new clock tick.
37
37
  */
38
38
  forkChoiceFinalized = "forkChoice:finalized",
39
39
  /**
@@ -78,8 +78,8 @@ export enum BlockErrorCode {
78
78
  TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS",
79
79
  /** Bid parent block root does not match block parent root */
80
80
  BID_PARENT_ROOT_MISMATCH = "BLOCK_ERROR_BID_PARENT_ROOT_MISMATCH",
81
- /** A block body operation or parent execution request list exceeds its per-block limit */
82
- TOO_MANY_BLOCK_OPERATIONS = "BLOCK_ERROR_TOO_MANY_BLOCK_OPERATIONS",
81
+ /** A block contains deposits where none are allowed */
82
+ NON_ZERO_DEPOSITS = "BLOCK_ERROR_NON_ZERO_DEPOSITS",
83
83
  /** The parent block's execution payload has been verified as invalid */
84
84
  PARENT_EXECUTION_INVALID = "BLOCK_ERROR_PARENT_EXECUTION_INVALID",
85
85
  /**
@@ -153,7 +153,7 @@ export type BlockErrorType =
153
153
  | {code: BlockErrorCode.DATA_UNAVAILABLE}
154
154
  | {code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS; blobKzgCommitmentsLen: number; commitmentLimit: number}
155
155
  | {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex}
156
- | {code: BlockErrorCode.TOO_MANY_BLOCK_OPERATIONS; name: string; count: number; limit: number}
156
+ | {code: BlockErrorCode.NON_ZERO_DEPOSITS; count: number}
157
157
  | {code: BlockErrorCode.PARENT_EXECUTION_INVALID; parentRoot: RootHex}
158
158
  | {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentRoot: RootHex; parentBlockHash: RootHex}
159
159
  | {code: BlockErrorCode.NON_LINEAR_PAYLOAD_ROOTS; parentBlockHash: RootHex; expectedBlockHash: RootHex};
@@ -12,8 +12,6 @@ export enum ExecutionPayloadEnvelopeErrorCode {
12
12
  BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH",
13
13
  BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH",
14
14
  EXECUTION_REQUESTS_ROOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_ROOT_MISMATCH",
15
- EXECUTION_REQUESTS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_COUNT_EXCEEDED",
16
- WITHDRAWALS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_WITHDRAWALS_COUNT_EXCEEDED",
17
15
  INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE",
18
16
  PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING",
19
17
  }
@@ -44,13 +42,6 @@ export type ExecutionPayloadEnvelopeErrorType =
44
42
  envelopeRequestsRoot: RootHex;
45
43
  bidRequestsRoot: RootHex;
46
44
  }
47
- | {
48
- code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED;
49
- name: string;
50
- count: number;
51
- limit: number;
52
- }
53
- | {code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED; count: number; limit: number}
54
45
  | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE}
55
46
  | {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex};
56
47
 
@@ -243,6 +243,22 @@ export class PrepareNextSlotScheduler {
243
243
  parentBlockHash = preparedState.latestExecutionPayloadHeader.blockHash;
244
244
  }
245
245
 
246
+ // If emitPayloadAttributes is true emit a SSE payloadAttributes event for
247
+ // every slot. Without the flag, only emit the event if we are proposing in the next slot.
248
+ if (
249
+ (feeRecipient || this.chain.opts.emitPayloadAttributes === true) &&
250
+ this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
251
+ ) {
252
+ const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
253
+ prepareState: stateAfterParentPayload,
254
+ prepareSlot,
255
+ parentBlockRoot: fromHex(updatedHead.blockRoot),
256
+ parentBlockHash,
257
+ feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
258
+ });
259
+ this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
260
+ }
261
+
246
262
  if (feeRecipient) {
247
263
  const preparationTime =
248
264
  computeTimeAtSlot(this.config, prepareSlot, this.chain.genesisTime) - Date.now() / 1000;
@@ -273,22 +289,6 @@ export class PrepareNextSlotScheduler {
273
289
  }
274
290
 
275
291
  this.computeStateHashTreeRoot(preparedState, isEpochTransition);
276
-
277
- // If emitPayloadAttributes is true emit a SSE payloadAttributes event for
278
- // every slot. Without the flag, only emit the event if we are proposing in the next slot.
279
- if (
280
- (feeRecipient || this.chain.opts.emitPayloadAttributes === true) &&
281
- this.chain.emitter.listenerCount(routes.events.EventType.payloadAttributes)
282
- ) {
283
- const data = getPayloadAttributesForSSE(fork as ForkPostBellatrix, this.chain, {
284
- prepareState: stateAfterParentPayload,
285
- prepareSlot,
286
- parentBlockRoot: fromHex(updatedHead.blockRoot),
287
- parentBlockHash,
288
- feeRecipient: feeRecipient ?? "0x0000000000000000000000000000000000000000",
289
- });
290
- this.chain.emitter.emit(routes.events.EventType.payloadAttributes, {data, version: fork});
291
- }
292
292
  } else {
293
293
  // Pre-bellatrix only reaches here at an epoch transition to precompute the next epoch state
294
294
  const preparedState = await this.chain.regen.getBlockSlotState(
@@ -528,23 +528,18 @@ export async function produceBlockBody<T extends BlockType>(
528
528
  }
529
529
  }
530
530
 
531
- if (ForkSeq[fork] >= ForkSeq.deneb) {
532
- const {blobKzgCommitments} = builderRes;
533
- if (blobKzgCommitments === undefined) {
534
- throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
535
- }
536
-
537
- (blockBody as deneb.BlindedBeaconBlockBody).blobKzgCommitments = blobKzgCommitments;
538
- Object.assign(logMeta, {blobs: blobKzgCommitments.length});
531
+ const {blobKzgCommitments, executionRequests} = builderRes;
532
+ if (blobKzgCommitments === undefined) {
533
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing blobKzgCommitments`);
539
534
  }
540
535
 
541
- if (ForkSeq[fork] >= ForkSeq.electra) {
542
- const {executionRequests} = builderRes;
543
- if (executionRequests === undefined) {
544
- throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
545
- }
546
- (blockBody as electra.BlindedBeaconBlockBody).executionRequests = executionRequests;
536
+ (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).blobKzgCommitments = blobKzgCommitments;
537
+ Object.assign(logMeta, {blobs: blobKzgCommitments.length});
538
+
539
+ if (executionRequests === undefined) {
540
+ throw Error(`Invalid builder getHeader response for fork=${fork}, missing executionRequests`);
547
541
  }
542
+ (blockBody as BlindedBeaconBlockBody<ForkName.fulu>).executionRequests = executionRequests;
548
543
  }
549
544
 
550
545
  // blockType === BlockType.Full
@@ -899,45 +894,38 @@ function preparePayloadAttributes(
899
894
  ): SSEPayloadAttributes["payloadAttributes"] {
900
895
  const timestamp = computeTimeAtSlot(chain.config, prepareSlot, prepareState.genesisTime);
901
896
  const prevRandao = prepareState.getRandaoMix(prepareState.epoch);
902
- const payloadAttributes = {
903
- timestamp,
904
- prevRandao,
905
- suggestedFeeRecipient: feeRecipient,
906
- };
907
-
908
- if (ForkSeq[fork] >= ForkSeq.capella) {
909
- if (!isStatePostCapella(prepareState)) {
910
- throw new Error("Expected Capella state for withdrawals");
911
- }
897
+ if (!isStatePostCapella(prepareState)) {
898
+ throw new Error("Expected Capella state for withdrawals");
899
+ }
912
900
 
913
- if (isStatePostGloas(prepareState)) {
914
- const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
915
- if (isExtendingPayload) {
916
- // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch
917
- // here means the caller did not apply parent payload to prepareState
918
- if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) {
919
- throw new Error("Expected state with parent execution payload applied for withdrawals");
920
- }
921
- (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
922
- prepareState.getExpectedWithdrawals().expectedWithdrawals;
923
- } else {
924
- // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
925
- // already deducted from CL balances but never credited on the EL (the envelope
926
- // was not delivered). The next payload must carry those same withdrawals to
927
- // restore CL/EL consistency, otherwise validators permanently lose that balance.
928
- (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
929
- prepareState.payloadExpectedWithdrawals;
901
+ let withdrawals: capella.Withdrawal[];
902
+ if (isStatePostGloas(prepareState)) {
903
+ const isExtendingPayload = byteArrayEquals(parentBlockHash, prepareState.latestExecutionPayloadBid.blockHash);
904
+ if (isExtendingPayload) {
905
+ // applyParentExecutionPayload sets latestBlockHash = parentBid.blockHash, so a mismatch
906
+ // here means the caller did not apply parent payload to prepareState
907
+ if (!byteArrayEquals(prepareState.latestBlockHash, prepareState.latestExecutionPayloadBid.blockHash)) {
908
+ throw new Error("Expected state with parent execution payload applied for withdrawals");
930
909
  }
910
+ withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
931
911
  } else {
932
- // withdrawals logic is now fork aware as it changes on electra fork post capella
933
- (payloadAttributes as capella.SSEPayloadAttributes["payloadAttributes"]).withdrawals =
934
- prepareState.getExpectedWithdrawals().expectedWithdrawals;
912
+ // When the parent block is empty, state.payloadExpectedWithdrawals holds a batch
913
+ // already deducted from CL balances but never credited on the EL (the envelope
914
+ // was not delivered). The next payload must carry those same withdrawals to
915
+ // restore CL/EL consistency, otherwise validators permanently lose that balance.
916
+ withdrawals = prepareState.payloadExpectedWithdrawals;
935
917
  }
918
+ } else {
919
+ withdrawals = prepareState.getExpectedWithdrawals().expectedWithdrawals;
936
920
  }
937
921
 
938
- if (ForkSeq[fork] >= ForkSeq.deneb) {
939
- (payloadAttributes as deneb.SSEPayloadAttributes["payloadAttributes"]).parentBeaconBlockRoot = parentBlockRoot;
940
- }
922
+ const payloadAttributes: deneb.SSEPayloadAttributes["payloadAttributes"] = {
923
+ timestamp,
924
+ prevRandao,
925
+ suggestedFeeRecipient: feeRecipient,
926
+ withdrawals,
927
+ parentBeaconBlockRoot: parentBlockRoot,
928
+ };
941
929
 
942
930
  if (ForkSeq[fork] >= ForkSeq.gloas) {
943
931
  if (!isStatePostGloas(prepareState)) {
@@ -1050,37 +1038,28 @@ export async function produceCommonBlockBody<T extends BlockType>(
1050
1038
  step: BlockProductionStep.attestations,
1051
1039
  });
1052
1040
 
1053
- const blockBody: Omit<CommonBlockBody, "blsToExecutionChanges" | "syncAggregate"> = {
1041
+ const endSyncAggregate = stepsMetrics?.startTimer();
1042
+ const parentBlockRoot = fromHex(parentBlock.blockRoot);
1043
+ const previousSlot = slot - 1;
1044
+ const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot);
1045
+ this.metrics?.production.producedSyncAggregateParticipants.observe(
1046
+ syncAggregate.syncCommitteeBits.getTrueBitIndexes().length
1047
+ );
1048
+ endSyncAggregate?.({
1049
+ step: BlockProductionStep.syncAggregate,
1050
+ });
1051
+
1052
+ // Live proposal production is supported from Fulu onward.
1053
+ return {
1054
1054
  randaoReveal,
1055
1055
  graffiti,
1056
- // Eth1 data voting is no longer required since electra
1057
1056
  eth1Data: currentState.eth1Data,
1058
1057
  proposerSlashings: this.opts.disableProposerSlashings === true ? [] : proposerSlashings,
1059
1058
  attesterSlashings,
1060
1059
  attestations,
1061
- // Since electra, deposits are processed by the execution layer,
1062
- // we no longer support handling deposits from earlier forks.
1063
1060
  deposits: [],
1064
1061
  voluntaryExits,
1062
+ blsToExecutionChanges,
1063
+ syncAggregate,
1065
1064
  };
1066
-
1067
- if (ForkSeq[fork] >= ForkSeq.capella) {
1068
- (blockBody as CommonBlockBody).blsToExecutionChanges = blsToExecutionChanges;
1069
- }
1070
-
1071
- const endSyncAggregate = stepsMetrics?.startTimer();
1072
- if (ForkSeq[fork] >= ForkSeq.altair) {
1073
- const parentBlockRoot = fromHex(parentBlock.blockRoot);
1074
- const previousSlot = slot - 1;
1075
- const syncAggregate = this.syncContributionAndProofPool.getAggregate(previousSlot, parentBlockRoot);
1076
- this.metrics?.production.producedSyncAggregateParticipants.observe(
1077
- syncAggregate.syncCommitteeBits.getTrueBitIndexes().length
1078
- );
1079
- (blockBody as CommonBlockBody).syncAggregate = syncAggregate;
1080
- }
1081
- endSyncAggregate?.({
1082
- step: BlockProductionStep.syncAggregate,
1083
- });
1084
-
1085
- return blockBody as CommonBlockBody;
1086
1065
  }