@lodestar/beacon-node 1.44.0-dev.1070262602 → 1.44.0-dev.16e0233677

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 (82) hide show
  1. package/lib/api/impl/config/constants.d.ts +1 -0
  2. package/lib/api/impl/config/constants.d.ts.map +1 -1
  3. package/lib/api/impl/config/constants.js +2 -1
  4. package/lib/api/impl/config/constants.js.map +1 -1
  5. package/lib/api/impl/debug/index.d.ts.map +1 -1
  6. package/lib/api/impl/debug/index.js +69 -12
  7. package/lib/api/impl/debug/index.js.map +1 -1
  8. package/lib/api/impl/validator/index.d.ts.map +1 -1
  9. package/lib/api/impl/validator/index.js +21 -9
  10. package/lib/api/impl/validator/index.js.map +1 -1
  11. package/lib/chain/archiveStore/archiveStore.d.ts +0 -1
  12. package/lib/chain/archiveStore/archiveStore.d.ts.map +1 -1
  13. package/lib/chain/archiveStore/archiveStore.js +0 -4
  14. package/lib/chain/archiveStore/archiveStore.js.map +1 -1
  15. package/lib/chain/errors/payloadAttestation.d.ts +6 -0
  16. package/lib/chain/errors/payloadAttestation.d.ts.map +1 -1
  17. package/lib/chain/errors/payloadAttestation.js +1 -0
  18. package/lib/chain/errors/payloadAttestation.js.map +1 -1
  19. package/lib/chain/prepareNextSlot.js +1 -1
  20. package/lib/chain/prepareNextSlot.js.map +1 -1
  21. package/lib/chain/produceBlock/produceBlockBody.js +3 -3
  22. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  23. package/lib/chain/regen/interface.d.ts +0 -1
  24. package/lib/chain/regen/interface.d.ts.map +1 -1
  25. package/lib/chain/regen/queued.d.ts +0 -1
  26. package/lib/chain/regen/queued.d.ts.map +1 -1
  27. package/lib/chain/regen/queued.js +0 -4
  28. package/lib/chain/regen/queued.js.map +1 -1
  29. package/lib/chain/stateCache/fifoBlockStateCache.d.ts +0 -5
  30. package/lib/chain/stateCache/fifoBlockStateCache.d.ts.map +1 -1
  31. package/lib/chain/stateCache/fifoBlockStateCache.js +0 -5
  32. package/lib/chain/stateCache/fifoBlockStateCache.js.map +1 -1
  33. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts +1 -4
  34. package/lib/chain/stateCache/persistentCheckpointsCache.d.ts.map +1 -1
  35. package/lib/chain/stateCache/persistentCheckpointsCache.js +5 -2
  36. package/lib/chain/stateCache/persistentCheckpointsCache.js.map +1 -1
  37. package/lib/chain/stateCache/types.d.ts +0 -2
  38. package/lib/chain/stateCache/types.d.ts.map +1 -1
  39. package/lib/chain/stateCache/types.js.map +1 -1
  40. package/lib/chain/validation/executionPayloadBid.js +22 -5
  41. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  42. package/lib/chain/validation/payloadAttestationMessage.js +10 -0
  43. package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
  44. package/lib/network/gossip/topic.d.ts +749 -2
  45. package/lib/network/gossip/topic.d.ts.map +1 -1
  46. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.d.ts.map +1 -1
  47. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js +5 -0
  48. package/lib/network/reqresp/handlers/dataColumnSidecarsByRange.js.map +1 -1
  49. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js +1 -1
  50. package/lib/network/reqresp/handlers/dataColumnSidecarsByRoot.js.map +1 -1
  51. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +3 -3
  52. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  53. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts +2 -1
  54. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.d.ts.map +1 -1
  55. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js +15 -1
  56. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRoot.js.map +1 -1
  57. package/lib/network/reqresp/handlers/index.js +2 -2
  58. package/lib/network/reqresp/handlers/index.js.map +1 -1
  59. package/lib/network/reqresp/utils/dataColumnResponseValidation.d.ts.map +1 -1
  60. package/lib/network/reqresp/utils/dataColumnResponseValidation.js +22 -3
  61. package/lib/network/reqresp/utils/dataColumnResponseValidation.js.map +1 -1
  62. package/package.json +14 -16
  63. package/src/api/impl/config/constants.ts +2 -0
  64. package/src/api/impl/debug/index.ts +73 -12
  65. package/src/api/impl/validator/index.ts +23 -14
  66. package/src/chain/archiveStore/archiveStore.ts +0 -5
  67. package/src/chain/errors/payloadAttestation.ts +2 -0
  68. package/src/chain/prepareNextSlot.ts +1 -1
  69. package/src/chain/produceBlock/produceBlockBody.ts +3 -3
  70. package/src/chain/regen/interface.ts +0 -1
  71. package/src/chain/regen/queued.ts +0 -5
  72. package/src/chain/stateCache/fifoBlockStateCache.ts +0 -6
  73. package/src/chain/stateCache/persistentCheckpointsCache.ts +6 -2
  74. package/src/chain/stateCache/types.ts +0 -2
  75. package/src/chain/validation/executionPayloadBid.ts +23 -5
  76. package/src/chain/validation/payloadAttestationMessage.ts +11 -0
  77. package/src/network/reqresp/handlers/dataColumnSidecarsByRange.ts +6 -0
  78. package/src/network/reqresp/handlers/dataColumnSidecarsByRoot.ts +1 -1
  79. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +3 -3
  80. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRoot.ts +20 -1
  81. package/src/network/reqresp/handlers/index.ts +2 -2
  82. package/src/network/reqresp/utils/dataColumnResponseValidation.ts +21 -3
@@ -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);
@@ -47,7 +47,6 @@ export interface IStateRegenerator extends IStateRegeneratorInternal {
47
47
  getCheckpointStateSync(cp: CheckpointHex): IBeaconStateView | null;
48
48
  getClosestHeadState(head: ProtoBlock): IBeaconStateView | null;
49
49
  pruneOnCheckpoint(finalizedEpoch: Epoch, justifiedEpoch: Epoch, headStateRoot: RootHex): void;
50
- pruneOnFinalized(finalizedEpoch: Epoch): void;
51
50
  processState(blockRootHex: RootHex, postState: IBeaconStateView): void;
52
51
  addCheckpointState(cp: phase0.Checkpoint, item: IBeaconStateView): void;
53
52
  updateHeadState(newHead: ProtoBlock, maybeHeadState: IBeaconStateView): void;
@@ -143,11 +143,6 @@ export class QueuedStateRegenerator implements IStateRegenerator {
143
143
  this.blockStateCache.prune(headStateRoot);
144
144
  }
145
145
 
146
- pruneOnFinalized(finalizedEpoch: number): void {
147
- this.checkpointStateCache.pruneFinalized(finalizedEpoch);
148
- this.blockStateCache.deleteAllBeforeEpoch(finalizedEpoch);
149
- }
150
-
151
146
  processState(blockRootHex: RootHex, postState: IBeaconStateView): void {
152
147
  this.blockStateCache.add(postState);
153
148
  this.checkpointStateCache.processState(blockRootHex, postState).catch((e) => {
@@ -167,12 +167,6 @@ export class FIFOBlockStateCache implements BlockStateCache {
167
167
  }
168
168
  }
169
169
 
170
- /**
171
- * No need for this implementation
172
- * This is only to conform to the old api
173
- */
174
- deleteAllBeforeEpoch(): void {}
175
-
176
170
  /**
177
171
  * ONLY FOR DEBUGGING PURPOSES. For lodestar debug API.
178
172
  */
@@ -414,11 +414,12 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
414
414
 
415
415
  /**
416
416
  * Prune all checkpoint states before the provided finalized epoch.
417
+ * Driven sequentially from processState() so it never interleaves with persist.
417
418
  */
418
- pruneFinalized(finalizedEpoch: Epoch): void {
419
+ private async pruneFinalized(finalizedEpoch: Epoch): Promise<void> {
419
420
  for (const epoch of this.epochIndex.keys()) {
420
421
  if (epoch < finalizedEpoch) {
421
- this.deleteAllEpochItems(epoch).catch((e) =>
422
+ await this.deleteAllEpochItems(epoch).catch((e) =>
422
423
  this.logger.debug("Error delete all epoch items", {epoch, finalizedEpoch}, e as Error)
423
424
  );
424
425
  }
@@ -476,6 +477,9 @@ export class PersistentCheckpointStateCache implements CheckpointStateCache {
476
477
  * As of Mar 2024, it takes <=350ms to persist a holesky state on fast server
477
478
  */
478
479
  async processState(blockRootHex: RootHex, state: IBeaconStateView): Promise<number> {
480
+ // prune finalized in the same flow so a finalized cp state is pruned, never persisted
481
+ await this.pruneFinalized(state.finalizedCheckpoint.epoch);
482
+
479
483
  let persistCount = 0;
480
484
  // it's important to sort the epochs in ascending order, in case of big reorg we always want to keep the most recent checkpoint states
481
485
  const sortedEpochs = Array.from(this.epochIndex.keys()).sort((a, b) => a - b);
@@ -30,7 +30,6 @@ export interface BlockStateCache {
30
30
  clear(): void;
31
31
  size: number;
32
32
  prune(headStateRootHex: RootHex): void;
33
- deleteAllBeforeEpoch(finalizedEpoch: Epoch): void;
34
33
  dumpSummary(): routes.lodestar.StateCacheItem[];
35
34
  /** Expose beacon states stored in cache. Use with caution */
36
35
  getStates(): IterableIterator<IBeaconStateView>;
@@ -67,7 +66,6 @@ export interface CheckpointStateCache {
67
66
  getOrReloadLatest(rootHex: RootHex, maxEpoch: Epoch): Promise<IBeaconStateView | null>;
68
67
  updatePreComputedCheckpoint(rootHex: RootHex, epoch: Epoch): number | null;
69
68
  prune(finalizedEpoch: Epoch, justifiedEpoch: Epoch): void;
70
- pruneFinalized(finalizedEpoch: Epoch): void;
71
69
  processState(blockRootHex: RootHex, state: IBeaconStateView): Promise<number>;
72
70
  clear(): void;
73
71
  dumpSummary(): routes.lodestar.StateCacheItem[];
@@ -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,
@@ -70,6 +70,17 @@ async function validatePayloadAttestationMessage(
70
70
  });
71
71
  }
72
72
 
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
+ });
82
+ }
83
+
73
84
  // [REJECT] The message's block `data.beacon_block_root` passes validation.
74
85
  // TODO GLOAS: implement this. Technically if we cannot get proto block from fork choice,
75
86
  // it is possible that the block didn't pass the validation
@@ -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(
@@ -71,9 +79,19 @@ export function validateRequestedDataColumns(chain: IBeaconChain, requestedColum
71
79
  throw new ResponseError(RespStatus.INVALID_REQUEST, "dataColumnSidecar requested without column indices");
72
80
  }
73
81
 
74
- const custodyColumns = chain.custodyConfig.custodyColumns;
75
- const availableColumns = requestedColumns.filter((c) => custodyColumns.includes(c));
76
- const missingColumns = requestedColumns.filter((c) => !custodyColumns.includes(c));
82
+ const {custodyColumns, custodyColumnsIndex} = chain.custodyConfig;
83
+ const availableColumns: ColumnIndex[] = [];
84
+ const missingColumns: ColumnIndex[] = [];
85
+ for (const c of requestedColumns) {
86
+ // `c` is peer-controlled and SSZ-deserialized as `uint64`, so it may exceed
87
+ // `NUMBER_OF_COLUMNS - 1`; `Uint8Array` returns `undefined` for OOB reads,
88
+ // and `undefined !== 0` would silently classify OOB indices as custodied.
89
+ if ((custodyColumnsIndex[c] ?? 0) !== 0) {
90
+ availableColumns.push(c);
91
+ } else {
92
+ missingColumns.push(c);
93
+ }
94
+ }
77
95
 
78
96
  if (missingColumns.length > 0) {
79
97
  chain.logger.verbose("Requested dataColumnSidecar for non-custody columns", {