@lodestar/beacon-node 1.47.0-dev.9572bcc698 → 1.47.0-dev.9ba9a5ce85

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 (148) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +41 -10
  3. package/lib/api/impl/beacon/blocks/index.js.map +1 -1
  4. package/lib/api/impl/lodestar/index.js +1 -1
  5. package/lib/api/impl/lodestar/index.js.map +1 -1
  6. package/lib/api/impl/validator/index.d.ts.map +1 -1
  7. package/lib/api/impl/validator/index.js +237 -49
  8. package/lib/api/impl/validator/index.js.map +1 -1
  9. package/lib/api/impl/validator/utils.js +1 -1
  10. package/lib/api/impl/validator/utils.js.map +1 -1
  11. package/lib/chain/archiveStore/archiveStore.js +1 -1
  12. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  13. package/lib/chain/blocks/index.js +7 -7
  14. package/lib/chain/blocks/index.js.map +1 -1
  15. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  16. package/lib/chain/blocks/verifyBlock.js +5 -2
  17. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  18. package/lib/chain/chain.d.ts +5 -2
  19. package/lib/chain/chain.d.ts.map +1 -1
  20. package/lib/chain/chain.js +6 -2
  21. package/lib/chain/chain.js.map +1 -1
  22. package/lib/chain/errors/executionPayloadBid.d.ts +5 -0
  23. package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
  24. package/lib/chain/errors/executionPayloadBid.js +1 -0
  25. package/lib/chain/errors/executionPayloadBid.js.map +1 -1
  26. package/lib/chain/forkChoice/index.d.ts.map +1 -1
  27. package/lib/chain/forkChoice/index.js +4 -0
  28. package/lib/chain/forkChoice/index.js.map +1 -1
  29. package/lib/chain/interface.d.ts +4 -2
  30. package/lib/chain/interface.d.ts.map +1 -1
  31. package/lib/chain/interface.js.map +1 -1
  32. package/lib/chain/opPools/executionPayloadBidPool.d.ts +7 -2
  33. package/lib/chain/opPools/executionPayloadBidPool.d.ts.map +1 -1
  34. package/lib/chain/opPools/executionPayloadBidPool.js +4 -4
  35. package/lib/chain/opPools/executionPayloadBidPool.js.map +1 -1
  36. package/lib/chain/opPools/index.d.ts +1 -1
  37. package/lib/chain/opPools/index.d.ts.map +1 -1
  38. package/lib/chain/opPools/index.js.map +1 -1
  39. package/lib/chain/prepareNextSlot.d.ts +5 -0
  40. package/lib/chain/prepareNextSlot.d.ts.map +1 -1
  41. package/lib/chain/prepareNextSlot.js +47 -6
  42. package/lib/chain/prepareNextSlot.js.map +1 -1
  43. package/lib/chain/produceBlock/computeNewStateRoot.d.ts.map +1 -1
  44. package/lib/chain/produceBlock/computeNewStateRoot.js +3 -25
  45. package/lib/chain/produceBlock/computeNewStateRoot.js.map +1 -1
  46. package/lib/chain/produceBlock/produceBlockBody.js +1 -1
  47. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  48. package/lib/chain/regen/interface.d.ts +3 -2
  49. package/lib/chain/regen/interface.d.ts.map +1 -1
  50. package/lib/chain/regen/interface.js +1 -0
  51. package/lib/chain/regen/interface.js.map +1 -1
  52. package/lib/chain/regen/queued.d.ts +2 -2
  53. package/lib/chain/regen/queued.d.ts.map +1 -1
  54. package/lib/chain/regen/queued.js +5 -5
  55. package/lib/chain/regen/queued.js.map +1 -1
  56. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +5 -1
  57. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  58. package/lib/chain/stateCache/fifoBlockStateCache.js +6 -0
  59. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  60. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +47 -5
  61. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  62. package/lib/chain/stateCache/persistentCheckpointsCache.js +117 -7
  63. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  64. package/lib/chain/stateCache/types.d.ts +3 -3
  65. package/lib/chain/stateCache/types.d.ts.map +1 -1
  66. package/lib/chain/validation/block.d.ts.map +1 -1
  67. package/lib/chain/validation/block.js +40 -10
  68. package/lib/chain/validation/block.js.map +1 -1
  69. package/lib/chain/validation/executionPayloadBid.d.ts +15 -3
  70. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  71. package/lib/chain/validation/executionPayloadBid.js +165 -10
  72. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  73. package/lib/execution/builder/apiClient.d.ts +70 -0
  74. package/lib/execution/builder/apiClient.d.ts.map +1 -0
  75. package/lib/execution/builder/apiClient.js +202 -0
  76. package/lib/execution/builder/apiClient.js.map +1 -0
  77. package/lib/execution/builder/validateBid.d.ts +23 -0
  78. package/lib/execution/builder/validateBid.d.ts.map +1 -0
  79. package/lib/execution/builder/validateBid.js +119 -0
  80. package/lib/execution/builder/validateBid.js.map +1 -0
  81. package/lib/metrics/metrics/lodestar.d.ts +29 -1
  82. package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
  83. package/lib/metrics/metrics/lodestar.js +61 -3
  84. package/lib/metrics/metrics/lodestar.js.map +1 -1
  85. package/lib/network/network.d.ts.map +1 -1
  86. package/lib/network/network.js +5 -4
  87. package/lib/network/network.js.map +1 -1
  88. package/lib/network/processor/constants.d.ts +7 -0
  89. package/lib/network/processor/constants.d.ts.map +1 -0
  90. package/lib/network/processor/constants.js +7 -0
  91. package/lib/network/processor/constants.js.map +1 -0
  92. package/lib/network/processor/gossipHandlers.js +1 -1
  93. package/lib/network/processor/gossipHandlers.js.map +1 -1
  94. package/lib/network/processor/gossipQueues/indexed.d.ts.map +1 -1
  95. package/lib/network/processor/gossipQueues/indexed.js +0 -1
  96. package/lib/network/processor/gossipQueues/indexed.js.map +1 -1
  97. package/lib/network/processor/index.d.ts +3 -0
  98. package/lib/network/processor/index.d.ts.map +1 -1
  99. package/lib/network/processor/index.js +52 -23
  100. package/lib/network/processor/index.js.map +1 -1
  101. package/lib/node/nodejs.d.ts.map +1 -1
  102. package/lib/node/nodejs.js +5 -0
  103. package/lib/node/nodejs.js.map +1 -1
  104. package/lib/sync/range/chain.d.ts.map +1 -1
  105. package/lib/sync/range/chain.js +9 -2
  106. package/lib/sync/range/chain.js.map +1 -1
  107. package/lib/sync/range/range.js +1 -1
  108. package/lib/sync/range/range.js.map +1 -1
  109. package/lib/sync/unknownBlock.d.ts +6 -2
  110. package/lib/sync/unknownBlock.d.ts.map +1 -1
  111. package/lib/sync/unknownBlock.js +48 -17
  112. package/lib/sync/unknownBlock.js.map +1 -1
  113. package/package.json +15 -15
  114. package/src/api/impl/beacon/blocks/index.ts +48 -13
  115. package/src/api/impl/lodestar/index.ts +1 -1
  116. package/src/api/impl/validator/index.ts +290 -49
  117. package/src/api/impl/validator/utils.ts +1 -1
  118. package/src/chain/archiveStore/archiveStore.ts +1 -1
  119. package/src/chain/blocks/index.ts +7 -7
  120. package/src/chain/blocks/verifyBlock.ts +5 -2
  121. package/src/chain/chain.ts +8 -1
  122. package/src/chain/errors/executionPayloadBid.ts +6 -0
  123. package/src/chain/forkChoice/index.ts +4 -0
  124. package/src/chain/interface.ts +4 -2
  125. package/src/chain/opPools/executionPayloadBidPool.ts +13 -7
  126. package/src/chain/opPools/index.ts +1 -1
  127. package/src/chain/prepareNextSlot.ts +49 -6
  128. package/src/chain/produceBlock/computeNewStateRoot.ts +3 -38
  129. package/src/chain/produceBlock/produceBlockBody.ts +1 -1
  130. package/src/chain/regen/interface.ts +3 -2
  131. package/src/chain/regen/queued.ts +5 -5
  132. package/src/chain/stateCache/fifoBlockStateCache.ts +8 -1
  133. package/src/chain/stateCache/persistentCheckpointsCache.ts +133 -8
  134. package/src/chain/stateCache/types.ts +3 -3
  135. package/src/chain/validation/block.ts +43 -10
  136. package/src/chain/validation/executionPayloadBid.ts +192 -12
  137. package/src/execution/builder/apiClient.ts +264 -0
  138. package/src/execution/builder/validateBid.ts +181 -0
  139. package/src/metrics/metrics/lodestar.ts +64 -4
  140. package/src/network/network.ts +7 -4
  141. package/src/network/processor/constants.ts +6 -0
  142. package/src/network/processor/gossipHandlers.ts +1 -1
  143. package/src/network/processor/gossipQueues/indexed.ts +0 -1
  144. package/src/network/processor/index.ts +67 -30
  145. package/src/node/nodejs.ts +5 -0
  146. package/src/sync/range/chain.ts +9 -2
  147. package/src/sync/range/range.ts +1 -1
  148. package/src/sync/unknownBlock.ts +62 -18
@@ -52,12 +52,16 @@ import {
52
52
  import {
53
53
  GWEI_TO_WEI,
54
54
  TimeoutError,
55
+ byteArrayEquals,
55
56
  defer,
56
57
  formatWeiToEth,
57
58
  fromHex,
59
+ prettyGweiToEth,
58
60
  prettyWeiToEth,
59
61
  resolveOrRacePromises,
62
+ sleep,
60
63
  toHex,
64
+ toPrintableUrl,
61
65
  toRootHex,
62
66
  } from "@lodestar/utils";
63
67
  import {MAX_BUILDER_BOOST_FACTOR} from "@lodestar/validator";
@@ -72,6 +76,7 @@ import {
72
76
  SyncCommitteeErrorCode,
73
77
  } from "../../../chain/errors/index.js";
74
78
  import {ChainEvent, CommonBlockBody} from "../../../chain/index.js";
79
+ import {PooledExecutionPayloadBid} from "../../../chain/opPools/index.js";
75
80
  import {PREPARE_NEXT_SLOT_BPS} from "../../../chain/prepareNextSlot.js";
76
81
  import {BlockType, ProduceFullDeneb, ProduceFullGloas} from "../../../chain/produceBlock/index.js";
77
82
  import {RegenCaller} from "../../../chain/regen/index.js";
@@ -80,7 +85,9 @@ import {validateApiAggregateAndProof} from "../../../chain/validation/index.js";
80
85
  import {validateGossipProposerPreferences} from "../../../chain/validation/proposerPreferences.js";
81
86
  import {validateSyncCommitteeGossipContributionAndProof} from "../../../chain/validation/syncCommitteeContributionAndProof.js";
82
87
  import {ZERO_HASH} from "../../../constants/index.js";
88
+ import {BUILDER_BID_DEADLINE_MS, BuilderApiBid, decodeBuilderUrl} from "../../../execution/builder/apiClient.js";
83
89
  import {BuilderStatus, NoBidReceived} from "../../../execution/builder/http.js";
90
+ import {getBuilderBidTotalGwei, validateBuilderApiExecutionPayloadBid} from "../../../execution/builder/validateBid.js";
84
91
  import {validateGossipFnRetryUnknownRoot} from "../../../network/processor/gossipHandlers.js";
85
92
  import {CommitteeSubscription} from "../../../network/subnets/index.js";
86
93
  import {callInNextEventLoop} from "../../../util/eventLoop.js";
@@ -112,6 +119,55 @@ import {
112
119
  const BLOCK_PRODUCTION_RACE_CUTOFF_MS = 2_000;
113
120
  /** Overall timeout for execution and block production apis */
114
121
  const BLOCK_PRODUCTION_RACE_TIMEOUT_MS = 12_000;
122
+ /** Rejection message of the bid block branch when there is no viable bid to commit to */
123
+ const NO_BID_AVAILABLE = "No builder bid available";
124
+
125
+ type BidCandidate = {
126
+ signedBid: gloas.SignedExecutionPayloadBid;
127
+ /** Total payment in Gwei, counting the execution payment at most at the entry's cap */
128
+ totalGwei: bigint;
129
+ boostFactor: bigint;
130
+ url?: string;
131
+ /** Time in milliseconds from the slot start when the bid was received */
132
+ receivedMs: number;
133
+ };
134
+
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.
139
+ */
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
+ }
168
+ }
169
+ return best;
170
+ }
115
171
 
116
172
  type ProduceBlockContentsRes = {executionPayloadValue: Wei; consensusBlockValue: Wei} & {
117
173
  data: BlockContents;
@@ -857,7 +913,7 @@ export function getValidatorApi(
857
913
  feeRecipient,
858
914
  strictFeeRecipientCheck,
859
915
  includePayload,
860
- builderBoostFactor,
916
+ builderConfig,
861
917
  }) {
862
918
  const fork = config.getForkName(slot);
863
919
 
@@ -865,7 +921,7 @@ export function getValidatorApi(
865
921
  throw new ApiError(400, `produceBlockV4 not supported for pre-gloas fork=${fork}`);
866
922
  }
867
923
 
868
- builderBoostFactor = builderBoostFactor ?? BigInt(100);
924
+ const builderBoostFactor = builderConfig.builderBoostFactor;
869
925
  if (builderBoostFactor > MAX_BUILDER_BOOST_FACTOR) {
870
926
  throw new ApiError(400, `Invalid builderBoostFactor=${builderBoostFactor} > MAX_BUILDER_BOOST_FACTOR`);
871
927
  }
@@ -892,32 +948,122 @@ export function getValidatorApi(
892
948
  })
893
949
  );
894
950
 
895
- // TODO GLOAS: add external builder api support when it is implemented
896
951
  const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
897
952
  const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
953
+ // Post-gloas every proposal parent has an execution payload hash
954
+ if (bidParentBlockHash === null) {
955
+ throw new ApiError(500, `Unknown parent block hash for proposal parent ${parentBlockRootHex}`);
956
+ }
898
957
  const circuitBreakerActive = chain.builderCircuitBreaker.isActive(slot, parentBlock);
899
- // Keep a builder bid as fallback unless the circuit breaker is active
900
- const builderBid = circuitBreakerActive
901
- ? null
902
- : chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
903
958
 
904
- const logCtx = {
905
- slot,
906
- parentSlot,
907
- parentBlockRoot: parentBlockRootHex,
908
- parentBlockHash: parentBlock.executionPayloadBlockHash,
909
- fork,
910
- builderBoostFactor,
911
- strictFeeRecipientCheck,
912
- circuitBreakerActive,
913
- ...(builderBid !== null
914
- ? {
915
- bidValue: builderBid.message.value,
916
- builderIndex: builderBid.message.builderIndex,
917
- bidBlockHash: toRootHex(builderBid.message.blockHash),
959
+ // Fire builder API bid requests while the local payload is built, one request per entry.
960
+ // Any entry failure yields no bid and never fails block production.
961
+ let builderApiBidsPromise: Promise<BuilderApiBid[]> = Promise.resolve([]);
962
+ if (builderConfig.builders.length > 0 && !circuitBreakerActive) {
963
+ try {
964
+ const proposerIndex = chain.getHeadState().getBeaconProposer(slot);
965
+ const proposerPubkey = chain.pubkeyCache.getOrThrow(proposerIndex).toBytes();
966
+ builderApiBidsPromise = chain.builderApiClient.getExecutionPayloadBids(
967
+ builderConfig.builders,
968
+ slot,
969
+ fromHex(bidParentBlockHash),
970
+ parentBlockRoot,
971
+ proposerPubkey
972
+ );
973
+ } catch (e) {
974
+ logger.warn("Unable to request builder API bids", {slot}, e as Error);
975
+ }
976
+ }
977
+
978
+ // A builder bid is expected if builders are configured or a p2p bid was already received.
979
+ // Used by the censorship override which may run before the bid deadline
980
+ const builderBidExpected =
981
+ builderConfig.builders.length > 0 ||
982
+ (!circuitBreakerActive &&
983
+ chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex) !== null);
984
+
985
+ // Select the p2p bid once builders had time to bid up, matching the deadline advertised
986
+ // on builder API bid requests, unless the circuit breaker is active
987
+ const p2pBidPromise: Promise<PooledExecutionPayloadBid | null> = circuitBreakerActive
988
+ ? Promise.resolve(null)
989
+ : sleep(Math.max(0, BUILDER_BID_DEADLINE_MS - chain.clock.msFromSlot(slot))).then(() => {
990
+ const p2pBid = chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
991
+ // Discard p2p bids below the proposer's configured floor on the total payment.
992
+ // A p2p bid's total is just its value since gossip validation enforces executionPayment=0.
993
+ if (p2pBid !== null && BigInt(p2pBid.signedBid.message.value) < builderConfig.minBid) {
994
+ logger.info("Best p2p bid below configured minimum", {
995
+ slot,
996
+ bidValue: prettyGweiToEth(p2pBid.signedBid.message.value),
997
+ minBid: prettyGweiToEth(builderConfig.minBid),
998
+ });
999
+ return null;
918
1000
  }
919
- : {}),
920
- };
1001
+ return p2pBid;
1002
+ });
1003
+
1004
+ // Candidates are ranked by their boosted counted total payment, the p2p bid is governed
1005
+ // by the top-level factors and each builder API bid by its own entry. Ties prefer the
1006
+ // earliest received builder API bid, so the signed block can be routed back to its
1007
+ // builder directly.
1008
+ const bestBidPromise: Promise<BidCandidate | null> = (async () => {
1009
+ const [builderApiBids, p2pBid] = await Promise.all([builderApiBidsPromise, p2pBidPromise]);
1010
+ let parentExecutionRequestsPromise: Promise<gloas.ExecutionRequests> | null = null;
1011
+
1012
+ const candidates = (
1013
+ await Promise.all(
1014
+ builderApiBids.map(async ({url, entry, signedBid, receivedMs}): Promise<BidCandidate | null> => {
1015
+ try {
1016
+ await validateBuilderApiExecutionPayloadBid(chain, signedBid, {
1017
+ slot,
1018
+ parentBlock,
1019
+ parentBlockHash: bidParentBlockHash,
1020
+ parentBlockRoot: parentBlockRootHex,
1021
+ entry,
1022
+ getParentExecutionRequests: () => {
1023
+ parentExecutionRequestsPromise ??= chain.getParentExecutionRequests(parentSlot, parentBlockRootHex);
1024
+ return parentExecutionRequestsPromise;
1025
+ },
1026
+ });
1027
+ return {
1028
+ signedBid,
1029
+ totalGwei: getBuilderBidTotalGwei(signedBid.message, entry.maxExecutionPayment),
1030
+ boostFactor: entry.builderBoostFactor,
1031
+ url,
1032
+ receivedMs,
1033
+ };
1034
+ } catch (e) {
1035
+ metrics?.builderApi.bidsDiscarded.inc();
1036
+ logger.warn("Ignoring invalid builder API bid", {slot, builder: toPrintableUrl(url)}, e as Error);
1037
+ return null;
1038
+ }
1039
+ })
1040
+ )
1041
+ ).filter((candidate): candidate is BidCandidate => candidate !== null);
1042
+
1043
+ if (p2pBid !== null) {
1044
+ candidates.push({
1045
+ signedBid: p2pBid.signedBid,
1046
+ totalGwei: BigInt(p2pBid.signedBid.message.value),
1047
+ boostFactor: builderConfig.builderBoostFactor,
1048
+ receivedMs: p2pBid.receivedMs,
1049
+ });
1050
+ }
1051
+
1052
+ const best = selectBestBid(candidates);
1053
+ if (candidates.length > 0) {
1054
+ logger.debug("Ranked builder bid candidates", {
1055
+ 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",
1063
+ });
1064
+ }
1065
+ return best;
1066
+ })();
921
1067
 
922
1068
  const commonBlockBodyPromise = chain.produceCommonBlockBody({
923
1069
  slot,
@@ -937,9 +1083,6 @@ export function getValidatorApi(
937
1083
  };
938
1084
 
939
1085
  metrics?.blockProductionRequests.inc({source: ProducedBlockSource.engine});
940
- if (builderBid !== null) {
941
- metrics?.blockProductionRequests.inc({source: ProducedBlockSource.builder});
942
- }
943
1086
 
944
1087
  const timed = <T>(source: ProducedBlockSource, fn: () => Promise<T>): Promise<T> => {
945
1088
  const t = metrics?.blockProductionTime.startTimer();
@@ -956,19 +1099,25 @@ export function getValidatorApi(
956
1099
  const enginePromise: ReturnType<typeof chain.produceBlock> = timed(ProducedBlockSource.engine, () =>
957
1100
  chain.produceBlock(baseAttrs)
958
1101
  ).then((engineBlock) => {
959
- // No need to wait for the bid block if the engine block will always be selected due to
960
- // suspected builder censorship or a builder boost factor of 0
961
- if (engineBlock.shouldOverrideBuilder || builderBoostFactor === BigInt(0)) {
1102
+ // No need to wait for the bid block if the engine block will always be selected, either
1103
+ // due to suspected builder censorship, or because no builders are configured and the
1104
+ // boost factor of 0 always prefers the local block over p2p bids
1105
+ if (engineBlock.shouldOverrideBuilder || (builderConfig.builders.length === 0 && builderBoostFactor === 0n)) {
962
1106
  controller.abort();
963
1107
  }
964
1108
  return engineBlock;
965
1109
  });
966
- const bidPromise: ReturnType<typeof chain.produceBlock> =
967
- builderBid !== null
968
- ? timed(ProducedBlockSource.builder, () => chain.produceBlock({...baseAttrs, builderBid}))
969
- : Promise.reject(new Error("No builder bid available"));
1110
+ const bidBlockPromise: ReturnType<typeof chain.produceBlock> = bestBidPromise.then((candidate) => {
1111
+ if (candidate === null) {
1112
+ throw new Error(NO_BID_AVAILABLE);
1113
+ }
1114
+ metrics?.blockProductionRequests.inc({source: ProducedBlockSource.builder});
1115
+ return timed(ProducedBlockSource.builder, () =>
1116
+ chain.produceBlock({...baseAttrs, builderBid: candidate.signedBid})
1117
+ );
1118
+ });
970
1119
 
971
- const [engineResult, bidResult] = await resolveOrRacePromises([enginePromise, bidPromise], {
1120
+ const [engineResult, bidBlockResult] = await resolveOrRacePromises([enginePromise, bidBlockPromise], {
972
1121
  resolveTimeoutMs: cutoffMs,
973
1122
  raceTimeoutMs: BLOCK_PRODUCTION_RACE_TIMEOUT_MS,
974
1123
  signal: controller.signal,
@@ -977,29 +1126,74 @@ export function getValidatorApi(
977
1126
  let bestResult: typeof engineResult | null = null;
978
1127
  let source: ProducedBlockSource = ProducedBlockSource.engine;
979
1128
 
1129
+ // Resolved instantly whenever the bid branch produced a block
1130
+ const bestBid = bidBlockResult.status === "fulfilled" ? await bestBidPromise : null;
1131
+
1132
+ const logCtx = {
1133
+ slot,
1134
+ parentSlot,
1135
+ parentBlockRoot: parentBlockRootHex,
1136
+ parentBlockHash: parentBlock.executionPayloadBlockHash,
1137
+ fork,
1138
+ builderBoostFactor,
1139
+ strictFeeRecipientCheck,
1140
+ circuitBreakerActive,
1141
+ builderEntries: builderConfig.builders.length,
1142
+ ...(bestBid !== null
1143
+ ? {
1144
+ bidSource: bestBid.url !== undefined ? toPrintableUrl(bestBid.url) : "p2p",
1145
+ bidValue: prettyGweiToEth(bestBid.signedBid.message.value),
1146
+ bidExecutionPayment: prettyGweiToEth(bestBid.signedBid.message.executionPayment),
1147
+ // The full bid total and the counted total used during bid selection
1148
+ bidTotal: prettyGweiToEth(
1149
+ BigInt(bestBid.signedBid.message.value) + bestBid.signedBid.message.executionPayment
1150
+ ),
1151
+ bidCountedTotal: prettyGweiToEth(bestBid.totalGwei),
1152
+ bidBoostFactor: bestBid.boostFactor,
1153
+ builderIndex: bestBid.signedBid.message.builderIndex,
1154
+ bidBlockHash: toRootHex(bestBid.signedBid.message.blockHash),
1155
+ bidReceivedMs: bestBid.receivedMs,
1156
+ }
1157
+ : {}),
1158
+ };
1159
+
980
1160
  // handle shouldOverrideBuilder separately
981
- if (engineResult.status === "fulfilled" && engineResult.value.shouldOverrideBuilder && builderBid !== null) {
1161
+ if (
1162
+ engineResult.status === "fulfilled" &&
1163
+ engineResult.value.shouldOverrideBuilder &&
1164
+ (builderBidExpected || bidBlockResult.status === "fulfilled")
1165
+ ) {
982
1166
  source = ProducedBlockSource.engine;
983
1167
  bestResult = engineResult;
984
1168
  metrics?.blockProductionSelectionResults.inc({
985
1169
  source: ProducedBlockSource.engine,
986
1170
  reason: EngineBlockSelectionReason.BuilderCensorship,
987
1171
  });
988
- logger.warn("Selected local block: censorship suspected in builder bid", logCtx);
989
- } else if (engineResult.status === "fulfilled" && bidResult.status === "fulfilled") {
1172
+ logger.warn("Selected local block: censorship suspected in builder bid", {
1173
+ ...logCtx,
1174
+ durationMs: engineResult.durationMs,
1175
+ ...getBlockValueLogInfo(engineResult.value),
1176
+ });
1177
+ } else if (engineResult.status === "fulfilled" && bidBlockResult.status === "fulfilled") {
990
1178
  const result = selectBlockProductionSourceByBoostFactor({
991
- builderBoostFactor,
1179
+ builderBoostFactor: bestBid?.boostFactor ?? builderBoostFactor,
992
1180
  engineExecutionPayloadValue: engineResult.value.executionPayloadValue,
993
- // The bid value is the payment to the proposer, in Gwei
994
- builderExecutionPayloadValue: BigInt(builderBid?.message.value ?? 0) * GWEI_TO_WEI,
1181
+ // The bid total payment is its value plus its counted executionPayment, in Gwei
1182
+ builderExecutionPayloadValue: (bestBid?.totalGwei ?? 0n) * GWEI_TO_WEI,
995
1183
  });
996
1184
  source = result.source;
997
1185
  metrics?.blockProductionSelectionResults.inc(result);
998
- logger.info(`Selected ${source} block`, {reason: result.reason, ...logCtx});
999
- bestResult = source === ProducedBlockSource.builder ? bidResult : engineResult;
1000
- } else if (bidResult.status === "fulfilled") {
1186
+ logger.info(`Selected ${source} block`, {
1187
+ reason: result.reason,
1188
+ ...logCtx,
1189
+ engineDurationMs: engineResult.durationMs,
1190
+ ...getBlockValueLogInfo(engineResult.value, ProducedBlockSource.engine),
1191
+ builderDurationMs: bidBlockResult.durationMs,
1192
+ });
1193
+ bestResult = source === ProducedBlockSource.builder ? bidBlockResult : engineResult;
1194
+ } else if (bidBlockResult.status === "fulfilled") {
1001
1195
  source = ProducedBlockSource.builder;
1002
- bestResult = bidResult;
1196
+ bestResult = bidBlockResult;
1003
1197
  const reason =
1004
1198
  engineResult.status === "pending"
1005
1199
  ? BuilderBlockSelectionReason.EnginePending
@@ -1008,28 +1202,32 @@ export function getValidatorApi(
1008
1202
  logger.info("Selected builder bid block: no local block produced", {
1009
1203
  reason,
1010
1204
  ...logCtx,
1205
+ durationMs: bidBlockResult.durationMs,
1206
+ ...getBlockValueLogInfo(bidBlockResult.value),
1011
1207
  error: engineResult.status === "rejected" ? (engineResult.reason as Error).message : undefined,
1012
1208
  });
1013
1209
  } else if (engineResult.status === "fulfilled") {
1014
1210
  source = ProducedBlockSource.engine;
1015
1211
  bestResult = engineResult;
1016
1212
  const reason =
1017
- builderBid === null
1213
+ bidBlockResult.status === "rejected" && (bidBlockResult.reason as Error).message === NO_BID_AVAILABLE
1018
1214
  ? EngineBlockSelectionReason.BuilderNoBid
1019
- : bidResult.status === "pending"
1215
+ : bidBlockResult.status === "pending"
1020
1216
  ? EngineBlockSelectionReason.BuilderPending
1021
1217
  : EngineBlockSelectionReason.BuilderError;
1022
1218
  metrics?.blockProductionSelectionResults.inc({source: ProducedBlockSource.engine, reason});
1023
1219
  logger.info("Selected local block: no builder bid block produced", {
1024
1220
  reason,
1025
1221
  ...logCtx,
1026
- error: bidResult.status === "rejected" ? (bidResult.reason as Error).message : undefined,
1222
+ durationMs: engineResult.durationMs,
1223
+ ...getBlockValueLogInfo(engineResult.value),
1224
+ error: bidBlockResult.status === "rejected" ? (bidBlockResult.reason as Error).message : undefined,
1027
1225
  });
1028
1226
  }
1029
1227
 
1030
1228
  if (bestResult === null || bestResult.status !== "fulfilled") {
1031
1229
  const engineReason = engineResult.status === "rejected" ? engineResult.reason : engineResult.status;
1032
- const bidReason = bidResult.status === "rejected" ? bidResult.reason : bidResult.status;
1230
+ const bidReason = bidBlockResult.status === "rejected" ? bidBlockResult.reason : bidBlockResult.status;
1033
1231
  logger.error("Block production failed", {
1034
1232
  ...logCtx,
1035
1233
  engineReason: String(engineReason),
@@ -1093,7 +1291,13 @@ export function getValidatorApi(
1093
1291
 
1094
1292
  return {
1095
1293
  data: block as gloas.BeaconBlock,
1096
- meta: {version: fork, consensusBlockValue, executionPayloadValue, executionPayloadIncluded: false},
1294
+ meta: {
1295
+ version: fork,
1296
+ consensusBlockValue,
1297
+ executionPayloadValue,
1298
+ executionPayloadIncluded: false,
1299
+ builderUrl: source === ProducedBlockSource.builder ? bestBid?.url : undefined,
1300
+ },
1097
1301
  };
1098
1302
  },
1099
1303
 
@@ -1877,6 +2081,43 @@ export function getValidatorApi(
1877
2081
  }
1878
2082
  },
1879
2083
 
2084
+ async submitBuilderPreferences({builderPreferences}) {
2085
+ const failures: FailureList = [];
2086
+
2087
+ await Promise.all(
2088
+ builderPreferences.map(async (entry, i) => {
2089
+ let builder = Buffer.from(entry.url).toString("utf8");
2090
+ try {
2091
+ const url = decodeBuilderUrl(entry.url);
2092
+ builder = toPrintableUrl(url);
2093
+ const proposerIndex = chain.getHeadState().getBeaconProposer(entry.auth.message.slot);
2094
+ const expectedProposerPubkey = chain.pubkeyCache.getOrThrow(proposerIndex).toBytes();
2095
+ if (!byteArrayEquals(entry.proposerPubkey, expectedProposerPubkey)) {
2096
+ throw new ApiError(
2097
+ 400,
2098
+ `Invalid proposer pubkey for builder preferences slot=${entry.auth.message.slot}`
2099
+ );
2100
+ }
2101
+ await chain.builderApiClient.submitBuilderPreferences(url, entry.proposerPubkey, {
2102
+ preferences: {maxExecutionPayment: entry.maxExecutionPayment},
2103
+ auth: entry.auth,
2104
+ });
2105
+ } catch (e) {
2106
+ failures.push({index: i, message: (e as Error).message});
2107
+ logger.verbose(
2108
+ `Error on submitBuilderPreferences [${i}]`,
2109
+ {slot: entry.auth.message.slot, builder},
2110
+ e as Error
2111
+ );
2112
+ }
2113
+ })
2114
+ );
2115
+
2116
+ if (failures.length > 0) {
2117
+ throw new IndexedError("Error submitting builder preferences", failures);
2118
+ }
2119
+ },
2120
+
1880
2121
  async getExecutionPayloadEnvelope({slot, beaconBlockRoot}) {
1881
2122
  const fork = config.getForkName(slot);
1882
2123
 
@@ -89,7 +89,7 @@ export function selectBlockProductionSourceByBoostFactor({
89
89
  return {source: ProducedBlockSource.builder, reason: BuilderBlockSelectionReason.BuilderPreferred};
90
90
  }
91
91
 
92
- if (engineExecutionPayloadValue >= (builderExecutionPayloadValue * builderBoostFactor) / BigInt(100)) {
92
+ if (engineExecutionPayloadValue * 100n >= builderExecutionPayloadValue * builderBoostFactor) {
93
93
  return {source: ProducedBlockSource.engine, reason: EngineBlockSelectionReason.BlockValue};
94
94
  }
95
95
 
@@ -192,7 +192,7 @@ export class ArchiveStore {
192
192
 
193
193
  private onCheckpoint = (): void => {
194
194
  const headStateRoot = this.chain.forkChoice.getHead().stateRoot;
195
- this.chain.regen.pruneOnCheckpoint(
195
+ this.chain.regen.onCheckpoint(
196
196
  this.chain.forkChoice.getFinalizedCheckpoint().epoch,
197
197
  this.chain.forkChoice.getJustifiedCheckpoint().epoch,
198
198
  headStateRoot
@@ -143,17 +143,17 @@ export async function processBlocks(
143
143
  await importBlock.call(this, fullyVerifiedBlock, opts);
144
144
  }
145
145
 
146
- const payloadInput = payloadEnvelopes?.get(slot);
147
- if (payloadInput?.hasPayloadEnvelope()) {
146
+ // PayloadEnvelopeInput is shared and may receive an envelope after the DA snapshot was taken.
147
+ const payloadDA = payloadDAStatuses.get(slot);
148
+ if (payloadDA !== undefined) {
149
+ const payloadInput = payloadEnvelopes?.get(slot);
150
+ if (payloadInput === undefined) {
151
+ throw new Error(`Missing payload input for slot ${slot} after DA verification`);
152
+ }
148
153
  if (!payloadInput.isComplete()) {
149
154
  // we validated DA before reaching this
150
155
  throw new Error(`Payload envelope for slot ${slot} not complete after DA verification`);
151
156
  }
152
- // we already awaited DA in verifyBlocksInEpoch for this segment
153
- const payloadDA = payloadDAStatuses.get(slot);
154
- if (payloadDA === undefined) {
155
- throw new Error(`Missing payload DA status for slot ${slot}`);
156
- }
157
157
  await importExecutionPayload.call(this, payloadInput, payloadDA, {validSignature: false});
158
158
  }
159
159
 
@@ -132,12 +132,15 @@ export async function verifyBlocksInEpoch(
132
132
  }> =
133
133
  fork >= ForkSeq.gloas
134
134
  ? (async () => {
135
- // Validate DA for ALL payloads in the Map, not just those paired with blockInputs.
135
+ // Range sync creates a PayloadEnvelopeInput for every Gloas block, but an entry without
136
+ // an envelope represents a valid EMPTY slot and has no payload DA to verify.
136
137
  // A checkpoint-sync batch may include a payload for a slot whose block was filtered
137
138
  // out of relevantBlocks (e.g., the anchor at the finalized slot); that payload still
138
139
  // needs DA validation so it can be imported in processBlocks.
139
140
  const payloadInputsForDa: PayloadEnvelopeInput[] =
140
- payloadEnvelopes !== null ? Array.from(payloadEnvelopes.values()) : [];
141
+ payloadEnvelopes !== null
142
+ ? Array.from(payloadEnvelopes.values()).filter((payloadInput) => payloadInput.hasPayloadEnvelope())
143
+ : [];
141
144
  const {dataAvailabilityStatuses, availableTime} = await verifyPayloadsDataAvailability(
142
145
  payloadInputsForDa,
143
146
  abortController.signal
@@ -61,6 +61,7 @@ import {ProcessShutdownCallback} from "@lodestar/validator";
61
61
  import {GENESIS_EPOCH, ZERO_HASH} from "../constants/index.js";
62
62
  import {IBeaconDb} from "../db/index.js";
63
63
  import {BLOB_SIDECARS_IN_WRAPPER_INDEX} from "../db/repositories/blobSidecars.js";
64
+ import {BuilderApiClient, BuilderApiClientOpts} from "../execution/builder/apiClient.js";
64
65
  import {BuilderStatus} from "../execution/builder/http.js";
65
66
  import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js";
66
67
  import {Metrics} from "../metrics/index.js";
@@ -165,6 +166,7 @@ export class BeaconChain implements IBeaconChain {
165
166
  readonly executionEngine: IExecutionEngine;
166
167
  readonly executionBuilder?: IExecutionBuilder;
167
168
  readonly builderCircuitBreaker: BuilderCircuitBreaker;
169
+ readonly builderApiClient: BuilderApiClient;
168
170
  // Expose config for convenience in modularized functions
169
171
  readonly config: BeaconConfig;
170
172
  readonly custodyConfig: CustodyConfig;
@@ -275,6 +277,7 @@ export class BeaconChain implements IBeaconChain {
275
277
  isAnchorStateFinalized,
276
278
  executionEngine,
277
279
  executionBuilder,
280
+ builderApiClientOpts,
278
281
  }: {
279
282
  privateKey: PrivateKey;
280
283
  config: BeaconConfig;
@@ -292,6 +295,7 @@ export class BeaconChain implements IBeaconChain {
292
295
  isAnchorStateFinalized: boolean;
293
296
  executionEngine: IExecutionEngine;
294
297
  executionBuilder?: IExecutionBuilder;
298
+ builderApiClientOpts?: BuilderApiClientOpts;
295
299
  }
296
300
  ) {
297
301
  this.opts = opts;
@@ -448,6 +452,8 @@ export class BeaconChain implements IBeaconChain {
448
452
  {forkChoice, logger, metrics}
449
453
  );
450
454
 
455
+ this.builderApiClient = new BuilderApiClient(builderApiClientOpts ?? {}, config, clock, bls, metrics, logger);
456
+
451
457
  this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({
452
458
  config,
453
459
  clock,
@@ -1321,9 +1327,10 @@ export class BeaconChain implements IBeaconChain {
1321
1327
  }
1322
1328
  }
1323
1329
 
1324
- predictProposerHead(slot: Slot): ProtoBlock {
1330
+ predictProposerHead(): ProtoBlock {
1325
1331
  this.metrics?.forkChoice.requests.inc();
1326
1332
  const timer = this.metrics?.forkChoice.findHead.startTimer({caller: FindHeadFnName.predictProposerHead});
1333
+ const slot = this.clock.currentSlot;
1327
1334
  const secFromSlot = this.clock.secFromSlot(slot);
1328
1335
 
1329
1336
  try {
@@ -9,6 +9,7 @@ export enum ExecutionPayloadBidErrorCode {
9
9
  BID_ALREADY_KNOWN = "EXECUTION_PAYLOAD_BID_ERROR_BID_ALREADY_KNOWN",
10
10
  BID_TOO_LOW = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_LOW",
11
11
  BID_TOO_HIGH = "EXECUTION_PAYLOAD_BID_ERROR_BID_TOO_HIGH",
12
+ BUILDER_MAY_EXIT = "EXECUTION_PAYLOAD_BID_ERROR_BUILDER_MAY_EXIT",
12
13
  TOO_MANY_KZG_COMMITMENTS = "EXECUTION_PAYLOAD_BID_ERROR_TOO_MANY_KZG_COMMITMENTS",
13
14
  UNKNOWN_BLOCK_ROOT = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_BLOCK_ROOT",
14
15
  UNKNOWN_PARENT_BLOCK_HASH = "EXECUTION_PAYLOAD_BID_ERROR_UNKNOWN_PARENT_BLOCK_HASH",
@@ -50,6 +51,11 @@ export type ExecutionPayloadBidErrorType =
50
51
  }
51
52
  | {code: ExecutionPayloadBidErrorCode.BID_TOO_LOW; bidValue: number; currentHighestBid: number}
52
53
  | {code: ExecutionPayloadBidErrorCode.BID_TOO_HIGH; bidValue: number; builderBalance: number}
54
+ | {
55
+ code: ExecutionPayloadBidErrorCode.BUILDER_MAY_EXIT;
56
+ builderIndex: BuilderIndex;
57
+ parentBlockRoot: RootHex;
58
+ }
53
59
  | {
54
60
  code: ExecutionPayloadBidErrorCode.TOO_MANY_KZG_COMMITMENTS;
55
61
  blobKzgCommitmentsLen: number;
@@ -134,6 +134,8 @@ export function initializeForkChoiceFromFinalizedState(
134
134
  stateRoot: toRootHex(blockHeader.stateRoot),
135
135
  blockRoot: toRootHex(checkpoint.root),
136
136
  timeliness: true, // Optimistically assume is timely
137
+ ptcTimeliness: true, // Spec: block_timeliness for anchor = [True, True]
138
+ proposerIndex: blockHeader.proposerIndex,
137
139
 
138
140
  justifiedEpoch: justifiedCheckpoint.epoch,
139
141
  justifiedRoot: toRootHex(justifiedCheckpoint.root),
@@ -230,6 +232,8 @@ export function initializeForkChoiceFromUnfinalizedState(
230
232
  blockRoot: headRoot,
231
233
  targetRoot: headRoot,
232
234
  timeliness: true, // Optimistically assume is timely
235
+ ptcTimeliness: true, // Spec: block_timeliness for anchor = [True, True]
236
+ proposerIndex: blockHeader.proposerIndex,
233
237
 
234
238
  justifiedEpoch: justifiedCheckpoint.epoch,
235
239
  justifiedRoot: toRootHex(justifiedCheckpoint.root),
@@ -24,6 +24,7 @@ import {
24
24
  rewards,
25
25
  } from "@lodestar/types";
26
26
  import {Logger} from "@lodestar/utils";
27
+ import {BuilderApiClient} from "../execution/builder/apiClient.js";
27
28
  import {IExecutionBuilder, IExecutionEngine} from "../execution/index.js";
28
29
  import {Metrics} from "../metrics/metrics.js";
29
30
  import {BufferPool} from "../util/bufferPool.js";
@@ -99,6 +100,7 @@ export interface IBeaconChain {
99
100
  readonly executionEngine: IExecutionEngine;
100
101
  readonly executionBuilder?: IExecutionBuilder;
101
102
  readonly builderCircuitBreaker: BuilderCircuitBreaker;
103
+ readonly builderApiClient: BuilderApiClient;
102
104
  // Expose config for convenience in modularized functions
103
105
  readonly config: BeaconConfig;
104
106
  readonly custodyConfig: CustodyConfig;
@@ -272,8 +274,8 @@ export interface IBeaconChain {
272
274
 
273
275
  recomputeForkChoiceHead(caller: ForkchoiceCaller): ProtoBlock;
274
276
 
275
- /** When proposerBoostReorg is enabled, this is called at slot n-1 to predict the head block to build on if we are proposing at slot n */
276
- predictProposerHead(slot: Slot): ProtoBlock;
277
+ /** When proposerBoostReorg is enabled, predict (as of the current slot) the head block a proposer would build on next slot */
278
+ predictProposerHead(): ProtoBlock;
277
279
 
278
280
  /** When proposerBoostReorg is enabled and we are proposing a block, this is called to determine which head block to build on */
279
281
  getProposerHead(slot: Slot): ProtoBlock;