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

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 (107) 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 -2
  13. package/lib/chain/blocks/importBlock.js.map +1 -1
  14. package/lib/chain/blocks/importExecutionPayload.d.ts.map +1 -1
  15. package/lib/chain/blocks/importExecutionPayload.js +3 -5
  16. package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
  17. package/lib/chain/blocks/index.d.ts.map +1 -1
  18. package/lib/chain/blocks/index.js +30 -17
  19. package/lib/chain/blocks/index.js.map +1 -1
  20. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts +3 -0
  21. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.d.ts.map +1 -1
  22. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js +4 -1
  23. package/lib/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js.map +1 -1
  24. package/lib/chain/blocks/types.d.ts +2 -1
  25. package/lib/chain/blocks/types.d.ts.map +1 -1
  26. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  27. package/lib/chain/blocks/utils/chainSegment.js +8 -0
  28. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  29. package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
  30. package/lib/chain/blocks/verifyBlock.js +5 -6
  31. package/lib/chain/blocks/verifyBlock.js.map +1 -1
  32. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts +0 -4
  33. package/lib/chain/blocks/verifyBlocksExecutionPayloads.d.ts.map +1 -1
  34. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +5 -2
  35. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  36. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts +2 -1
  37. package/lib/chain/blocks/verifyBlocksSanityChecks.d.ts.map +1 -1
  38. package/lib/chain/blocks/verifyBlocksSanityChecks.js +16 -7
  39. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  40. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts +2 -2
  41. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.d.ts.map +1 -1
  42. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +5 -2
  43. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  44. package/lib/chain/errors/proposerPreferences.d.ts +8 -1
  45. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  46. package/lib/chain/errors/proposerPreferences.js +1 -0
  47. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  48. package/lib/chain/produceBlock/produceBlockBody.d.ts +1 -0
  49. package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
  50. package/lib/chain/produceBlock/produceBlockBody.js +2 -7
  51. package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
  52. package/lib/chain/regen/queued.d.ts.map +1 -1
  53. package/lib/chain/regen/queued.js +1 -4
  54. package/lib/chain/regen/queued.js.map +1 -1
  55. package/lib/chain/regen/regen.d.ts.map +1 -1
  56. package/lib/chain/regen/regen.js +1 -4
  57. package/lib/chain/regen/regen.js.map +1 -1
  58. package/lib/chain/seenCache/seenProposerPreferences.d.ts +8 -7
  59. package/lib/chain/seenCache/seenProposerPreferences.d.ts.map +1 -1
  60. package/lib/chain/seenCache/seenProposerPreferences.js +17 -11
  61. package/lib/chain/seenCache/seenProposerPreferences.js.map +1 -1
  62. package/lib/chain/validation/executionPayloadBid.js +11 -8
  63. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  64. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  65. package/lib/chain/validation/proposerPreferences.js +39 -17
  66. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  67. package/lib/network/gossip/topic.d.ts +2 -0
  68. package/lib/network/gossip/topic.d.ts.map +1 -1
  69. package/lib/node/nodejs.js +2 -2
  70. package/lib/node/nodejs.js.map +1 -1
  71. package/lib/node/notifier.js +1 -7
  72. package/lib/node/notifier.js.map +1 -1
  73. package/lib/sync/range/batch.d.ts.map +1 -1
  74. package/lib/sync/range/batch.js +54 -17
  75. package/lib/sync/range/batch.js.map +1 -1
  76. package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
  77. package/lib/sync/utils/downloadByRange.js +17 -8
  78. package/lib/sync/utils/downloadByRange.js.map +1 -1
  79. package/lib/util/sszBytes.d.ts.map +1 -1
  80. package/lib/util/sszBytes.js +8 -6
  81. package/lib/util/sszBytes.js.map +1 -1
  82. package/package.json +15 -15
  83. package/src/api/impl/debug/index.ts +0 -1
  84. package/src/api/impl/validator/index.ts +2 -1
  85. package/src/chain/blocks/blockInput/blockInput.ts +4 -1
  86. package/src/chain/blocks/importBlock.ts +14 -3
  87. package/src/chain/blocks/importExecutionPayload.ts +3 -5
  88. package/src/chain/blocks/index.ts +20 -9
  89. package/src/chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.ts +4 -1
  90. package/src/chain/blocks/types.ts +2 -1
  91. package/src/chain/blocks/utils/chainSegment.ts +8 -0
  92. package/src/chain/blocks/verifyBlock.ts +7 -5
  93. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +6 -4
  94. package/src/chain/blocks/verifyBlocksSanityChecks.ts +16 -6
  95. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +7 -2
  96. package/src/chain/errors/proposerPreferences.ts +9 -1
  97. package/src/chain/produceBlock/produceBlockBody.ts +3 -7
  98. package/src/chain/regen/queued.ts +2 -7
  99. package/src/chain/regen/regen.ts +2 -7
  100. package/src/chain/seenCache/seenProposerPreferences.ts +20 -12
  101. package/src/chain/validation/executionPayloadBid.ts +11 -8
  102. package/src/chain/validation/proposerPreferences.ts +37 -18
  103. package/src/node/nodejs.ts +2 -2
  104. package/src/node/notifier.ts +1 -8
  105. package/src/sync/range/batch.ts +54 -19
  106. package/src/sync/utils/downloadByRange.ts +18 -8
  107. package/src/util/sszBytes.ts +8 -6
@@ -20,7 +20,7 @@ export type VerifyExecutionPayloadEnvelopeOpts = {
20
20
  * performed outside this function, see `verifyExecutionPayloadEnvelopeSignature` and
21
21
  * `importExecutionPayload` which run both in parallel with this check.
22
22
  *
23
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope
23
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope
24
24
  */
25
25
  export function verifyExecutionPayloadEnvelope(
26
26
  config: BeaconConfig,
@@ -43,6 +43,11 @@ export function verifyExecutionPayloadEnvelope(
43
43
  `Envelope's block is not the latest block header envelope=${toRootHex(envelope.beaconBlockRoot)} latestBlockHeader=${toRootHex(headerRoot)}`
44
44
  );
45
45
  }
46
+ if (!byteArrayEquals(envelope.parentBeaconBlockRoot, state.latestBlockHeader.parentRoot)) {
47
+ throw new Error(
48
+ `Envelope's parent_beacon_block_root mismatch envelope=${toRootHex(envelope.parentBeaconBlockRoot)} state=${toRootHex(state.latestBlockHeader.parentRoot)}`
49
+ );
50
+ }
46
51
 
47
52
  // Verify consistency with the committed bid
48
53
  const bid = state.latestExecutionPayloadBid;
@@ -108,7 +113,7 @@ export function verifyExecutionPayloadEnvelope(
108
113
  /**
109
114
  * Verify the BLS signature of an execution payload envelope.
110
115
  *
111
- * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope_signature
116
+ * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/fork-choice.md#new-verify_execution_payload_envelope_signature
112
117
  */
113
118
  export async function verifyExecutionPayloadEnvelopeSignature(
114
119
  config: BeaconConfig,
@@ -1,9 +1,10 @@
1
- import {Slot, ValidatorIndex} from "@lodestar/types";
1
+ import {RootHex, Slot, ValidatorIndex} from "@lodestar/types";
2
2
  import {GossipActionError} from "./gossipValidation.js";
3
3
 
4
4
  export enum ProposerPreferencesErrorCode {
5
5
  INVALID_EPOCH = "PROPOSER_PREFERENCES_ERROR_INVALID_EPOCH",
6
6
  PROPOSAL_SLOT_PASSED = "PROPOSER_PREFERENCES_ERROR_PROPOSAL_SLOT_PASSED",
7
+ UNKNOWN_DEPENDENT_ROOT = "PROPOSER_PREFERENCES_ERROR_UNKNOWN_DEPENDENT_ROOT",
7
8
  INVALID_PROPOSER = "PROPOSER_PREFERENCES_ERROR_INVALID_PROPOSER",
8
9
  ALREADY_KNOWN = "PROPOSER_PREFERENCES_ERROR_ALREADY_KNOWN",
9
10
  INVALID_SIGNATURE = "PROPOSER_PREFERENCES_ERROR_INVALID_SIGNATURE",
@@ -20,15 +21,22 @@ export type ProposerPreferencesErrorType =
20
21
  proposalSlot: Slot;
21
22
  currentSlot: Slot;
22
23
  }
24
+ | {
25
+ code: ProposerPreferencesErrorCode.UNKNOWN_DEPENDENT_ROOT;
26
+ proposalSlot: Slot;
27
+ dependentRoot: RootHex;
28
+ }
23
29
  | {
24
30
  code: ProposerPreferencesErrorCode.INVALID_PROPOSER;
25
31
  proposalSlot: Slot;
26
32
  validatorIndex: ValidatorIndex;
33
+ dependentRoot: RootHex;
27
34
  }
28
35
  | {
29
36
  code: ProposerPreferencesErrorCode.ALREADY_KNOWN;
30
37
  proposalSlot: Slot;
31
38
  validatorIndex: ValidatorIndex;
39
+ dependentRoot: RootHex;
32
40
  }
33
41
  | {
34
42
  code: ProposerPreferencesErrorCode.INVALID_SIGNATURE;
@@ -49,7 +49,7 @@ import {
49
49
  ssz,
50
50
  } from "@lodestar/types";
51
51
  import {Logger, byteArrayEquals, fromHex, sleep, toHex, toPubkeyHex, toRootHex} from "@lodestar/utils";
52
- import {ZERO_HASH, ZERO_HASH_HEX} from "../../constants/index.js";
52
+ import {ZERO_HASH_HEX} from "../../constants/index.js";
53
53
  import {numToQuantity} from "../../execution/engine/utils.js";
54
54
  import {
55
55
  IExecutionBuilder,
@@ -111,6 +111,7 @@ export type ProduceFullGloas = {
111
111
  executionRequests: electra.ExecutionRequests;
112
112
  blobsBundle: BlobsBundle<ForkPostGloas>;
113
113
  cells: fulu.Cell[][];
114
+ parentBlockRoot: Root;
114
115
  };
115
116
  export type ProduceFullFulu = {
116
117
  type: BlockType.Full;
@@ -225,12 +226,6 @@ export async function produceBlockBody<T extends BlockType>(
225
226
  stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests);
226
227
  } else {
227
228
  parentBlockHash = currentState.latestExecutionPayloadBid.parentBlockHash;
228
- // At gloas genesis the committed bid has no prior EL block to reference
229
- // (`bid.parentBlockHash` is zero). Fall back to `bid.blockHash` (= eth1 genesis hash) so the
230
- // FCU to the EL carries a valid head. Post-genesis bids always reference a non-zero parent.
231
- if (byteArrayEquals(parentBlockHash, ZERO_HASH)) {
232
- parentBlockHash = currentState.latestExecutionPayloadBid.blockHash;
233
- }
234
229
  parentExecutionRequests = ssz.electra.ExecutionRequests.defaultValue();
235
230
  }
236
231
  const prepareRes = await prepareExecutionPayload(
@@ -317,6 +312,7 @@ export async function produceBlockBody<T extends BlockType>(
317
312
  gloasResult.executionRequests = executionRequests;
318
313
  gloasResult.blobsBundle = blobsBundle;
319
314
  gloasResult.cells = cells;
315
+ gloasResult.parentBlockRoot = fromHex(parentBlock.blockRoot);
320
316
 
321
317
  const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
322
318
  this.metrics?.blockPayload.payloadFetchedTime.observe({prepType}, fetchedTime);
@@ -1,7 +1,7 @@
1
1
  import {routes} from "@lodestar/api";
2
2
  import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
3
3
  import {IBeaconStateView, computeEpochAtSlot} from "@lodestar/state-transition";
4
- import {BeaconBlock, Epoch, RootHex, Slot, isGloasBeaconBlock, phase0} from "@lodestar/types";
4
+ import {BeaconBlock, Epoch, RootHex, Slot, phase0} from "@lodestar/types";
5
5
  import {Logger, toRootHex} from "@lodestar/utils";
6
6
  import {Metrics} from "../../metrics/index.js";
7
7
  import {JobItemQueue} from "../../util/queue/index.js";
@@ -88,12 +88,7 @@ export class QueuedStateRegenerator implements IStateRegenerator {
88
88
  */
89
89
  getPreStateSync(block: BeaconBlock): IBeaconStateView | null {
90
90
  const parentRoot = toRootHex(block.parentRoot);
91
- const parentBlock = isGloasBeaconBlock(block)
92
- ? this.forkChoice.getBlockHexAndBlockHash(
93
- parentRoot,
94
- toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash)
95
- )
96
- : this.forkChoice.getBlockHexDefaultStatus(parentRoot);
91
+ const parentBlock = this.forkChoice.getBlockHexDefaultStatus(parentRoot);
97
92
  if (!parentBlock) {
98
93
  throw new RegenError({
99
94
  code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE,
@@ -9,7 +9,7 @@ import {
9
9
  computeEpochAtSlot,
10
10
  computeStartSlotAtEpoch,
11
11
  } from "@lodestar/state-transition";
12
- import {BeaconBlock, RootHex, SignedBeaconBlock, Slot, isGloasBeaconBlock} from "@lodestar/types";
12
+ import {BeaconBlock, RootHex, SignedBeaconBlock, Slot} from "@lodestar/types";
13
13
  import {Logger, fromHex, toRootHex} from "@lodestar/utils";
14
14
  import {IBeaconDb} from "../../db/index.js";
15
15
  import {Metrics} from "../../metrics/index.js";
@@ -57,12 +57,7 @@ export class StateRegenerator implements IStateRegeneratorInternal {
57
57
  regenCaller: RegenCaller
58
58
  ): Promise<IBeaconStateView> {
59
59
  const parentRoot = toRootHex(block.parentRoot);
60
- const parentBlock = isGloasBeaconBlock(block)
61
- ? this.modules.forkChoice.getBlockHexAndBlockHash(
62
- parentRoot,
63
- toRootHex(block.body.signedExecutionPayloadBid.message.parentBlockHash)
64
- )
65
- : this.modules.forkChoice.getBlockHexDefaultStatus(parentRoot);
60
+ const parentBlock = this.modules.forkChoice.getBlockHexDefaultStatus(parentRoot);
66
61
  if (!parentBlock) {
67
62
  throw new RegenError({
68
63
  code: RegenErrorCode.BLOCK_NOT_IN_FORKCHOICE,
@@ -1,28 +1,36 @@
1
- import {Slot, ValidatorIndex} from "@lodestar/types";
1
+ import {RootHex, Slot, ValidatorIndex} from "@lodestar/types";
2
2
  import {MapDef} from "@lodestar/utils";
3
3
 
4
4
  /**
5
- * Tracks signed proposer preferences we've already seen per (proposal_slot, validator_index).
5
+ * Tracks signed proposer preferences we've already seen per (dependent_root, proposal_slot, validator_index).
6
6
  */
7
7
  export class SeenProposerPreferences {
8
- private readonly validatorIndexesBySlot = new MapDef<Slot, Set<ValidatorIndex>>(() => new Set<ValidatorIndex>());
8
+ private readonly validatorBySlotByDependentRoot = new MapDef<RootHex, Map<Slot, ValidatorIndex>>(
9
+ () => new Map<Slot, ValidatorIndex>()
10
+ );
9
11
 
10
- isKnown(proposalSlot: Slot, validatorIndex: ValidatorIndex): boolean {
11
- return this.validatorIndexesBySlot.get(proposalSlot)?.has(validatorIndex) === true;
12
+ isKnown(dependentRoot: RootHex, proposalSlot: Slot, validatorIndex: ValidatorIndex): boolean {
13
+ return this.validatorBySlotByDependentRoot.get(dependentRoot)?.get(proposalSlot) === validatorIndex;
12
14
  }
13
15
 
14
- add(proposalSlot: Slot, validatorIndex: ValidatorIndex): void {
15
- this.validatorIndexesBySlot.getOrDefault(proposalSlot).add(validatorIndex);
16
+ add(dependentRoot: RootHex, proposalSlot: Slot, validatorIndex: ValidatorIndex): void {
17
+ this.validatorBySlotByDependentRoot.getOrDefault(dependentRoot).set(proposalSlot, validatorIndex);
16
18
  }
17
19
 
18
20
  /**
19
- * Entries are only load-bearing while `proposal_slot > state.slot`. Once the slot has passed the
20
- * `[IGNORE] proposal_slot > state.slot` gossip rule takes over, so drop them on each slot tick.
21
+ * Entries are only load-bearing while `proposal_slot > current_slot`. Once the slot has
22
+ * passed the `[IGNORE] proposal_slot > current_slot` gossip rule takes over, so drop them
23
+ * on each slot tick.
21
24
  */
22
25
  prune(currentSlot: Slot): void {
23
- for (const slot of this.validatorIndexesBySlot.keys()) {
24
- if (slot < currentSlot) {
25
- this.validatorIndexesBySlot.delete(slot);
26
+ for (const [dependentRoot, slotMap] of this.validatorBySlotByDependentRoot.entries()) {
27
+ for (const slot of slotMap.keys()) {
28
+ if (slot < currentSlot) {
29
+ slotMap.delete(slot);
30
+ }
31
+ }
32
+ if (slotMap.size === 0) {
33
+ this.validatorBySlotByDependentRoot.delete(dependentRoot);
26
34
  }
27
35
  }
28
36
  }
@@ -48,9 +48,12 @@ async function validateExecutionPayloadBid(
48
48
  });
49
49
  }
50
50
 
51
- // [IGNORE] the `SignedProposerPreferences` where `preferences.proposal_slot`
52
- // is equal to `bid.slot` has been seen.
53
- // TODO GLOAS: Implement this along with proposer preference
51
+ // [IGNORE] A `SignedProposerPreferences` matching `bid.slot` and the bid's branch has been
52
+ // seen i.e. `proposal_slot == bid.slot` AND `dependent_root ==
53
+ // get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`,
54
+ // where `parent_state` is the post-state of `bid.parent_block_root`.
55
+ // This is the message referenced as `proposer_preferences` in the following REJECT rules.
56
+ // TODO GLOAS: Implement once a ProposerPreferencesPool exists.
54
57
 
55
58
  // [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
56
59
  // `is_active_builder(state, bid.builder_index)` returns `True`.
@@ -71,11 +74,11 @@ async function validateExecutionPayloadBid(
71
74
  });
72
75
  }
73
76
 
74
- // [REJECT] `bid.fee_recipient` matches the `fee_recipient` from the proposer's
75
- // `SignedProposerPreferences` associated with `bid.slot`.
76
- // [REJECT] `bid.gas_limit` matches the `gas_limit` from the proposer's
77
- // `SignedProposerPreferences` associated with `bid.slot`.
78
- // TODO GLOAS: Implement this along with proposer preference
77
+ // [REJECT] `bid.fee_recipient == proposer_preferences.fee_recipient`.
78
+ // [REJECT] `bid.gas_limit == proposer_preferences.gas_limit`.
79
+ // Both compared against the matching `proposer_preferences` defined above (same branch
80
+ // via dependent_root, same proposal_slot).
81
+ // TODO GLOAS: Implement once a ProposerPreferencesPool exists.
79
82
 
80
83
  // [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
81
84
  // consensus layer -- i.e. validate that
@@ -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
  }
@@ -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) {
@@ -202,6 +202,7 @@ export class Batch {
202
202
  const envelopesBySlot = this.state.payloadEnvelopes ?? new Map<Slot, PayloadEnvelopeInput>();
203
203
 
204
204
  // ensure blocks are in slot-wise order
205
+ const isPostGloas = isForkPostGloas(this.forkName);
205
206
  for (const blockInput of blocks) {
206
207
  const blockSlot = blockInput.slot;
207
208
  // check if block/data is present (hasBlock/hasAllData). If present then check if startSlot is the same as
@@ -217,21 +218,36 @@ export class Batch {
217
218
  if (blockInput.hasBlock() && blockStartSlot === blockSlot) {
218
219
  blockStartSlot = blockSlot + 1;
219
220
  }
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) {
221
+
222
+ // Range sync uses hasComputedAllData (all sampled columns physically present), not hasAllData
223
+ // which flips at the reconstruction threshold. Sync never triggers reconstruction, so accepting
224
+ // a half-downloaded block here makes writeBlockInputToDb later block on waitForComputedAllData.
225
+ if (isPostGloas) {
226
+ // Post-Gloas: column data lives on PayloadEnvelopeInput, not on BlockInputNoData.
227
+ const payloadInput = envelopesBySlot.get(blockSlot);
228
+ if (blockInput.hasBlock() && envelopeStartSlot === blockSlot && payloadInput?.hasPayloadEnvelope()) {
229
+ envelopeStartSlot = blockSlot + 1;
230
+ }
231
+ if (payloadInput && !payloadInput.hasComputedAllData()) {
232
+ for (const index of payloadInput.getMissingSampledColumnMeta().missing) {
230
233
  neededColumns.add(index);
231
234
  }
235
+ } else if (payloadInput?.hasComputedAllData() && dataStartSlot === blockSlot) {
236
+ // Only advance dataStartSlot when we know columns for this slot are complete. If
237
+ // payloadInput is missing entirely we cannot tell, so stop here so the next round
238
+ // re-requests columns (and envelopes) starting at this slot.
239
+ dataStartSlot = blockSlot + 1;
240
+ }
241
+ } else {
242
+ if (isBlockInputColumns(blockInput) ? !blockInput.hasComputedAllData() : !blockInput.hasAllData()) {
243
+ if (isBlockInputColumns(blockInput)) {
244
+ for (const index of blockInput.getMissingSampledColumnMeta().missing) {
245
+ neededColumns.add(index);
246
+ }
247
+ }
248
+ } else if (dataStartSlot === blockSlot) {
249
+ dataStartSlot = blockSlot + 1;
232
250
  }
233
- } else if (dataStartSlot === blockSlot) {
234
- dataStartSlot = blockSlot + 1;
235
251
  }
236
252
  }
237
253
 
@@ -251,11 +267,15 @@ export class Batch {
251
267
  // range of 40 - 63, startSlot will be inclusive but subtraction will exclusive so need to + 1
252
268
  const count = endSlot - dataStartSlot + 1;
253
269
  if (isForkPostFulu(this.forkName) && withinValidRequestWindow) {
254
- requests.columnsRequest = {
255
- count,
256
- startSlot: dataStartSlot,
257
- columns: Array.from(neededColumns),
258
- };
270
+ // Skip the column re-request when we have no specific column indices outstanding.
271
+ // Peer rejects an empty `columns` list
272
+ if (neededColumns.size > 0) {
273
+ requests.columnsRequest = {
274
+ count,
275
+ startSlot: dataStartSlot,
276
+ columns: Array.from(neededColumns),
277
+ };
278
+ }
259
279
  } else if (isForkPostDeneb(this.forkName) && withinValidRequestWindow) {
260
280
  requests.blobsRequest = {
261
281
  count,
@@ -379,7 +399,11 @@ export class Batch {
379
399
  const slots = new Set<number>();
380
400
  for (const block of blocks) {
381
401
  slots.add(block.slot);
382
- if (!block.hasBlockAndAllData()) {
402
+ const dataComplete = isBlockInputColumns(block)
403
+ ? // by_range needs to download all columns
404
+ block.hasBlock() && block.hasComputedAllData()
405
+ : block.hasBlockAndAllData();
406
+ if (!dataComplete) {
383
407
  allComplete = false;
384
408
  }
385
409
  }
@@ -395,11 +419,22 @@ export class Batch {
395
419
  }
396
420
  const newPayloadEnvelopes = payloadEnvelopes ?? this.state.payloadEnvelopes;
397
421
 
422
+ if (allComplete && isForkPostGloas(this.forkName)) {
423
+ for (const block of blocks) {
424
+ const payloadInput = newPayloadEnvelopes?.get(block.slot);
425
+ // by_range needs to download all columns
426
+ if (!payloadInput?.hasPayloadEnvelope() || !payloadInput.hasComputedAllData()) {
427
+ allComplete = false;
428
+ break;
429
+ }
430
+ }
431
+ }
432
+
398
433
  if (allComplete) {
399
434
  this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes};
400
435
  } else {
401
- this.requests = this.getRequests(blocks);
402
436
  this.state = {status: BatchStatus.AwaitingDownload, blocks, payloadEnvelopes: newPayloadEnvelopes};
437
+ this.requests = this.getRequests(blocks);
403
438
  }
404
439
 
405
440
  return this.state as DownloadSuccessState;
@@ -14,6 +14,7 @@ import {
14
14
  deneb,
15
15
  fulu,
16
16
  gloas,
17
+ isGloasBeaconBlock,
17
18
  isGloasDataColumnSidecar,
18
19
  phase0,
19
20
  } from "@lodestar/types";
@@ -201,12 +202,10 @@ export function cacheByRangeResponses({
201
202
  });
202
203
  }
203
204
 
204
- // Attach envelopes to entries whose envelope was returned by the peer. The returned
205
- // payloadEnvelopes map only contains entries with envelopes ready for importExecutionPayload.
206
- let payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null = null;
205
+ let payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null =
206
+ existingPayloadEnvelopes !== null ? new Map(existingPayloadEnvelopes) : null;
207
207
  if (downloadedPayloadEnvelopes !== null) {
208
- payloadEnvelopes = new Map(existingPayloadEnvelopes ?? []);
209
-
208
+ payloadEnvelopes ??= new Map();
210
209
  for (const [slot, envelope] of downloadedPayloadEnvelopes) {
211
210
  const blockInput = updatedBatchBlocks.get(slot);
212
211
  if (!blockInput?.hasBlock() || !isForkPostGloas(blockInput.forkName)) {
@@ -361,7 +360,7 @@ export async function requestByRange({
361
360
  let blocks: undefined | SignedBeaconBlock[];
362
361
  let blobSidecars: undefined | deneb.BlobSidecars;
363
362
  let columnSidecars: undefined | DataColumnSidecar[];
364
- let payloadEnvelopes: undefined | gloas.SignedExecutionPayloadEnvelope[];
363
+ const payloadEnvelopes: gloas.SignedExecutionPayloadEnvelope[] = [];
365
364
 
366
365
  const requests: Promise<unknown>[] = [];
367
366
 
@@ -369,6 +368,17 @@ export async function requestByRange({
369
368
  requests.push(
370
369
  network.sendBeaconBlocksByRange(peerIdStr, blocksRequest).then((blockResponse) => {
371
370
  blocks = blockResponse;
371
+ const firstBlock = blockResponse.at(0);
372
+ if (firstBlock && isGloasBeaconBlock(firstBlock.message)) {
373
+ return network
374
+ .sendExecutionPayloadEnvelopesByRoot(peerIdStr, [
375
+ firstBlock.message.body.signedExecutionPayloadBid.message.parentBlockRoot,
376
+ ])
377
+ .then((envelopeResponse) => {
378
+ payloadEnvelopes?.unshift(...envelopeResponse);
379
+ });
380
+ }
381
+ return undefined;
372
382
  })
373
383
  );
374
384
  }
@@ -392,7 +402,7 @@ export async function requestByRange({
392
402
  if (envelopesRequest) {
393
403
  requests.push(
394
404
  network.sendExecutionPayloadEnvelopesByRange(peerIdStr, envelopesRequest).then((envelopeResponse) => {
395
- payloadEnvelopes = envelopeResponse;
405
+ payloadEnvelopes?.push(...envelopeResponse);
396
406
  })
397
407
  );
398
408
  }
@@ -1179,7 +1189,7 @@ export function validateEnvelopesByRangeResponse(
1179
1189
  const slot = payloadEnvelope.message.payload.slotNumber;
1180
1190
  const batchBlockRoot = batchBlockRoots.get(slot);
1181
1191
 
1182
- // Envelopes for slots not in the batch are silently ignored (orphaned payloads)
1192
+ // Envelopes for slots not in the batch are silently ignored (orphaned payloads or a parent payload)
1183
1193
  if (batchBlockRoot === undefined) {
1184
1194
  continue;
1185
1195
  }
@@ -558,9 +558,10 @@ export function getBeaconBlockRootFromDataColumnSidecarSerialized(data: Uint8Arr
558
558
  * └─ ExecutionPayloadEnvelope (starts at byte 100):
559
559
  * ├─ 4 bytes: payload offset
560
560
  * ├─ 4 bytes: executionRequests offset
561
- * ├─ 8 bytes: builderIndex (offset 108-115)
562
- * ├─ 32 bytes: beaconBlockRoot (offset 116-147)
563
- * └─ variable: payload data (starts at envelope + 48)
561
+ * ├─ 8 bytes: builderIndex (offset 108-115)
562
+ * ├─ 32 bytes: beaconBlockRoot (offset 116-147)
563
+ * ├─ 32 bytes: parentBeaconBlockRoot (offset 148-179) new in Gloas alpha.6 (consensus-specs#5152)
564
+ * └─ variable: payload data (starts at envelope + 80)
564
565
  * └─ ExecutionPayload fixed portion includes slotNumber at offset 532
565
566
  */
566
567
  const SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET = 4;
@@ -576,12 +577,13 @@ const BEACON_BLOCK_ROOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
576
577
  EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET +
577
578
  EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE; // 116
578
579
 
579
- // Envelope fixed portion (without slot): payload_offset(4) + requests_offset(4) + builderIndex(8) + beaconBlockRoot(32) = 48
580
+ // Envelope fixed portion: payload_offset(4) + requests_offset(4) + builderIndex(8) + beaconBlockRoot(32) + parentBeaconBlockRoot(32) = 80
580
581
  const EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE =
581
582
  EXECUTION_PAYLOAD_ENVELOPE_PAYLOAD_OFFSET +
582
583
  EXECUTION_PAYLOAD_ENVELOPE_REQUESTS_OFFSET +
583
584
  EXECUTION_PAYLOAD_ENVELOPE_BUILDER_INDEX_SIZE +
584
- ROOT_SIZE; // 48
585
+ ROOT_SIZE +
586
+ ROOT_SIZE; // 80
585
587
 
586
588
  // slotNumber offset within ExecutionPayload fixed portion:
587
589
  // parentHash(32) + feeRecipient(20) + stateRoot(32) + receiptsRoot(32) + logsBloom(256) +
@@ -595,7 +597,7 @@ const ENVELOPE_START_IN_SIGNED =
595
597
  SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MESSAGE_OFFSET + SIGNED_EXECUTION_PAYLOAD_ENVELOPE_SIGNATURE_SIZE; // 100
596
598
 
597
599
  const SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE =
598
- ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 48 + 532 = 680
600
+ ENVELOPE_START_IN_SIGNED + EXECUTION_PAYLOAD_ENVELOPE_FIXED_SIZE + SLOT_NUMBER_OFFSET_IN_EXECUTION_PAYLOAD; // 100 + 80 + 532 = 712
599
601
 
600
602
  export function getSlotFromExecutionPayloadEnvelopeSerialized(data: Uint8Array): Slot | null {
601
603
  if (data.length < SLOT_OFFSET_IN_SIGNED_EXECUTION_PAYLOAD_ENVELOPE + SLOT_SIZE) {