@lodestar/beacon-node 1.44.0-dev.0e1ac44567 → 1.44.0-dev.1557420d0a

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 (54) hide show
  1. package/lib/api/impl/debug/index.d.ts.map +1 -1
  2. package/lib/api/impl/debug/index.js +69 -12
  3. package/lib/api/impl/debug/index.js.map +1 -1
  4. package/lib/api/impl/validator/index.d.ts.map +1 -1
  5. package/lib/api/impl/validator/index.js +21 -9
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/errors/payloadAttestation.d.ts +6 -0
  8. package/lib/chain/errors/payloadAttestation.d.ts.map +1 -1
  9. package/lib/chain/errors/payloadAttestation.js +1 -0
  10. package/lib/chain/errors/payloadAttestation.js.map +1 -1
  11. package/lib/chain/prepareNextSlot.js +1 -1
  12. package/lib/chain/prepareNextSlot.js.map +1 -1
  13. package/lib/chain/produceBlock/produceBlockBody.js +3 -3
  14. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  15. package/lib/chain/regen/interface.d.ts +1 -0
  16. package/lib/chain/regen/interface.d.ts.map +1 -1
  17. package/lib/chain/regen/interface.js +1 -0
  18. package/lib/chain/regen/interface.js.map +1 -1
  19. package/lib/chain/validation/executionPayloadBid.js +22 -5
  20. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  21. package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
  22. package/lib/chain/validation/payloadAttestationMessage.js +24 -4
  23. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  24. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  25. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +5 -0
  26. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  27. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js +1 -1
  28. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js.map +1 -1
  29. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +3 -3
  30. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  31. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts +2 -1
  32. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts.map +1 -1
  33. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +15 -1
  34. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
  35. package/lib/network/reqresp/handlers/index.js +2 -2
  36. package/lib/network/reqresp/handlers/index.js.map +1 -1
  37. package/lib/network/reqresp/utils/dataColumnResponseValidation.d.ts.map +1 -1
  38. package/lib/network/reqresp/utils/dataColumnResponseValidation.js +8 -0
  39. package/lib/network/reqresp/utils/dataColumnResponseValidation.js.map +1 -1
  40. package/package.json +14 -16
  41. package/src/api/impl/debug/index.ts +73 -12
  42. package/src/api/impl/validator/index.ts +23 -14
  43. package/src/chain/errors/payloadAttestation.ts +2 -0
  44. package/src/chain/prepareNextSlot.ts +1 -1
  45. package/src/chain/produceBlock/produceBlockBody.ts +3 -3
  46. package/src/chain/regen/interface.ts +1 -0
  47. package/src/chain/validation/executionPayloadBid.ts +23 -5
  48. package/src/chain/validation/payloadAttestationMessage.ts +26 -4
  49. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +6 -0
  50. package/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +1 -1
  51. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +3 -3
  52. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +20 -1
  53. package/src/network/reqresp/handlers/index.ts +2 -2
  54. package/src/network/reqresp/utils/dataColumnResponseValidation.ts +8 -0
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.44.0-dev.0e1ac44567",
14
+ "version": "1.44.0-dev.1557420d0a",
15
15
  "type": "module",
16
16
  "exports": {
17
17
  ".": {
@@ -116,7 +116,6 @@
116
116
  "@chainsafe/libp2p-quic": "^2.0.1",
117
117
  "@chainsafe/persistent-merkle-tree": "^1.2.5",
118
118
  "@chainsafe/prometheus-gc-stats": "^1.0.0",
119
- "@chainsafe/pubkey-index-map": "^3.0.0",
120
119
  "@chainsafe/snappy-wasm": "^0.5.0",
121
120
  "@chainsafe/ssz": "^1.4.0",
122
121
  "@chainsafe/threads": "^1.11.3",
@@ -135,17 +134,17 @@
135
134
  "@libp2p/peer-id": "^6.0.4",
136
135
  "@libp2p/prometheus-metrics": "^5.0.14",
137
136
  "@libp2p/tcp": "^11.0.13",
138
- "@lodestar/api": "^1.44.0-dev.0e1ac44567",
139
- "@lodestar/config": "^1.44.0-dev.0e1ac44567",
140
- "@lodestar/db": "^1.44.0-dev.0e1ac44567",
141
- "@lodestar/fork-choice": "^1.44.0-dev.0e1ac44567",
142
- "@lodestar/logger": "^1.44.0-dev.0e1ac44567",
143
- "@lodestar/params": "^1.44.0-dev.0e1ac44567",
144
- "@lodestar/reqresp": "^1.44.0-dev.0e1ac44567",
145
- "@lodestar/state-transition": "^1.44.0-dev.0e1ac44567",
146
- "@lodestar/types": "^1.44.0-dev.0e1ac44567",
147
- "@lodestar/utils": "^1.44.0-dev.0e1ac44567",
148
- "@lodestar/validator": "^1.44.0-dev.0e1ac44567",
137
+ "@lodestar/api": "^1.44.0-dev.1557420d0a",
138
+ "@lodestar/config": "^1.44.0-dev.1557420d0a",
139
+ "@lodestar/db": "^1.44.0-dev.1557420d0a",
140
+ "@lodestar/fork-choice": "^1.44.0-dev.1557420d0a",
141
+ "@lodestar/logger": "^1.44.0-dev.1557420d0a",
142
+ "@lodestar/params": "^1.44.0-dev.1557420d0a",
143
+ "@lodestar/reqresp": "^1.44.0-dev.1557420d0a",
144
+ "@lodestar/state-transition": "^1.44.0-dev.1557420d0a",
145
+ "@lodestar/types": "^1.44.0-dev.1557420d0a",
146
+ "@lodestar/utils": "^1.44.0-dev.1557420d0a",
147
+ "@lodestar/validator": "^1.44.0-dev.1557420d0a",
149
148
  "@multiformats/multiaddr": "^13.0.1",
150
149
  "datastore-core": "^11.0.2",
151
150
  "datastore-fs": "^11.0.2",
@@ -168,13 +167,12 @@
168
167
  "@libp2p/interface-internal": "^3.0.13",
169
168
  "@libp2p/logger": "^6.2.2",
170
169
  "@libp2p/utils": "^7.0.13",
171
- "@lodestar/spec-test-util": "^1.44.0-dev.0e1ac44567",
170
+ "@lodestar/spec-test-util": "^1.44.0-dev.1557420d0a",
172
171
  "@types/js-yaml": "^4.0.5",
173
172
  "@types/qs": "^6.9.7",
174
173
  "@types/tmp": "^0.2.3",
175
174
  "dotenv": "^16.4.5",
176
175
  "js-yaml": "^4.1.0",
177
- "rewiremock": "^3.14.5",
178
176
  "rimraf": "^4.4.1",
179
177
  "snappy": "^7.2.2",
180
178
  "snappyjs": "^0.7.0",
@@ -186,5 +184,5 @@
186
184
  "beacon",
187
185
  "blockchain"
188
186
  ],
189
- "gitHead": "7f58518cbab06220cf04bcb0afa22f0be6991bd0"
187
+ "gitHead": "8dfc11795c5d10f3f174ef4922f90612ec5131b2"
190
188
  }
@@ -1,7 +1,8 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {ApplicationMethods} from "@lodestar/api/server";
3
- import {ExecutionStatus} from "@lodestar/fork-choice";
3
+ import {ExecutionStatus, PayloadStatus} from "@lodestar/fork-choice";
4
4
  import {ForkPostDeneb, ZERO_HASH_HEX, isForkPostDeneb, isForkPostFulu} from "@lodestar/params";
5
+ import {computeTimeAtSlot} from "@lodestar/state-transition";
5
6
  import {BeaconState, DataColumnSidecar, DataColumnSidecars, type SignedBeaconBlock, sszTypesFor} from "@lodestar/types";
6
7
  import {toRootHex} from "@lodestar/utils";
7
8
  import {getBlobKzgCommitments} from "../../../util/dataColumns.js";
@@ -12,6 +13,29 @@ import {getStateResponseWithRegen} from "../beacon/state/utils.js";
12
13
  import {ApiModules} from "../types.js";
13
14
  import {assertUniqueItems} from "../utils.js";
14
15
 
16
+ function toForkChoiceValidity(status: ExecutionStatus): "valid" | "invalid" | "optimistic" {
17
+ switch (status) {
18
+ case ExecutionStatus.Valid:
19
+ return "valid";
20
+ case ExecutionStatus.Invalid:
21
+ return "invalid";
22
+ case ExecutionStatus.Syncing:
23
+ case ExecutionStatus.PreMerge:
24
+ return "optimistic";
25
+ }
26
+ }
27
+
28
+ function toPayloadStatusName(status: PayloadStatus): "pending" | "empty" | "full" {
29
+ switch (status) {
30
+ case PayloadStatus.PENDING:
31
+ return "pending";
32
+ case PayloadStatus.EMPTY:
33
+ return "empty";
34
+ case PayloadStatus.FULL:
35
+ return "full";
36
+ }
37
+ }
38
+
15
39
  export function getDebugApi({
16
40
  chain,
17
41
  config,
@@ -40,23 +64,60 @@ export function getDebugApi({
40
64
  justifiedEpoch: node.justifiedEpoch,
41
65
  finalizedEpoch: node.finalizedEpoch,
42
66
  weight: node.weight,
43
- validity: (() => {
44
- switch (node.executionStatus) {
45
- case ExecutionStatus.Valid:
46
- return "valid";
47
- case ExecutionStatus.Invalid:
48
- return "invalid";
49
- case ExecutionStatus.Syncing:
50
- case ExecutionStatus.PreMerge:
51
- return "optimistic";
52
- }
53
- })(),
67
+ validity: toForkChoiceValidity(node.executionStatus),
54
68
  executionBlockHash: node.executionPayloadBlockHash ?? ZERO_HASH_HEX,
55
69
  })),
56
70
  },
57
71
  };
58
72
  },
59
73
 
74
+ async getDebugForkChoiceV2() {
75
+ const {forkChoice} = chain;
76
+ return {
77
+ data: {
78
+ justifiedCheckpoint: forkChoice.getJustifiedCheckpoint(),
79
+ finalizedCheckpoint: forkChoice.getFinalizedCheckpoint(),
80
+ forkChoiceNodes: forkChoice.getAllNodes().map((node) => {
81
+ // Payload-specific fields apply only to a revealed Gloas payload = the FULL variant of a
82
+ // Gloas block
83
+ const ptc = node.payloadStatus === PayloadStatus.FULL ? forkChoice.getPTCVoteCounts(node.blockRoot) : null;
84
+ return {
85
+ payloadStatus: toPayloadStatusName(node.payloadStatus),
86
+ slot: node.slot,
87
+ blockRoot: node.blockRoot,
88
+ parentRoot: node.parentRoot,
89
+ weight: node.weight,
90
+ validity: toForkChoiceValidity(node.executionStatus),
91
+ executionBlockHash: node.executionPayloadBlockHash ?? ZERO_HASH_HEX,
92
+ extraData: {
93
+ executionOptimistic: isOptimisticBlock(node),
94
+ timestamp: computeTimeAtSlot(config, node.slot, chain.genesisTime),
95
+ target: node.targetRoot,
96
+ justifiedEpoch: node.justifiedEpoch,
97
+ finalizedEpoch: node.finalizedEpoch,
98
+ unrealizedJustifiedEpoch: node.unrealizedJustifiedEpoch,
99
+ unrealizedFinalizedEpoch: node.unrealizedFinalizedEpoch,
100
+ payloadAttesterCount: ptc?.attesterCount ?? null,
101
+ payloadAvailabilityYesCount: ptc?.payloadPresentCount ?? null,
102
+ payloadDataAvailabilityYesCount: ptc?.dataAvailableCount ?? null,
103
+ gasLimit:
104
+ node.payloadStatus === PayloadStatus.FULL && "executionPayloadGasLimit" in node
105
+ ? node.executionPayloadGasLimit
106
+ : null,
107
+ },
108
+ };
109
+ }),
110
+ extraData: {
111
+ unrealizedJustifiedCheckpoint: forkChoice.getUnrealizedJustifiedCheckpoint(),
112
+ unrealizedFinalizedCheckpoint: forkChoice.getUnrealizedFinalizedCheckpoint(),
113
+ proposerBoostRoot: forkChoice.getProposerBoostRoot(),
114
+ previousProposerBoostRoot: forkChoice.getPreviousProposerBoostRoot(),
115
+ headRoot: forkChoice.getHeadRoot(),
116
+ },
117
+ },
118
+ };
119
+ },
120
+
60
121
  async getProtoArrayNodes() {
61
122
  const nodes = chain.forkChoice.getAllNodes().map((node) => ({
62
123
  // if node has executionPayloadNumber, it will overwrite the below default
@@ -927,11 +927,9 @@ export function getValidatorApi(
927
927
  // TODO GLOAS: respect builderSelection (MaxProfit, BuilderAlways, ExecutionAlways, etc.) to let
928
928
  // the user control bid source preferences and value comparison. Also add external builder api
929
929
  // support when it is implemented.
930
- const builderBid = chain.executionPayloadBidPool.getBestBid(
931
- slot,
932
- parentBlock.executionPayloadBlockHash,
933
- parentBlockRootHex
934
- );
930
+ const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot);
931
+ const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash;
932
+ const builderBid = chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex);
935
933
 
936
934
  const logCtx = {
937
935
  slot,
@@ -1112,23 +1110,34 @@ export function getValidatorApi(
1112
1110
  notWhileSyncing();
1113
1111
  await waitForSlot(slot);
1114
1112
 
1115
- const block = chain.forkChoice.getCanonicalBlockClosestLteSlot(slot);
1113
+ const block = chain.forkChoice.getCanonicalBlockAtSlot(slot);
1116
1114
  if (!block) {
1117
- throw new ApiError(404, `No canonical block found at or before slot=${slot}`);
1115
+ // No block is seen at slot. Return 404 so vc can skip casting payload attestation.
1116
+ throw new ApiError(404, `No canonical block found at slot=${slot}`);
1118
1117
  }
1119
1118
 
1120
- const blockIsForSlot = block.slot === slot;
1121
1119
  const payloadInput = chain.seenPayloadEnvelopeInputCache.get(block.blockRoot);
1122
1120
  // Spec: set payload_present only if the envelope was seen before get_payload_due_ms()
1123
1121
  // into the slot. Use the envelope's own arrival time (getPayloadEnvelopeSource), not
1124
1122
  // the input's creation time.
1125
1123
  const payloadDueSec = config.getPayloadDueMs() / 1000;
1126
- const payloadPresent =
1127
- blockIsForSlot &&
1128
- payloadInput !== undefined &&
1129
- payloadInput.hasPayloadEnvelope() &&
1130
- chain.clock.secFromSlot(slot, payloadInput.getPayloadEnvelopeSource().seenTimestampSec) < payloadDueSec;
1131
- const blobDataAvailable = blockIsForSlot && (payloadInput?.hasAllData() ?? false);
1124
+ const payloadSeenSec =
1125
+ payloadInput?.hasPayloadEnvelope() === true
1126
+ ? chain.clock.secFromSlot(slot, payloadInput.getPayloadEnvelopeSource().seenTimestampSec)
1127
+ : null;
1128
+ const payloadPresent = payloadSeenSec !== null && payloadSeenSec < payloadDueSec;
1129
+ const blobDataAvailable = payloadInput?.hasAllData() === true;
1130
+
1131
+ logger.debug("Produced payload attestation data", {
1132
+ slot,
1133
+ blockRoot: block.blockRoot,
1134
+ blockSlot: block.slot,
1135
+ payloadPresent,
1136
+ blobDataAvailable,
1137
+ hasPayloadInput: payloadInput !== undefined,
1138
+ payloadSeenSec,
1139
+ payloadDueSec,
1140
+ });
1132
1141
 
1133
1142
  return {
1134
1143
  data: {
@@ -5,6 +5,7 @@ export enum PayloadAttestationErrorCode {
5
5
  NOT_CURRENT_SLOT = "PAYLOAD_ATTESTATION_ERROR_NOT_CURRENT_SLOT",
6
6
  PAYLOAD_ATTESTATION_ALREADY_KNOWN = "PAYLOAD_ATTESTATION_ERROR_PAYLOAD_ATTESTATION_ALREADY_KNOWN",
7
7
  UNKNOWN_BLOCK_ROOT = "PAYLOAD_ATTESTATION_ERROR_UNKNOWN_BLOCK_ROOT",
8
+ INVALID_BLOCK_SLOT = "PAYLOAD_ATTESTATION_ERROR_INVALID_BLOCK_SLOT",
8
9
  INVALID_BLOCK = "PAYLOAD_ATTESTATION_ERROR_INVALID_BLOCK",
9
10
  INVALID_ATTESTER = "PAYLOAD_ATTESTATION_ERROR_INVALID_ATTESTER",
10
11
  INVALID_SIGNATURE = "PAYLOAD_ATTESTATION_ERROR_INVALID_SIGNATURE",
@@ -18,6 +19,7 @@ export type PayloadAttestationErrorType =
18
19
  blockRoot: RootHex;
19
20
  }
20
21
  | {code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT; blockRoot: RootHex}
22
+ | {code: PayloadAttestationErrorCode.INVALID_BLOCK_SLOT; blockRoot: RootHex; blockSlot: Slot; slot: Slot}
21
23
  | {code: PayloadAttestationErrorCode.INVALID_BLOCK; blockRoot: RootHex}
22
24
  | {code: PayloadAttestationErrorCode.INVALID_ATTESTER; attesterIndex: ValidatorIndex}
23
25
  | {code: PayloadAttestationErrorCode.INVALID_SIGNATURE};
@@ -170,7 +170,7 @@ export class PrepareNextSlotScheduler {
170
170
  let stateAfterParentPayload: IBeaconStateViewBellatrix = updatedPrepareState;
171
171
  if (isStatePostGloas(updatedPrepareState)) {
172
172
  // Spec: should_build_on_full(store, head) — see produceBlockBody.ts for context.
173
- if (this.chain.forkChoice.shouldBuildOnFull(updatedHead)) {
173
+ if (this.chain.forkChoice.shouldBuildOnFull(updatedHead, prepareSlot)) {
174
174
  parentBlockHash = updatedPrepareState.latestExecutionPayloadBid.blockHash;
175
175
  // Skip applying parent payload unless we're proposing the next slot or have to emit payload_attributes events
176
176
  if (feeRecipient !== undefined || this.chain.opts.emitPayloadAttributes === true) {
@@ -276,9 +276,9 @@ export async function produceBlockBody<T extends BlockType>(
276
276
  // Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below
277
277
  let stateAfterParentPayload: IBeaconStateViewBellatrix = currentState;
278
278
  // Spec: should_build_on_full(store, head). `parentBlock` is the proposer's head
279
- // (set by chain.getProposerHead(slot)). Returns false when the PTC majority
280
- // signalled the blob data is not available, forcing a build on EMPTY (reorg).
281
- const isBuildingOnFull = this.forkChoice.shouldBuildOnFull(parentBlock);
279
+ // (set by chain.getProposerHead(slot)). Returns false when the PTC majority signalled
280
+ // the blob data is not available or the payload was not timely, forcing a build on EMPTY (reorg).
281
+ const isBuildingOnFull = this.forkChoice.shouldBuildOnFull(parentBlock, blockSlot);
282
282
  if (isBuildingOnFull) {
283
283
  parentBlockHash = currentState.latestExecutionPayloadBid.blockHash;
284
284
  parentExecutionRequests = await this.getParentExecutionRequests(parentBlock.slot, parentBlock.blockRoot);
@@ -22,6 +22,7 @@ export enum RegenCaller {
22
22
  validateGossipAttestation = "validateGossipAttestation",
23
23
  validateGossipVoluntaryExit = "validateGossipVoluntaryExit",
24
24
  validateGossipExecutionPayloadBid = "validateGossipExecutionPayloadBid",
25
+ validateGossipPayloadAttestationMessage = "validateGossipPayloadAttestationMessage",
25
26
  validateGossipProposerPreferences = "validateGossipProposerPreferences",
26
27
  onForkChoiceFinalized = "onForkChoiceFinalized",
27
28
  restApi = "restApi",
@@ -35,10 +35,6 @@ async function validateExecutionPayloadBid(
35
35
  const bid = signedExecutionPayloadBid.message;
36
36
  const parentBlockRootHex = toRootHex(bid.parentBlockRoot);
37
37
  const parentBlockHashHex = toRootHex(bid.parentBlockHash);
38
- const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.validateGossipExecutionPayloadBid);
39
- if (!isStatePostGloas(state)) {
40
- throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
41
- }
42
38
 
43
39
  // [IGNORE] `bid.slot` is the current slot or the next slot.
44
40
  const currentSlot = chain.clock.currentSlot;
@@ -111,9 +107,31 @@ async function validateExecutionPayloadBid(
111
107
  });
112
108
  }
113
109
 
110
+ // Use the bid's parent branch state for builder checks
111
+ const state = await chain.regen
112
+ .getBlockSlotState(parentBlock, bid.slot, {dontTransferCache: true}, RegenCaller.validateGossipExecutionPayloadBid)
113
+ .catch(() => {
114
+ throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
115
+ code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
116
+ parentBlockRoot: parentBlockRootHex,
117
+ });
118
+ });
119
+
120
+ if (!isStatePostGloas(state)) {
121
+ throw new Error(`Expected gloas+ state for execution payload bid validation, got fork=${state.forkName}`);
122
+ }
123
+
114
124
  // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
115
125
  // `is_active_builder(state, bid.builder_index)` returns `True`.
116
- const builder = state.getBuilder(bid.builderIndex);
126
+ let builder: gloas.Builder;
127
+ try {
128
+ builder = state.getBuilder(bid.builderIndex);
129
+ } catch {
130
+ throw new ExecutionPayloadBidError(GossipAction.REJECT, {
131
+ code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
132
+ builderIndex: bid.builderIndex,
133
+ });
134
+ }
117
135
  if (!isActiveBuilder(builder, state.finalizedCheckpoint.epoch)) {
118
136
  throw new ExecutionPayloadBidError(GossipAction.REJECT, {
119
137
  code: ExecutionPayloadBidErrorCode.BUILDER_NOT_ELIGIBLE,
@@ -8,6 +8,7 @@ import {RootHex, gloas, ssz} from "@lodestar/types";
8
8
  import {toRootHex} from "@lodestar/utils";
9
9
  import {GossipAction, PayloadAttestationError, PayloadAttestationErrorCode} from "../errors/index.js";
10
10
  import {IBeaconChain} from "../index.js";
11
+ import {RegenCaller} from "../regen/index.js";
11
12
 
12
13
  export type PayloadAttestationValidationResult = {
13
14
  attDataRootHex: RootHex;
@@ -61,22 +62,43 @@ async function validatePayloadAttestationMessage(
61
62
  // [IGNORE] The message's block `data.beacon_block_root` has been seen (via
62
63
  // gossip or non-gossip sources) (a client MAY queue attestation for processing
63
64
  // once the block is retrieved. Note a client might want to request payload after).
64
- if (!chain.forkChoice.hasBlock(data.beaconBlockRoot)) {
65
+ const block = chain.forkChoice.getBlockDefaultStatus(data.beaconBlockRoot);
66
+ if (!block) {
65
67
  throw new PayloadAttestationError(GossipAction.IGNORE, {
66
68
  code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT,
67
69
  blockRoot: toRootHex(data.beaconBlockRoot),
68
70
  });
69
71
  }
70
72
 
71
- const state = chain.getHeadState();
72
- if (!isStatePostGloas(state)) {
73
- throw new Error(`Expected gloas+ state for payload attestation validation, got fork=${state.forkName}`);
73
+ // [IGNORE] The block referenced by `data.beacon_block_root` is at slot `data.slot`,
74
+ // i.e. the block has `block.slot == data.slot`.
75
+ if (block.slot !== data.slot) {
76
+ throw new PayloadAttestationError(GossipAction.IGNORE, {
77
+ code: PayloadAttestationErrorCode.INVALID_BLOCK_SLOT,
78
+ blockRoot: toRootHex(data.beaconBlockRoot),
79
+ blockSlot: block.slot,
80
+ slot: data.slot,
81
+ });
74
82
  }
75
83
 
76
84
  // [REJECT] The message's block `data.beacon_block_root` passes validation.
77
85
  // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice,
78
86
  // it is possible that the block didn't pass the validation
79
87
 
88
+ // Use the referenced block's branch state for the PTC committee check
89
+ const state = await chain.regen
90
+ .getBlockSlotState(block, data.slot, {dontTransferCache: true}, RegenCaller.validateGossipPayloadAttestationMessage)
91
+ .catch(() => {
92
+ throw new PayloadAttestationError(GossipAction.IGNORE, {
93
+ code: PayloadAttestationErrorCode.UNKNOWN_BLOCK_ROOT,
94
+ blockRoot: toRootHex(data.beaconBlockRoot),
95
+ });
96
+ });
97
+
98
+ if (!isStatePostGloas(state)) {
99
+ throw new Error(`Expected gloas+ state for payload attestation validation, got fork=${state.forkName}`);
100
+ }
101
+
80
102
  // [REJECT] The message's validator index is within the payload committee in
81
103
  // `get_ptc(state, data.slot)`. The `state` is the head state corresponding to
82
104
  // processing the block up to the current slot as determined by the fork choice.
@@ -1,5 +1,6 @@
1
1
  import {PeerId} from "@libp2p/interface";
2
2
  import {ChainConfig} from "@lodestar/config";
3
+ import {PayloadStatus} from "@lodestar/fork-choice";
3
4
  import {ForkSeq, GENESIS_SLOT} from "@lodestar/params";
4
5
  import {RespStatus, ResponseError, ResponseOutgoing} from "@lodestar/reqresp";
5
6
  import {computeEpochAtSlot} from "@lodestar/state-transition";
@@ -104,6 +105,11 @@ export async function* onDataColumnSidecarsByRange(
104
105
 
105
106
  // Must include only columns in the range requested
106
107
  if (block.slot > archiveMaxSlot && block.slot >= startSlot && block.slot < endSlot) {
108
+ // Post-gloas, columns exist only for FULL blocks (pre-gloas blocks are always FULL)
109
+ if (block.payloadStatus !== PayloadStatus.FULL) {
110
+ continue;
111
+ }
112
+
107
113
  // Note: Here the forkChoice head may change due to a re-org, so the headChain reflects the canonical chain
108
114
  // at the time of the start of the request. Spec is clear the chain of columns must be consistent, but on
109
115
  // re-org there's no need to abort the request
@@ -30,7 +30,7 @@ export async function* onDataColumnSidecarsByRoot(
30
30
  const {blockRoot, columns: requestedColumns} = dataColumnsByRootIdentifier;
31
31
  const availableColumns = validateRequestedDataColumns(chain, requestedColumns);
32
32
  if (availableColumns.length === 0) {
33
- return;
33
+ continue;
34
34
  }
35
35
 
36
36
  const blockRootHex = toRootHex(blockRoot);
@@ -20,9 +20,9 @@ export async function* onExecutionPayloadEnvelopesByRange(
20
20
  }
21
21
 
22
22
  const finalized = db.executionPayloadEnvelopeArchive;
23
- const finalizedSlot = chain.forkChoice.getFinalizedCheckpointSlot();
24
- // The current finalized block's envelope is still in the hot db; archive migration happens
25
- // in the next finalization run (see migrateExecutionPayloadEnvelopesFromHotToColdDb).
23
+ // Use the finalized block's actual slot as the checkpoint epoch-boundary slot may be skipped
24
+ const finalizedSlot = chain.forkChoice.getFinalizedBlock().slot;
25
+ // The finalized block's envelope stays in the hot db until the next finalization run
26
26
  const archiveMaxSlot = finalizedSlot - 1;
27
27
 
28
28
  // Finalized range of envelopes
@@ -1,14 +1,18 @@
1
+ import {PeerId} from "@libp2p/interface";
1
2
  import {ResponseOutgoing} from "@lodestar/reqresp";
2
3
  import {computeEpochAtSlot} from "@lodestar/state-transition";
3
4
  import {toRootHex} from "@lodestar/utils";
4
5
  import {IBeaconChain} from "../../../chain/index.js";
5
6
  import {IBeaconDb} from "../../../db/index.js";
6
7
  import {ExecutionPayloadEnvelopesByRootRequest} from "../../../util/types.js";
8
+ import {prettyPrintPeerId} from "../../util.js";
7
9
 
8
10
  export async function* onExecutionPayloadEnvelopesByRoot(
9
11
  requestBody: ExecutionPayloadEnvelopesByRootRequest,
10
12
  chain: IBeaconChain,
11
- db: IBeaconDb
13
+ db: IBeaconDb,
14
+ peerId: PeerId,
15
+ peerClient: string
12
16
  ): AsyncIterable<ResponseOutgoing> {
13
17
  // The gloas req/resp spec uses MIN_EPOCHS_FOR_BLOCK_REQUESTS to define the minimum range peers MUST serve.
14
18
  // Archival nodes may still serve older retained payloads to allow genesis sync.
@@ -20,6 +24,14 @@ export async function* onExecutionPayloadEnvelopesByRoot(
20
24
  const slot = block ? block.slot : await db.blockArchive.getSlotByRoot(root);
21
25
 
22
26
  if (slot === null) {
27
+ chain.logger.debug(
28
+ "Cannot serve ExecutionPayloadEnvelopesByRoot: block root not in fork choice or block archive",
29
+ {
30
+ root: rootHex,
31
+ peer: prettyPrintPeerId(peerId),
32
+ client: peerClient,
33
+ }
34
+ );
23
35
  continue;
24
36
  }
25
37
 
@@ -29,6 +41,13 @@ export async function* onExecutionPayloadEnvelopesByRoot(
29
41
  data: envelopeBytes,
30
42
  boundary: chain.config.getForkBoundaryAtEpoch(computeEpochAtSlot(slot)),
31
43
  };
44
+ } else {
45
+ chain.logger.debug("Cannot serve ExecutionPayloadEnvelopesByRoot: envelope not found", {
46
+ slot,
47
+ root: rootHex,
48
+ peer: prettyPrintPeerId(peerId),
49
+ client: peerClient,
50
+ });
32
51
  }
33
52
  }
34
53
  }
@@ -70,9 +70,9 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh
70
70
  return onDataColumnSidecarsByRoot(body, chain, db, peerId, peerClient);
71
71
  },
72
72
 
73
- [ReqRespMethod.ExecutionPayloadEnvelopesByRoot]: (req) => {
73
+ [ReqRespMethod.ExecutionPayloadEnvelopesByRoot]: (req, peerId, peerClient) => {
74
74
  const body = ExecutionPayloadEnvelopesByRootRequestType(chain.config).deserialize(req.data);
75
- return onExecutionPayloadEnvelopesByRoot(body, chain, db);
75
+ return onExecutionPayloadEnvelopesByRoot(body, chain, db, peerId, peerClient);
76
76
  },
77
77
  [ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req) => {
78
78
  const body = ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest.deserialize(req.data);
@@ -1,4 +1,5 @@
1
1
  import {LogData} from "@lodestar/logger";
2
+ import {ForkSeq} from "@lodestar/params";
2
3
  import {RespStatus, ResponseError} from "@lodestar/reqresp";
3
4
  import {ColumnIndex, Slot} from "@lodestar/types";
4
5
  import {prettyBytes, prettyPrintIndices, toRootHex} from "@lodestar/utils";
@@ -38,6 +39,13 @@ export async function handleColumnSidecarUnavailability({
38
39
 
39
40
  chain.logger.debug("dataColumnSidecar requested unavailable", logData);
40
41
 
42
+ // Post-gloas, columns exist only for FULL blocks; a finalized block is FULL if its envelope was
43
+ // archived. Bid blobsCount is unreliable here since an EMPTY block's bid may still commit to blobs
44
+ if (blockRoot === undefined && chain.config.getForkSeq(slot) >= ForkSeq.gloas) {
45
+ const envelopeBytes = await db.executionPayloadEnvelopeArchive.getBinary(slot);
46
+ if (!envelopeBytes) return;
47
+ }
48
+
41
49
  const blockBytes = blockRoot ? await db.block.getBinary(blockRoot) : await db.blockArchive.getBinary(slot);
42
50
  if (!blockBytes) {
43
51
  chain.logger.verbose(