@lodestar/beacon-node 1.43.0-dev.a45ba75824 → 1.43.0-dev.a58ad80359

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 (156) hide show
  1. package/lib/api/impl/debug/index.d.ts.map +1 -1
  2. package/lib/api/impl/debug/index.js +0 -1
  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 +2 -1
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/chain/blocks/blockInput/blockInput.d.ts +3 -0
  8. package/lib/chain/blocks/blockInput/blockInput.d.ts.map +1 -1
  9. package/lib/chain/blocks/blockInput/blockInput.js +4 -1
  10. package/lib/chain/blocks/blockInput/blockInput.js.map +1 -1
  11. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  12. package/lib/chain/blocks/importBlock.js +14 -18
  13. package/lib/chain/blocks/importBlock.js.map +1 -1
  14. package/lib/chain/blocks/importExecutionPayload.d.ts +4 -0
  15. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  16. package/lib/chain/blocks/importExecutionPayload.js +15 -6
  17. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  18. package/lib/chain/blocks/index.d.ts.map +1 -1
  19. package/lib/chain/blocks/index.js +29 -17
  20. package/lib/chain/blocks/index.js.map +1 -1
  21. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +11 -1
  22. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  23. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +23 -1
  24. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  25. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts +16 -0
  26. package/lib/chain/blocks/payloadEnvelopeInput/types.d.ts.map +1 -1
  27. package/lib/chain/blocks/types.d.ts +2 -1
  28. package/lib/chain/blocks/types.d.ts.map +1 -1
  29. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  30. package/lib/chain/blocks/utils/chainSegment.js +8 -0
  31. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  32. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  33. package/lib/chain/blocks/verifyBlock.js +5 -6
  34. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  35. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  36. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  37. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  38. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  39. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  40. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  41. package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
  42. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  43. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  44. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  45. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +10 -6
  46. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  47. package/lib/chain/chain.d.ts.map +1 -1
  48. package/lib/chain/chain.js +25 -10
  49. package/lib/chain/chain.js.map +1 -1
  50. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  51. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  52. package/lib/chain/errors/proposerPreferences.js +1 -0
  53. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  54. package/lib/chain/initState.d.ts.map +1 -1
  55. package/lib/chain/initState.js +6 -1
  56. package/lib/chain/initState.js.map +1 -1
  57. package/lib/chain/prepareNextSlot.js +1 -1
  58. package/lib/chain/prepareNextSlot.js.map +1 -1
  59. package/lib/chain/produceBlock/produceBlockBody.d.ts +1 -0
  60. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  61. package/lib/chain/produceBlock/produceBlockBody.js +2 -7
  62. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  63. package/lib/chain/regen/queued.d.ts.map +1 -1
  64. package/lib/chain/regen/queued.js +1 -4
  65. package/lib/chain/regen/queued.js.map +1 -1
  66. package/lib/chain/regen/regen.d.ts.map +1 -1
  67. package/lib/chain/regen/regen.js +1 -4
  68. package/lib/chain/regen/regen.js.map +1 -1
  69. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts +14 -10
  70. package/lib/chain/seenCache/seenPayloadEnvelopeInput.d.ts.map +1 -1
  71. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js +51 -17
  72. package/lib/chain/seenCache/seenPayloadEnvelopeInput.js.map +1 -1
  73. package/lib/chain/seenCache/seenProposerPreferences.d.ts +8 -7
  74. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  75. package/lib/chain/seenCache/seenProposerPreferences.js +11 -10
  76. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  77. package/lib/chain/validation/executionPayloadBid.js +11 -8
  78. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  79. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  80. package/lib/chain/validation/proposerPreferences.js +39 -17
  81. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  82. package/lib/network/gossip/topic.d.ts +2 -0
  83. package/lib/network/gossip/topic.d.ts.map +1 -1
  84. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  85. package/lib/network/processor/gossipHandlers.js +14 -0
  86. package/lib/network/processor/gossipHandlers.js.map +1 -1
  87. package/lib/node/nodejs.js +2 -2
  88. package/lib/node/nodejs.js.map +1 -1
  89. package/lib/node/notifier.js +1 -7
  90. package/lib/node/notifier.js.map +1 -1
  91. package/lib/sync/constants.d.ts +3 -1
  92. package/lib/sync/constants.d.ts.map +1 -1
  93. package/lib/sync/constants.js +3 -4
  94. package/lib/sync/constants.js.map +1 -1
  95. package/lib/sync/range/batch.d.ts +12 -3
  96. package/lib/sync/range/batch.d.ts.map +1 -1
  97. package/lib/sync/range/batch.js +163 -33
  98. package/lib/sync/range/batch.js.map +1 -1
  99. package/lib/sync/range/chain.d.ts +13 -2
  100. package/lib/sync/range/chain.d.ts.map +1 -1
  101. package/lib/sync/range/chain.js +38 -4
  102. package/lib/sync/range/chain.js.map +1 -1
  103. package/lib/sync/range/range.d.ts.map +1 -1
  104. package/lib/sync/range/range.js +14 -3
  105. package/lib/sync/range/range.js.map +1 -1
  106. package/lib/sync/sync.d.ts.map +1 -1
  107. package/lib/sync/sync.js +13 -0
  108. package/lib/sync/sync.js.map +1 -1
  109. package/lib/sync/unknownBlock.d.ts +7 -0
  110. package/lib/sync/unknownBlock.d.ts.map +1 -1
  111. package/lib/sync/unknownBlock.js +138 -10
  112. package/lib/sync/unknownBlock.js.map +1 -1
  113. package/lib/sync/utils/downloadByRange.d.ts +29 -8
  114. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  115. package/lib/sync/utils/downloadByRange.js +87 -31
  116. package/lib/sync/utils/downloadByRange.js.map +1 -1
  117. package/lib/util/sszBytes.d.ts.map +1 -1
  118. package/lib/util/sszBytes.js +8 -6
  119. package/lib/util/sszBytes.js.map +1 -1
  120. package/package.json +16 -16
  121. package/src/api/impl/debug/index.ts +0 -1
  122. package/src/api/impl/validator/index.ts +2 -1
  123. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  124. package/src/chain/blocks/importBlock.ts +14 -21
  125. package/src/chain/blocks/importExecutionPayload.ts +21 -11
  126. package/src/chain/blocks/index.ts +18 -9
  127. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +31 -2
  128. package/src/chain/blocks/payloadEnvelopeInput/types.ts +17 -0
  129. package/src/chain/blocks/types.ts +2 -1
  130. package/src/chain/blocks/utils/chainSegment.ts +8 -0
  131. package/src/chain/blocks/verifyBlock.ts +7 -5
  132. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  133. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
  134. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +14 -6
  135. package/src/chain/chain.ts +29 -9
  136. package/src/chain/errors/proposerPreferences.ts +9 -1
  137. package/src/chain/initState.ts +9 -1
  138. package/src/chain/prepareNextSlot.ts +1 -1
  139. package/src/chain/produceBlock/produceBlockBody.ts +3 -7
  140. package/src/chain/regen/queued.ts +2 -7
  141. package/src/chain/regen/regen.ts +2 -7
  142. package/src/chain/seenCache/seenPayloadEnvelopeInput.ts +66 -20
  143. package/src/chain/seenCache/seenProposerPreferences.ts +14 -11
  144. package/src/chain/validation/executionPayloadBid.ts +11 -8
  145. package/src/chain/validation/proposerPreferences.ts +37 -18
  146. package/src/network/processor/gossipHandlers.ts +18 -0
  147. package/src/node/nodejs.ts +2 -2
  148. package/src/node/notifier.ts +1 -8
  149. package/src/sync/constants.ts +4 -4
  150. package/src/sync/range/batch.ts +187 -38
  151. package/src/sync/range/chain.ts +52 -5
  152. package/src/sync/range/range.ts +16 -3
  153. package/src/sync/sync.ts +13 -1
  154. package/src/sync/unknownBlock.ts +170 -10
  155. package/src/sync/utils/downloadByRange.ts +149 -33
  156. package/src/util/sszBytes.ts +8 -6
@@ -3,12 +3,11 @@ import {
3
3
  computeEpochAtSlot,
4
4
  createSingleSignatureSetFromComponents,
5
5
  getProposerPreferencesSigningRoot,
6
- isStatePostGloas,
7
6
  } from "@lodestar/state-transition";
8
- import {gloas} from "@lodestar/types";
7
+ import {ValidatorIndex, gloas} from "@lodestar/types";
8
+ import {toRootHex} from "@lodestar/utils";
9
9
  import {GossipAction, ProposerPreferencesError, ProposerPreferencesErrorCode} from "../errors/index.js";
10
10
  import {IBeaconChain} from "../index.js";
11
- import {RegenCaller} from "../regen/index.js";
12
11
 
13
12
  /**
14
13
  * Validates a gossiped `SignedProposerPreferences` per
@@ -19,7 +18,8 @@ export async function validateGossipProposerPreferences(
19
18
  signedProposerPreferences: gloas.SignedProposerPreferences
20
19
  ): Promise<void> {
21
20
  const preferences = signedProposerPreferences.message;
22
- const {proposalSlot, validatorIndex} = preferences;
21
+ const {proposalSlot, validatorIndex, dependentRoot} = preferences;
22
+ const dependentRootHex = toRootHex(dependentRoot);
23
23
  const proposalEpoch = computeEpochAtSlot(proposalSlot);
24
24
 
25
25
  // [IGNORE] `preferences.proposal_slot` is in the current or next epoch.
@@ -42,32 +42,51 @@ export async function validateGossipProposerPreferences(
42
42
  });
43
43
  }
44
44
 
45
- const state = await chain.getHeadStateAtCurrentEpoch(RegenCaller.validateGossipProposerPreferences);
46
- if (!isStatePostGloas(state)) {
47
- throw new Error(`Expected gloas+ state for proposer preferences validation, got fork=${state.forkName}`);
45
+ // [IGNORE] The block with root `dependent_root` has been seen by the node.
46
+ // Resolve the proposer lookahead for the message's branch via head state (fast path) or
47
+ // the previous-root checkpoint state (populated by `processSlotsToNearestCheckpoint` for
48
+ // any imported branch crossing into `proposalEpoch - 1`). The head-state path also handles
49
+ // narrow timing windows where the checkpoint state isn't yet populated.
50
+ const headState = chain.getHeadState();
51
+ let proposers: ValidatorIndex[] | null = null;
52
+ if (headState.epoch === proposalEpoch && headState.currentDecisionRoot === dependentRootHex) {
53
+ proposers = headState.currentProposers;
54
+ } else if (headState.epoch === proposalEpoch - 1 && headState.nextDecisionRoot === dependentRootHex) {
55
+ proposers = headState.nextProposers;
56
+ } else {
57
+ // Sync lookup only to not trigger disk reload from gossip input.
58
+ const checkpointState = chain.regen.getCheckpointStateSync({epoch: proposalEpoch - 1, rootHex: dependentRootHex});
59
+ if (checkpointState !== null) {
60
+ // State is at `proposalEpoch - 1`, so proposers for `proposalSlot` (next epoch from
61
+ // the state's perspective) live in `nextProposers`.
62
+ proposers = checkpointState.nextProposers;
63
+ }
64
+ }
65
+ if (proposers === null) {
66
+ throw new ProposerPreferencesError(GossipAction.IGNORE, {
67
+ code: ProposerPreferencesErrorCode.UNKNOWN_DEPENDENT_ROOT,
68
+ proposalSlot,
69
+ dependentRoot: dependentRootHex,
70
+ });
48
71
  }
49
72
 
50
- // [REJECT] `preferences.validator_index` is present at the correct slot in the current or next
51
- // epoch's portion of `state.proposer_lookahead` i.e. `is_valid_proposal_slot(state, preferences)`
52
- // returns True.
53
- const epochOffset = proposalEpoch - state.epoch;
54
- const proposers = epochOffset === 0 ? state.currentProposers : state.nextProposers;
55
- const expectedProposer = proposers[proposalSlot % SLOTS_PER_EPOCH];
56
- if (epochOffset < 0 || epochOffset > 1 || expectedProposer !== validatorIndex) {
73
+ // [REJECT] `is_valid_proposal_slot(state, preferences)` returns True.
74
+ if (proposers[proposalSlot % SLOTS_PER_EPOCH] !== validatorIndex) {
57
75
  throw new ProposerPreferencesError(GossipAction.REJECT, {
58
76
  code: ProposerPreferencesErrorCode.INVALID_PROPOSER,
59
77
  proposalSlot,
60
78
  validatorIndex,
79
+ dependentRoot: dependentRootHex,
61
80
  });
62
81
  }
63
82
 
64
- // [IGNORE] The `signed_proposer_preferences` is the first valid message received from the validator
65
- // with index `preferences.validator_index` and the given slot `preferences.proposal_slot`.
66
- if (chain.seenProposerPreferences.isKnown(proposalSlot, validatorIndex)) {
83
+ // [IGNORE] First valid message for (dependent_root, proposal_slot, validator_index).
84
+ if (chain.seenProposerPreferences.isKnown(dependentRootHex, proposalSlot, validatorIndex)) {
67
85
  throw new ProposerPreferencesError(GossipAction.IGNORE, {
68
86
  code: ProposerPreferencesErrorCode.ALREADY_KNOWN,
69
87
  proposalSlot,
70
88
  validatorIndex,
89
+ dependentRoot: dependentRootHex,
71
90
  });
72
91
  }
73
92
 
@@ -87,5 +106,5 @@ export async function validateGossipProposerPreferences(
87
106
  }
88
107
 
89
108
  // Valid
90
- chain.seenProposerPreferences.add(proposalSlot, validatorIndex);
109
+ chain.seenProposerPreferences.add(dependentRootHex, proposalSlot, validatorIndex);
91
110
  }
@@ -607,6 +607,24 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
607
607
  // Returns the delay between the start of `block.slot` and `current time`
608
608
  const delaySec = chain.clock.secFromSlot(slot);
609
609
  metrics?.gossipBlock.elapsedTimeTillProcessed.observe(delaySec);
610
+
611
+ if (isForkPostGloas(blockInput.forkName)) {
612
+ const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockInput.blockRootHex);
613
+ // This payloadInput should have been created just after gossip validation
614
+ if (!payloadInput) {
615
+ throw Error(
616
+ `PayloadEnvelopeInput not seeded for block ${blockInput.blockRootHex} during gossip processing`
617
+ );
618
+ }
619
+
620
+ // Immediately attempt fetch of data columns from execution engine as the bid contains kzg commitments
621
+ // which is all the information we need so there is no reason to delay until execution payload arrives
622
+ // TODO GLOAS: If we want EL retries after this initial attempt, add an explicit retry policy here
623
+ // (for example later in the slot). Do not couple retries to incoming gossip columns.
624
+ // Columns fetched here feed payloadInput.addColumn, which resolves waitForAllData for any
625
+ // in-flight importExecutionPayload. No processExecutionPayload trigger needed from this path.
626
+ chain.getBlobsTracker.triggerGetBlobs(payloadInput);
627
+ }
610
628
  })
611
629
  .catch((e) => {
612
630
  // Adjust verbosity based on error type
@@ -221,7 +221,7 @@ export class BeaconNode {
221
221
 
222
222
  let executionEngineOpts = opts.executionEngine;
223
223
  if (opts.executionEngine.mode === "mock") {
224
- const eth1BlockHash =
224
+ const latestEth1BlockHash =
225
225
  isStatePostBellatrix(anchorState) && anchorState.isExecutionStateType
226
226
  ? isStatePostGloas(anchorState)
227
227
  ? toRootHex(anchorState.latestBlockHash)
@@ -230,7 +230,7 @@ export class BeaconNode {
230
230
  executionEngineOpts = {
231
231
  ...opts.executionEngine,
232
232
  genesisBlockHash: ZERO_HASH_HEX,
233
- eth1BlockHash,
233
+ eth1BlockHash: opts.executionEngine.eth1BlockHash ?? latestEth1BlockHash,
234
234
  genesisTime: anchorState.genesisTime,
235
235
  config,
236
236
  };
@@ -167,14 +167,7 @@ function getHeadExecutionInfo(
167
167
  return [];
168
168
  }
169
169
 
170
- // A PayloadSeparated head is a gloas beacon block imported before its payload envelope
171
- // arrives, in that case the exec-block row surfaces the inherited parent anchor (from the
172
- // bid), which is already validated. Normalize to "valid" to avoid leaking internal
173
- // fork-choice bookkeeping into the log. Once the payload envelope arrives and the FULL
174
- // variant becomes head, executionStatus is Valid/Syncing naturally.
175
- // TODO GLOAS: revisit once optimistic sync is implemented
176
- const executionStatusStr =
177
- headInfo.executionStatus === ExecutionStatus.PayloadSeparated ? "valid" : headInfo.executionStatus.toLowerCase();
170
+ const executionStatusStr = headInfo.executionStatus.toLowerCase();
178
171
 
179
172
  // Add execution status to notifier only if head is on/post bellatrix
180
173
  if (isStatePostBellatrix(headState) && headState.isExecutionStateType) {
@@ -5,10 +5,10 @@ export const PARALLEL_HEAD_CHAINS = 2;
5
5
  export const MIN_FINALIZED_CHAIN_VALIDATED_EPOCHS = 10;
6
6
 
7
7
  /** The number of times to retry a batch before it is considered failed. */
8
- // export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
9
- // this constant is increased a lot for peerDAS because we may have many failed download due to rate limit not implemented yet
10
- // TODO: change it back to 5 when this issue is implemented https://github.com/ChainSafe/lodestar/issues/8033
11
- export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 20;
8
+ export const MAX_BATCH_DOWNLOAD_ATTEMPTS = 5;
9
+
10
+ /** Backoff before assigning more range-sync batches to a peer that rate-limited us. */
11
+ export const RATE_LIMITED_PEER_BACKOFF_MS = 5_000;
12
12
 
13
13
  /**
14
14
  * Consider batch faulty after downloading and processing this number of times
@@ -1,18 +1,19 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
2
  import {ForkName, isForkPostDeneb, isForkPostFulu, isForkPostGloas} from "@lodestar/params";
3
- import {Epoch, RootHex, Slot, phase0} from "@lodestar/types";
4
- import {LodestarError, prettyPrintIndices} from "@lodestar/utils";
3
+ import {Epoch, RootHex, SignedBeaconBlock, Slot, gloas, phase0} from "@lodestar/types";
4
+ import {LodestarError, byteArrayEquals, prettyPrintIndices, toRootHex} from "@lodestar/utils";
5
5
  import {isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
6
6
  import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
7
7
  import {isDaOutOfRange} from "../../chain/blocks/blockInput/utils.js";
8
8
  import {PayloadEnvelopeInput} from "../../chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js";
9
9
  import {BlockError, BlockErrorCode} from "../../chain/errors/index.js";
10
+ import {ZERO_HASH} from "../../constants/constants.js";
10
11
  import {PeerSyncMeta} from "../../network/peers/peersData.js";
11
12
  import {IClock} from "../../util/clock.js";
12
13
  import {CustodyConfig} from "../../util/dataColumns.js";
13
14
  import {PeerIdStr} from "../../util/peerId.js";
14
15
  import {MAX_BATCH_DOWNLOAD_ATTEMPTS, MAX_BATCH_PROCESSING_ATTEMPTS} from "../constants.js";
15
- import {DownloadByRangeRequests} from "../utils/downloadByRange.js";
16
+ import {DownloadByRangeRequests, ParentPayloadCommitments} from "../utils/downloadByRange.js";
16
17
  import {getBatchSlotRange, hashBlocks} from "./utils/index.js";
17
18
 
18
19
  /**
@@ -141,8 +142,18 @@ export class Batch {
141
142
  private readonly config: ChainForkConfig;
142
143
  private readonly clock: IClock;
143
144
  private readonly custodyConfig: CustodyConfig;
144
-
145
- constructor(startEpoch: Epoch, config: ChainForkConfig, clock: IClock, custodyConfig: CustodyConfig) {
145
+ private readonly isFirstBatchInChain: boolean;
146
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
147
+
148
+ constructor(
149
+ startEpoch: Epoch,
150
+ config: ChainForkConfig,
151
+ clock: IClock,
152
+ custodyConfig: CustodyConfig,
153
+ isFirstBatchInChain: boolean,
154
+ latestBid: gloas.ExecutionPayloadBid | undefined,
155
+ targetSlot: Slot
156
+ ) {
146
157
  this.config = config;
147
158
  this.clock = clock;
148
159
  this.custodyConfig = custodyConfig;
@@ -151,10 +162,40 @@ export class Batch {
151
162
  this.forkName = this.config.getForkName(startSlot);
152
163
  this.startEpoch = startEpoch;
153
164
  this.startSlot = startSlot;
154
- this.count = count;
165
+ this.count = Math.min(count, targetSlot - startSlot + 1);
166
+ this.isFirstBatchInChain = isFirstBatchInChain;
167
+ this.latestBid = latestBid;
155
168
  this.requests = this.getRequests([]);
156
169
  }
157
170
 
171
+ private shouldDownloadParentEnvelope(firstBlock?: SignedBeaconBlock): boolean {
172
+ if (!this.isFirstBatchInChain) return false;
173
+
174
+ if (this.startSlot === 0 || !isForkPostGloas(this.config.getForkName(this.startSlot - 1))) {
175
+ return false;
176
+ }
177
+
178
+ // we only know if we should download parent envelope if firstBlock is downloaded
179
+ if (firstBlock === undefined) return false;
180
+ if (this.latestBid === undefined) return false;
181
+ const firstBlockBidParentHash = (firstBlock.message.body as gloas.BeaconBlockBody).signedExecutionPayloadBid.message
182
+ .parentBlockHash;
183
+ return byteArrayEquals(firstBlockBidParentHash, this.latestBid.blockHash);
184
+ }
185
+
186
+ getParentPayloadCommitments(parentBlockRoot: Uint8Array): ParentPayloadCommitments {
187
+ if (this.latestBid === undefined) {
188
+ throw new Error(
189
+ `Coding error: getParentPayloadCommitments called without latestBid for parentBlockRoot=${toRootHex(parentBlockRoot)}`
190
+ );
191
+ }
192
+ return {
193
+ blockRoot: parentBlockRoot,
194
+ blockRootHex: toRootHex(parentBlockRoot),
195
+ kzgCommitments: this.latestBid.blobKzgCommitments,
196
+ };
197
+ }
198
+
158
199
  /**
159
200
  * Builds ByRange requests for block, blobs and columns
160
201
  */
@@ -202,6 +243,7 @@ export class Batch {
202
243
  const envelopesBySlot = this.state.payloadEnvelopes ?? new Map<Slot, PayloadEnvelopeInput>();
203
244
 
204
245
  // ensure blocks are in slot-wise order
246
+ const isPostGloas = isForkPostGloas(this.forkName);
205
247
  for (const blockInput of blocks) {
206
248
  const blockSlot = blockInput.slot;
207
249
  // check if block/data is present (hasBlock/hasAllData). If present then check if startSlot is the same as
@@ -217,21 +259,36 @@ export class Batch {
217
259
  if (blockInput.hasBlock() && blockStartSlot === blockSlot) {
218
260
  blockStartSlot = blockSlot + 1;
219
261
  }
220
- if (
221
- blockInput.hasBlock() &&
222
- envelopeStartSlot === blockSlot &&
223
- envelopesBySlot.get(blockSlot)?.hasPayloadEnvelope()
224
- ) {
225
- envelopeStartSlot = blockSlot + 1;
226
- }
227
- if (!blockInput.hasAllData()) {
228
- if (isBlockInputColumns(blockInput)) {
229
- for (const index of blockInput.getMissingSampledColumnMeta().missing) {
262
+
263
+ // Range sync uses hasComputedAllData (all sampled columns physically present), not hasAllData
264
+ // which flips at the reconstruction threshold. Sync never triggers reconstruction, so accepting
265
+ // a half-downloaded block here makes writeBlockInputToDb later block on waitForComputedAllData.
266
+ if (isPostGloas) {
267
+ // Post-Gloas: column data lives on PayloadEnvelopeInput, not on BlockInputNoData.
268
+ const payloadInput = envelopesBySlot.get(blockSlot);
269
+ if (blockInput.hasBlock() && envelopeStartSlot === blockSlot && payloadInput?.hasPayloadEnvelope()) {
270
+ envelopeStartSlot = blockSlot + 1;
271
+ }
272
+ if (payloadInput && !payloadInput.hasComputedAllData()) {
273
+ for (const index of payloadInput.getMissingSampledColumnMeta().missing) {
230
274
  neededColumns.add(index);
231
275
  }
276
+ } else if (payloadInput?.hasComputedAllData() && dataStartSlot === blockSlot) {
277
+ // Only advance dataStartSlot when we know columns for this slot are complete. If
278
+ // payloadInput is missing entirely we cannot tell, so stop here so the next round
279
+ // re-requests columns (and envelopes) starting at this slot.
280
+ dataStartSlot = blockSlot + 1;
281
+ }
282
+ } else {
283
+ if (isBlockInputColumns(blockInput) ? !blockInput.hasComputedAllData() : !blockInput.hasAllData()) {
284
+ if (isBlockInputColumns(blockInput)) {
285
+ for (const index of blockInput.getMissingSampledColumnMeta().missing) {
286
+ neededColumns.add(index);
287
+ }
288
+ }
289
+ } else if (dataStartSlot === blockSlot) {
290
+ dataStartSlot = blockSlot + 1;
232
291
  }
233
- } else if (dataStartSlot === blockSlot) {
234
- dataStartSlot = blockSlot + 1;
235
292
  }
236
293
  }
237
294
 
@@ -251,11 +308,15 @@ export class Batch {
251
308
  // range of 40 - 63, startSlot will be inclusive but subtraction will exclusive so need to + 1
252
309
  const count = endSlot - dataStartSlot + 1;
253
310
  if (isForkPostFulu(this.forkName) && withinValidRequestWindow) {
254
- requests.columnsRequest = {
255
- count,
256
- startSlot: dataStartSlot,
257
- columns: Array.from(neededColumns),
258
- };
311
+ // Skip the column re-request when we have no specific column indices outstanding.
312
+ // Peer rejects an empty `columns` list
313
+ if (neededColumns.size > 0) {
314
+ requests.columnsRequest = {
315
+ count,
316
+ startSlot: dataStartSlot,
317
+ columns: Array.from(neededColumns),
318
+ };
319
+ }
259
320
  } else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
260
321
  requests.blobsRequest = {
261
322
  count,
@@ -272,6 +333,36 @@ export class Batch {
272
333
  };
273
334
  }
274
335
 
336
+ // Only the first batch of a SyncChain may need the dangling-parent payload by-root.
337
+ if (blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
338
+ // shouldDownloadParentEnvelope() = true means there are at least 1 block
339
+ const parentRoot = blocks[0].getBlock().message.parentRoot;
340
+ if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
341
+ const parentRootHex = toRootHex(parentRoot);
342
+ let parentPayloadInput: PayloadEnvelopeInput | undefined;
343
+ if (this.state.payloadEnvelopes) {
344
+ for (const pi of this.state.payloadEnvelopes.values()) {
345
+ if (pi.blockRootHex === parentRootHex) {
346
+ parentPayloadInput = pi;
347
+ break;
348
+ }
349
+ }
350
+ }
351
+
352
+ const needsEnvelope = !parentPayloadInput?.hasPayloadEnvelope();
353
+ const missingColumns = parentPayloadInput
354
+ ? parentPayloadInput.getMissingSampledColumnMeta().missing
355
+ : this.custodyConfig.sampledColumns;
356
+
357
+ if (needsEnvelope || missingColumns.length > 0) {
358
+ requests.parentPayloadRequest = {
359
+ ...(needsEnvelope ? {envelopeBlockRoot: parentRoot} : {}),
360
+ ...(missingColumns.length > 0 ? {blockRoot: parentRoot, columns: missingColumns} : {}),
361
+ };
362
+ }
363
+ }
364
+ }
365
+
275
366
  return requests;
276
367
  }
277
368
 
@@ -283,24 +374,28 @@ export class Batch {
283
374
  return this.requests;
284
375
  }
285
376
 
286
- // post-fulu we need to ensure that we only request columns that the peer has advertised
287
- const {columnsRequest} = this.requests;
288
- if (columnsRequest == null) {
289
- return this.requests;
290
- }
377
+ // post-fulu we need to ensure that we only request columns that the peer has advertised.
378
+ const {columnsRequest, parentPayloadRequest} = this.requests;
291
379
 
292
380
  const peerColumns = new Set(peer.custodyColumns ?? []);
293
- const requestedColumns = columnsRequest.columns.filter((c) => peerColumns.has(c));
294
- if (requestedColumns.length === columnsRequest.columns.length) {
295
- return this.requests;
296
- }
381
+ const filteredColumnsRequest =
382
+ columnsRequest != null ? columnsRequest.columns.filter((c) => peerColumns.has(c)) : null;
383
+ const parentColumns = parentPayloadRequest?.columns;
384
+ const filteredParentColumns = parentColumns != null ? parentColumns.filter((c) => peerColumns.has(c)) : null;
385
+
386
+ const updatedColumnRequest =
387
+ columnsRequest != null && filteredColumnsRequest != null
388
+ ? {columnsRequest: {...columnsRequest, columns: filteredColumnsRequest}}
389
+ : {};
390
+ const updatedParentPayloadRequest =
391
+ parentPayloadRequest != null && filteredParentColumns != null
392
+ ? {parentPayloadRequest: {...parentPayloadRequest, columns: filteredParentColumns}}
393
+ : {};
297
394
 
298
395
  return {
299
396
  ...this.requests,
300
- columnsRequest: {
301
- ...columnsRequest,
302
- columns: requestedColumns,
303
- },
397
+ ...updatedColumnRequest,
398
+ ...updatedParentPayloadRequest,
304
399
  };
305
400
  }
306
401
 
@@ -379,7 +474,11 @@ export class Batch {
379
474
  const slots = new Set<number>();
380
475
  for (const block of blocks) {
381
476
  slots.add(block.slot);
382
- if (!block.hasBlockAndAllData()) {
477
+ const dataComplete = isBlockInputColumns(block)
478
+ ? // by_range needs to download all columns
479
+ block.hasBlock() && block.hasComputedAllData()
480
+ : block.hasBlockAndAllData();
481
+ if (!dataComplete) {
383
482
  allComplete = false;
384
483
  }
385
484
  }
@@ -395,11 +494,45 @@ export class Batch {
395
494
  }
396
495
  const newPayloadEnvelopes = payloadEnvelopes ?? this.state.payloadEnvelopes;
397
496
 
497
+ if (allComplete && isForkPostGloas(this.forkName)) {
498
+ for (const block of blocks) {
499
+ const payloadInput = newPayloadEnvelopes?.get(block.slot);
500
+ // by_range needs every block's envelope and all sampled columns.
501
+ if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
502
+ allComplete = false;
503
+ break;
504
+ }
505
+ }
506
+ }
507
+
508
+ // First batch of a sync chain must additionally have the dangling-parent payload fully
509
+ // present, otherwise `processBlocks` will throw PARENT_PAYLOAD_UNKNOWN. The parent's
510
+ // `PayloadEnvelopeInput` is identified by `blockRootHex` matching `blocks[0].parentRoot`.
511
+ if (allComplete && blocks.length > 0 && this.shouldDownloadParentEnvelope(blocks[0].getBlock())) {
512
+ const parentRoot = blocks[0].getBlock().message.parentRoot;
513
+ // Genesis has no parent payload — nothing to wait for.
514
+ if (!byteArrayEquals(parentRoot, ZERO_HASH)) {
515
+ const parentRootHex = toRootHex(parentRoot);
516
+ let parentPayloadComplete = false;
517
+ if (newPayloadEnvelopes) {
518
+ for (const payloadInput of newPayloadEnvelopes.values()) {
519
+ if (payloadInput.blockRootHex === parentRootHex) {
520
+ parentPayloadComplete = payloadInput.hasPayloadEnvelope() && payloadInput.hasComputedAllData();
521
+ break;
522
+ }
523
+ }
524
+ }
525
+ if (!parentPayloadComplete) {
526
+ allComplete = false;
527
+ }
528
+ }
529
+ }
530
+
398
531
  if (allComplete) {
399
532
  this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
400
533
  } else {
401
- this.requests = this.getRequests(blocks);
402
534
  this.state = {status: BatchStatus.AwaitingDownload, blocks, payloadEnvelopes: newPayloadEnvelopes};
535
+ this.requests = this.getRequests(blocks);
403
536
  }
404
537
 
405
538
  return this.state as DownloadSuccessState;
@@ -425,6 +558,22 @@ export class Batch {
425
558
  };
426
559
  }
427
560
 
561
+ /**
562
+ * Downloading -> AwaitingDownload (without counting as a failed attempt).
563
+ * Used when the peer rate-limited us — the request was never actually served.
564
+ */
565
+ downloadingRateLimited(): void {
566
+ if (this.state.status !== BatchStatus.Downloading) {
567
+ throw new BatchError(this.wrongStatusErrorType(BatchStatus.Downloading));
568
+ }
569
+
570
+ this.state = {
571
+ status: BatchStatus.AwaitingDownload,
572
+ blocks: this.state.blocks,
573
+ payloadEnvelopes: this.state.payloadEnvelopes,
574
+ };
575
+ }
576
+
428
577
  /**
429
578
  * AwaitingProcessing -> Processing
430
579
  */
@@ -1,5 +1,6 @@
1
1
  import {ChainForkConfig} from "@lodestar/config";
2
- import {Epoch, Root, Slot} from "@lodestar/types";
2
+ import {RequestErrorCode} from "@lodestar/reqresp";
3
+ import {Epoch, Root, Slot, gloas} from "@lodestar/types";
3
4
  import {ErrorAborted, LodestarError, Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
4
5
  import {isBlockInputBlobs, isBlockInputColumns} from "../../chain/blocks/blockInput/blockInput.js";
5
6
  import {BlockInputErrorCode} from "../../chain/blocks/blockInput/errors.js";
@@ -15,7 +16,12 @@ import {CustodyConfig} from "../../util/dataColumns.js";
15
16
  import {ItTrigger} from "../../util/itTrigger.js";
16
17
  import {PeerIdStr} from "../../util/peerId.js";
17
18
  import {WarnResult, wrapError} from "../../util/wrapError.js";
18
- import {BATCH_BUFFER_SIZE, EPOCHS_PER_BATCH, MAX_LOOK_AHEAD_EPOCHS} from "../constants.js";
19
+ import {
20
+ BATCH_BUFFER_SIZE,
21
+ EPOCHS_PER_BATCH,
22
+ MAX_LOOK_AHEAD_EPOCHS,
23
+ RATE_LIMITED_PEER_BACKOFF_MS,
24
+ } from "../constants.js";
19
25
  import {DownloadByRangeError, DownloadByRangeErrorCode} from "../utils/downloadByRange.js";
20
26
  import {RangeSyncType} from "../utils/remoteSyncType.js";
21
27
  import {Batch, BatchError, BatchErrorCode, BatchMetadata, BatchStatus} from "./batch.js";
@@ -139,20 +145,32 @@ export class SyncChain {
139
145
  private readonly batchProcessor = new ItTrigger();
140
146
  /** Sorted map of batches undergoing some kind of processing. */
141
147
  private readonly batches = new Map<Epoch, Batch>();
148
+ /**
149
+ * `true` until the first `Batch` is constructed via `includeNextBatch`
150
+ */
151
+ private isFirstBatch = true;
142
152
  private readonly peerset = new Map<PeerIdStr, ChainTarget>();
153
+ /**
154
+ * Tracks peers that have rate-limited us, mapped to the timestamp (ms) until which we should avoid them.
155
+ * This is a sync-layer optimization to avoid assigning batches to backed-off peers.
156
+ * The reqresp SelfRateLimiter independently enforces backoff at the protocol level as a safety net.
157
+ */
158
+ private readonly rateLimitedPeers = new Map<PeerIdStr, number>();
143
159
 
144
160
  private readonly logger: Logger;
145
161
  private readonly config: ChainForkConfig;
146
162
  private readonly clock: IClock;
147
163
  private readonly metrics: Metrics | null;
148
164
  private readonly custodyConfig: CustodyConfig;
165
+ private readonly latestBid: gloas.ExecutionPayloadBid | undefined;
149
166
 
150
167
  constructor(
151
168
  initialBatchEpoch: Epoch,
152
169
  initialTarget: ChainTarget,
153
170
  syncType: RangeSyncType,
154
171
  fns: SyncChainFns,
155
- modules: SyncChainModules
172
+ modules: SyncChainModules,
173
+ latestBid: gloas.ExecutionPayloadBid | undefined
156
174
  ) {
157
175
  const {config, clock, custodyConfig, logger, metrics} = modules;
158
176
  this.firstBatchEpoch = initialBatchEpoch;
@@ -168,6 +186,7 @@ export class SyncChain {
168
186
  this.clock = clock;
169
187
  this.metrics = metrics;
170
188
  this.custodyConfig = custodyConfig;
189
+ this.latestBid = latestBid;
171
190
  this.logger = logger;
172
191
  this.logId = `${syncType}-${nextChainId++}`;
173
192
 
@@ -248,6 +267,7 @@ export class SyncChain {
248
267
  */
249
268
  removePeer(peerId: PeerIdStr): boolean {
250
269
  const deleted = this.peerset.delete(peerId);
270
+ this.rateLimitedPeers.delete(peerId);
251
271
  this.computeTarget();
252
272
  return deleted;
253
273
  }
@@ -383,8 +403,18 @@ export class SyncChain {
383
403
  return;
384
404
  }
385
405
 
406
+ const now = Date.now();
386
407
  const peersSyncInfo: PeerSyncInfo[] = [];
387
408
  for (const [peerId, target] of this.peerset.entries()) {
409
+ // Skip peers that are currently in rate-limit backoff
410
+ const rateLimitedUntil = this.rateLimitedPeers.get(peerId);
411
+ if (rateLimitedUntil !== undefined) {
412
+ if (now < rateLimitedUntil) {
413
+ continue;
414
+ }
415
+ this.rateLimitedPeers.delete(peerId);
416
+ }
417
+
388
418
  try {
389
419
  peersSyncInfo.push({...this.getConnectedPeerSyncMeta(peerId), target});
390
420
  } catch (e) {
@@ -458,7 +488,17 @@ export class SyncChain {
458
488
  return null;
459
489
  }
460
490
 
461
- const batch = new Batch(startEpoch, this.config, this.clock, this.custodyConfig);
491
+ const batch = new Batch(
492
+ startEpoch,
493
+ this.config,
494
+ this.clock,
495
+ this.custodyConfig,
496
+ this.isFirstBatch,
497
+ // `latestBid` is only meaningful for the first batch's parent-payload check
498
+ this.isFirstBatch ? this.latestBid : undefined,
499
+ this.target.slot
500
+ );
501
+ this.isFirstBatch = false;
462
502
  this.batches.set(startEpoch, batch);
463
503
  return batch;
464
504
  }
@@ -516,7 +556,14 @@ export class SyncChain {
516
556
  {id: this.logId, ...batch.getMetadata(), peer: prettyPrintPeerIdStr(peer.peerId)},
517
557
  res.err
518
558
  );
519
- batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
559
+ if (errCode === RequestErrorCode.RESP_RATE_LIMITED || errCode === RequestErrorCode.REQUEST_SELF_RATE_LIMITED) {
560
+ // Peer rate-limited us — don't count as a failed download attempt and mark peer for backoff
561
+ this.rateLimitedPeers.set(peer.peerId, Date.now() + RATE_LIMITED_PEER_BACKOFF_MS);
562
+ batch.downloadingRateLimited();
563
+ this.triggerBatchDownloader();
564
+ } else {
565
+ batch.downloadingError(peer.peerId); // Throws after MAX_DOWNLOAD_ATTEMPTS
566
+ }
520
567
  } else {
521
568
  this.logger.verbose("Batch download success", {
522
569
  id: this.logId,