@lodestar/beacon-node 1.45.0 → 1.46.0-dev.1cd9a671ea

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 (151) hide show
  1. package/lib/api/impl/beacon/blocks/index.d.ts.map +1 -1
  2. package/lib/api/impl/beacon/blocks/index.js +186 -57
  3. package/lib/api/impl/beacon/blocks/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 +172 -88
  6. package/lib/api/impl/validator/index.js.map +1 -1
  7. package/lib/api/rest/base.d.ts.map +1 -1
  8. package/lib/api/rest/base.js +6 -3
  9. package/lib/api/rest/base.js.map +1 -1
  10. package/lib/chain/blocks/importBlock.d.ts.map +1 -1
  11. package/lib/chain/blocks/importBlock.js +2 -17
  12. package/lib/chain/blocks/importBlock.js.map +1 -1
  13. package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
  14. package/lib/chain/blocks/utils/chainSegment.js +13 -5
  15. package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
  16. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js +1 -1
  17. package/lib/chain/blocks/verifyBlocksExecutionPayloads.js.map +1 -1
  18. package/lib/chain/blocks/verifyBlocksSanityChecks.js +1 -1
  19. package/lib/chain/blocks/verifyBlocksSanityChecks.js.map +1 -1
  20. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js +2 -2
  21. package/lib/chain/blocks/verifyExecutionPayloadEnvelope.js.map +1 -1
  22. package/lib/chain/chain.d.ts.map +1 -1
  23. package/lib/chain/chain.js +44 -2
  24. package/lib/chain/chain.js.map +1 -1
  25. package/lib/chain/errors/blockError.d.ts +48 -6
  26. package/lib/chain/errors/blockError.d.ts.map +1 -1
  27. package/lib/chain/errors/blockError.js +25 -4
  28. package/lib/chain/errors/blockError.js.map +1 -1
  29. package/lib/chain/errors/executionPayloadEnvelope.d.ts +11 -0
  30. package/lib/chain/errors/executionPayloadEnvelope.d.ts.map +1 -1
  31. package/lib/chain/errors/executionPayloadEnvelope.js +2 -0
  32. package/lib/chain/errors/executionPayloadEnvelope.js.map +1 -1
  33. package/lib/chain/errors/proposerPreferences.d.ts +14 -1
  34. package/lib/chain/errors/proposerPreferences.d.ts.map +1 -1
  35. package/lib/chain/errors/proposerPreferences.js +2 -0
  36. package/lib/chain/errors/proposerPreferences.js.map +1 -1
  37. package/lib/chain/lightClient/index.d.ts.map +1 -1
  38. package/lib/chain/lightClient/index.js +11 -6
  39. package/lib/chain/lightClient/index.js.map +1 -1
  40. package/lib/chain/lightClient/proofs.d.ts.map +1 -1
  41. package/lib/chain/lightClient/proofs.js +28 -2
  42. package/lib/chain/lightClient/proofs.js.map +1 -1
  43. package/lib/chain/lightClient/types.d.ts +3 -1
  44. package/lib/chain/lightClient/types.d.ts.map +1 -1
  45. package/lib/chain/opPools/aggregatedAttestationPool.js +1 -1
  46. package/lib/chain/opPools/aggregatedAttestationPool.js.map +1 -1
  47. package/lib/chain/opPools/proposerPreferencesPool.d.ts +1 -1
  48. package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -1
  49. package/lib/chain/opPools/proposerPreferencesPool.js +1 -1
  50. package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -1
  51. package/lib/chain/validation/aggregateAndProof.js +10 -6
  52. package/lib/chain/validation/aggregateAndProof.js.map +1 -1
  53. package/lib/chain/validation/attestation.d.ts.map +1 -1
  54. package/lib/chain/validation/attestation.js +10 -6
  55. package/lib/chain/validation/attestation.js.map +1 -1
  56. package/lib/chain/validation/block.d.ts.map +1 -1
  57. package/lib/chain/validation/block.js +53 -4
  58. package/lib/chain/validation/block.js.map +1 -1
  59. package/lib/chain/validation/executionPayloadBid.d.ts +5 -0
  60. package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
  61. package/lib/chain/validation/executionPayloadBid.js +31 -3
  62. package/lib/chain/validation/executionPayloadBid.js.map +1 -1
  63. package/lib/chain/validation/executionPayloadEnvelope.d.ts.map +1 -1
  64. package/lib/chain/validation/executionPayloadEnvelope.js +29 -0
  65. package/lib/chain/validation/executionPayloadEnvelope.js.map +1 -1
  66. package/lib/chain/validation/proposerPreferences.d.ts +9 -2
  67. package/lib/chain/validation/proposerPreferences.d.ts.map +1 -1
  68. package/lib/chain/validation/proposerPreferences.js +61 -9
  69. package/lib/chain/validation/proposerPreferences.js.map +1 -1
  70. package/lib/chain/validation/signatureSets/aggregateAndProof.d.ts.map +1 -1
  71. package/lib/chain/validation/signatureSets/aggregateAndProof.js +2 -3
  72. package/lib/chain/validation/signatureSets/aggregateAndProof.js.map +1 -1
  73. package/lib/db/repositories/lightclientSyncCommitteeWitness.d.ts.map +1 -1
  74. package/lib/db/repositories/lightclientSyncCommitteeWitness.js +42 -7
  75. package/lib/db/repositories/lightclientSyncCommitteeWitness.js.map +1 -1
  76. package/lib/network/gossip/encoding.d.ts +2 -2
  77. package/lib/network/gossip/encoding.d.ts.map +1 -1
  78. package/lib/network/gossip/encoding.js +15 -9
  79. package/lib/network/gossip/encoding.js.map +1 -1
  80. package/lib/network/gossip/gossipsub.d.ts.map +1 -1
  81. package/lib/network/gossip/gossipsub.js +2 -2
  82. package/lib/network/gossip/gossipsub.js.map +1 -1
  83. package/lib/network/gossip/topic.d.ts +74 -231
  84. package/lib/network/gossip/topic.d.ts.map +1 -1
  85. package/lib/network/gossip/topic.js +24 -1
  86. package/lib/network/gossip/topic.js.map +1 -1
  87. package/lib/network/interface.d.ts +4 -1
  88. package/lib/network/interface.d.ts.map +1 -1
  89. package/lib/network/network.d.ts +4 -1
  90. package/lib/network/network.d.ts.map +1 -1
  91. package/lib/network/network.js +19 -11
  92. package/lib/network/network.js.map +1 -1
  93. package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
  94. package/lib/network/processor/gossipHandlers.js +12 -6
  95. package/lib/network/processor/gossipHandlers.js.map +1 -1
  96. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.d.ts.map +1 -1
  97. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js +4 -2
  98. package/lib/network/reqresp/handlers/executionPayloadEnvelopesByRange.js.map +1 -1
  99. package/lib/network/reqresp/handlers/index.d.ts.map +1 -1
  100. package/lib/network/reqresp/handlers/index.js +20 -11
  101. package/lib/network/reqresp/handlers/index.js.map +1 -1
  102. package/lib/network/util.d.ts +1 -0
  103. package/lib/network/util.d.ts.map +1 -1
  104. package/lib/network/util.js +4 -0
  105. package/lib/network/util.js.map +1 -1
  106. package/lib/sync/sync.d.ts.map +1 -1
  107. package/lib/sync/sync.js +7 -0
  108. package/lib/sync/sync.js.map +1 -1
  109. package/lib/sync/unknownBlock.d.ts.map +1 -1
  110. package/lib/sync/unknownBlock.js +7 -2
  111. package/lib/sync/unknownBlock.js.map +1 -1
  112. package/lib/util/execution.js +1 -1
  113. package/lib/util/execution.js.map +1 -1
  114. package/package.json +18 -16
  115. package/src/api/impl/beacon/blocks/index.ts +221 -63
  116. package/src/api/impl/validator/index.ts +206 -98
  117. package/src/api/rest/base.ts +8 -3
  118. package/src/chain/blocks/importBlock.ts +0 -17
  119. package/src/chain/blocks/utils/chainSegment.ts +16 -5
  120. package/src/chain/blocks/verifyBlocksExecutionPayloads.ts +1 -1
  121. package/src/chain/blocks/verifyBlocksSanityChecks.ts +1 -1
  122. package/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +2 -2
  123. package/src/chain/chain.ts +61 -2
  124. package/src/chain/errors/blockError.ts +44 -7
  125. package/src/chain/errors/executionPayloadEnvelope.ts +9 -0
  126. package/src/chain/errors/proposerPreferences.ts +16 -1
  127. package/src/chain/lightClient/index.ts +12 -7
  128. package/src/chain/lightClient/proofs.ts +36 -1
  129. package/src/chain/lightClient/types.ts +3 -1
  130. package/src/chain/opPools/aggregatedAttestationPool.ts +1 -1
  131. package/src/chain/opPools/proposerPreferencesPool.ts +1 -1
  132. package/src/chain/validation/aggregateAndProof.ts +12 -6
  133. package/src/chain/validation/attestation.ts +12 -6
  134. package/src/chain/validation/block.ts +69 -4
  135. package/src/chain/validation/executionPayloadBid.ts +33 -3
  136. package/src/chain/validation/executionPayloadEnvelope.ts +37 -0
  137. package/src/chain/validation/proposerPreferences.ts +71 -9
  138. package/src/chain/validation/signatureSets/aggregateAndProof.ts +3 -3
  139. package/src/db/repositories/lightclientSyncCommitteeWitness.ts +57 -9
  140. package/src/network/gossip/encoding.ts +13 -7
  141. package/src/network/gossip/gossipsub.ts +7 -2
  142. package/src/network/gossip/topic.ts +30 -0
  143. package/src/network/interface.ts +3 -1
  144. package/src/network/network.ts +24 -15
  145. package/src/network/processor/gossipHandlers.ts +12 -6
  146. package/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +4 -2
  147. package/src/network/reqresp/handlers/index.ts +21 -12
  148. package/src/network/util.ts +5 -0
  149. package/src/sync/sync.ts +10 -0
  150. package/src/sync/unknownBlock.ts +8 -2
  151. package/src/util/execution.ts +1 -1
@@ -1,17 +1,19 @@
1
- import {SLOTS_PER_EPOCH} from "@lodestar/params";
1
+ import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice";
2
+ import {GENESIS_SLOT, MIN_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
2
3
  import {
3
4
  computeEpochAtSlot,
5
+ computeStartSlotAtEpoch,
4
6
  createSingleSignatureSetFromComponents,
5
7
  getProposerPreferencesSigningRoot,
6
8
  } from "@lodestar/state-transition";
7
- import {ValidatorIndex, gloas} from "@lodestar/types";
9
+ import {Epoch, Slot, ValidatorIndex, gloas} from "@lodestar/types";
8
10
  import {toRootHex} from "@lodestar/utils";
9
11
  import {GossipAction, ProposerPreferencesError, ProposerPreferencesErrorCode} from "../errors/index.js";
10
12
  import {IBeaconChain} from "../index.js";
11
13
 
12
14
  /**
13
15
  * Validates a gossiped `SignedProposerPreferences` per
14
- * https://github.com/ethereum/consensus-specs/blob/master/specs/gloas/p2p-interface.md#proposer_preferences
16
+ * https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.13/specs/gloas/p2p-interface.md#new-proposer_preferences
15
17
  */
16
18
  export async function validateGossipProposerPreferences(
17
19
  chain: IBeaconChain,
@@ -22,9 +24,9 @@ export async function validateGossipProposerPreferences(
22
24
  const dependentRootHex = toRootHex(dependentRoot);
23
25
  const proposalEpoch = computeEpochAtSlot(proposalSlot);
24
26
 
25
- // [IGNORE] `preferences.proposal_slot` is in the current or next epoch.
27
+ // [IGNORE] `preferences.proposal_slot` is within the proposer lookahead.
26
28
  const currentEpoch = chain.clock.currentEpoch;
27
- if (proposalEpoch < currentEpoch || proposalEpoch > currentEpoch + 1) {
29
+ if (proposalEpoch < currentEpoch || proposalEpoch > currentEpoch + MIN_SEED_LOOKAHEAD) {
28
30
  throw new ProposerPreferencesError(GossipAction.IGNORE, {
29
31
  code: ProposerPreferencesErrorCode.INVALID_EPOCH,
30
32
  proposalSlot,
@@ -44,21 +46,56 @@ export async function validateGossipProposerPreferences(
44
46
  }
45
47
 
46
48
  // [IGNORE] The block with root `dependent_root` has been seen by the node.
49
+ const dependentBlock = chain.forkChoice.getBlockHexDefaultStatus(dependentRootHex);
50
+ if (dependentBlock === null) {
51
+ throw new ProposerPreferencesError(GossipAction.IGNORE, {
52
+ code: ProposerPreferencesErrorCode.UNKNOWN_DEPENDENT_ROOT,
53
+ proposalSlot,
54
+ dependentRoot: dependentRootHex,
55
+ });
56
+ }
57
+
58
+ const dependentEpoch = proposalEpoch - MIN_SEED_LOOKAHEAD;
59
+ const dependentRootSlot = getDependentRootSlot(dependentEpoch);
60
+
61
+ // [REJECT] The slot of the block with root `preferences.dependent_root` is strictly less than
62
+ // `compute_start_slot_at_epoch(compute_epoch_at_slot(preferences.proposal_slot) - MIN_SEED_LOOKAHEAD)`.
63
+ if (dependentBlock.slot > dependentRootSlot) {
64
+ throw new ProposerPreferencesError(GossipAction.REJECT, {
65
+ code: ProposerPreferencesErrorCode.INVALID_DEPENDENT_ROOT_SLOT,
66
+ dependentRoot: dependentRootHex,
67
+ dependentBlockSlot: dependentBlock.slot,
68
+ dependentRootSlot,
69
+ dependentEpoch,
70
+ });
71
+ }
72
+
73
+ // [IGNORE] `is_valid_dependent_root(store, preferences.dependent_root, epoch)` returns `True`,
74
+ // where `epoch` is `compute_epoch_at_slot(preferences.proposal_slot) - MIN_SEED_LOOKAHEAD`.
75
+ if (!isValidDependentRoot(chain.forkChoice, dependentBlock, dependentEpoch)) {
76
+ throw new ProposerPreferencesError(GossipAction.IGNORE, {
77
+ code: ProposerPreferencesErrorCode.INVALID_DEPENDENT_ROOT,
78
+ dependentRoot: dependentRootHex,
79
+ dependentBlockSlot: dependentBlock.slot,
80
+ dependentEpoch,
81
+ });
82
+ }
83
+
47
84
  // Resolve the proposer lookahead for the message's branch via head state (fast path) or
48
85
  // the previous-root checkpoint state (populated by `processSlotsToNearestCheckpoint` for
49
- // any imported branch crossing into `proposalEpoch - 1`). The head-state path also handles
86
+ // any imported branch crossing into `dependentEpoch`). The head-state path also handles
50
87
  // narrow timing windows where the checkpoint state isn't yet populated.
51
88
  const headState = chain.getHeadState();
52
89
  let proposers: ValidatorIndex[] | null = null;
53
90
  if (headState.epoch === proposalEpoch && headState.currentDecisionRoot === dependentRootHex) {
54
91
  proposers = headState.currentProposers;
55
- } else if (headState.epoch === proposalEpoch - 1 && headState.nextDecisionRoot === dependentRootHex) {
92
+ } else if (headState.epoch === dependentEpoch && headState.nextDecisionRoot === dependentRootHex) {
56
93
  proposers = headState.nextProposers;
57
94
  } else {
58
95
  // Sync lookup only to not trigger disk reload from gossip input.
59
- const checkpointState = chain.regen.getCheckpointStateSync({epoch: proposalEpoch - 1, rootHex: dependentRootHex});
96
+ const checkpointState = chain.regen.getCheckpointStateSync({epoch: dependentEpoch, rootHex: dependentRootHex});
60
97
  if (checkpointState !== null) {
61
- // State is at `proposalEpoch - 1`, so proposers for `proposalSlot` (next epoch from
98
+ // State is at `dependentEpoch`, so proposers for `proposalSlot` (next epoch from
62
99
  // the state's perspective) live in `nextProposers`.
63
100
  proposers = checkpointState.nextProposers;
64
101
  }
@@ -118,3 +155,28 @@ export async function validateGossipProposerPreferences(
118
155
 
119
156
  chain.proposerPreferencesPool.add(signedProposerPreferences);
120
157
  }
158
+
159
+ /**
160
+ * Check whether `dependentBlock` is a viable shuffling-dependent root for `epoch`:
161
+ * it is before the epoch boundary and either has a direct child at or after
162
+ * the boundary, or is the current fork-choice head.
163
+ */
164
+ export function isValidDependentRoot(forkChoice: IForkChoice, dependentBlock: ProtoBlock, epoch: Epoch): boolean {
165
+ const epochStartSlot = computeStartSlotAtEpoch(epoch);
166
+ if (dependentBlock.slot > getDependentRootSlot(epoch)) {
167
+ return false;
168
+ }
169
+
170
+ for (const block of forkChoice.getBlockSummariesByParentRoot(dependentBlock.blockRoot)) {
171
+ if (block.slot >= epochStartSlot) {
172
+ return true;
173
+ }
174
+ }
175
+
176
+ return dependentBlock.blockRoot === forkChoice.getHeadRoot();
177
+ }
178
+
179
+ /** Return the slot used to select a dependent root for `epoch`, clamped to genesis. */
180
+ function getDependentRootSlot(epoch: Epoch): Slot {
181
+ return Math.max(GENESIS_SLOT, computeStartSlotAtEpoch(epoch) - 1);
182
+ }
@@ -1,7 +1,7 @@
1
1
  import {BeaconConfig} from "@lodestar/config";
2
- import {DOMAIN_AGGREGATE_AND_PROOF, ForkSeq} from "@lodestar/params";
2
+ import {DOMAIN_AGGREGATE_AND_PROOF} from "@lodestar/params";
3
3
  import {ISignatureSet, SignatureSetType, computeSigningRoot, computeStartSlotAtEpoch} from "@lodestar/state-transition";
4
- import {Epoch, SignedAggregateAndProof, ValidatorIndex, ssz} from "@lodestar/types";
4
+ import {Epoch, SignedAggregateAndProof, ValidatorIndex} from "@lodestar/types";
5
5
 
6
6
  export function getAggregateAndProofSigningRoot(
7
7
  config: BeaconConfig,
@@ -14,7 +14,7 @@ export function getAggregateAndProofSigningRoot(
14
14
  const slot = computeStartSlotAtEpoch(epoch);
15
15
  const fork = config.getForkName(slot);
16
16
  const aggregatorDomain = config.getDomainAtFork(fork, DOMAIN_AGGREGATE_AND_PROOF);
17
- const sszType = ForkSeq[fork] >= ForkSeq.electra ? ssz.electra.AggregateAndProof : ssz.phase0.AggregateAndProof;
17
+ const sszType = config.getForkTypes(slot).AggregateAndProof;
18
18
  return computeSigningRoot(sszType, aggregateAndProof.message, aggregatorDomain);
19
19
  }
20
20
 
@@ -1,14 +1,16 @@
1
1
  import {ContainerType, VectorCompositeType} from "@chainsafe/ssz";
2
2
  import {ChainForkConfig} from "@lodestar/config";
3
3
  import {DatabaseController, Repository} from "@lodestar/db";
4
+ import {CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS, NEXT_SYNC_COMMITTEE_DEPTH_GLOAS} from "@lodestar/params";
4
5
  import {ssz} from "@lodestar/types";
5
6
  import {SyncCommitteeWitness} from "../../chain/lightClient/types.js";
6
7
  import {Bucket, getBucketNameByValue} from "../buckets.js";
7
8
 
8
- // We add a 1-byte prefix where 0 means pre-electra and 1 means post-electra
9
+ // We add a 1-byte prefix where 0 means pre-Electra, 1 means post-Electra, and 2 means post-Gloas.
9
10
  enum PrefixByte {
10
11
  PRE_ELECTRA = 0,
11
12
  POST_ELECTRA = 1,
13
+ POST_GLOAS = 2,
12
14
  }
13
15
 
14
16
  export const NUM_WITNESS = 4;
@@ -35,6 +37,35 @@ export class SyncCommitteeWitnessRepository extends Repository<Uint8Array, SyncC
35
37
  // Overrides for multi-fork
36
38
  encodeValue(value: SyncCommitteeWitness): Uint8Array {
37
39
  const numWitness = value.witness.length;
40
+ const hasGloasBranches =
41
+ value.currentSyncCommitteeBranch !== undefined || value.nextSyncCommitteeBranch !== undefined;
42
+
43
+ if (hasGloasBranches) {
44
+ if (
45
+ value.currentSyncCommitteeBranch?.length !== CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS ||
46
+ value.nextSyncCommitteeBranch?.length !== NEXT_SYNC_COMMITTEE_DEPTH_GLOAS
47
+ ) {
48
+ throw Error(
49
+ `Invalid post-Gloas sync committee branch lengths current=${value.currentSyncCommitteeBranch?.length} next=${value.nextSyncCommitteeBranch?.length}`
50
+ );
51
+ }
52
+
53
+ const type = new ContainerType({
54
+ currentSyncCommitteeBranch: new VectorCompositeType(ssz.Root, CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS),
55
+ nextSyncCommitteeBranch: new VectorCompositeType(ssz.Root, NEXT_SYNC_COMMITTEE_DEPTH_GLOAS),
56
+ currentSyncCommitteeRoot: ssz.Root,
57
+ nextSyncCommitteeRoot: ssz.Root,
58
+ });
59
+
60
+ const valueBytes = type.serialize({
61
+ currentSyncCommitteeBranch: value.currentSyncCommitteeBranch,
62
+ nextSyncCommitteeBranch: value.nextSyncCommitteeBranch,
63
+ currentSyncCommitteeRoot: value.currentSyncCommitteeRoot,
64
+ nextSyncCommitteeRoot: value.nextSyncCommitteeRoot,
65
+ });
66
+
67
+ return prefixData(PrefixByte.POST_GLOAS, valueBytes);
68
+ }
38
69
 
39
70
  if (numWitness !== NUM_WITNESS && numWitness !== NUM_WITNESS_ELECTRA) {
40
71
  throw Error(`Number of witness can only be 4 pre-electra or 5 post-electra numWitness=${numWitness}`);
@@ -51,19 +82,25 @@ export class SyncCommitteeWitnessRepository extends Repository<Uint8Array, SyncC
51
82
  // We need to differentiate between post-electra and pre-electra witness
52
83
  // such that we can deserialize correctly
53
84
  const isPostElectra = numWitness === NUM_WITNESS_ELECTRA;
54
- const prefixByte = new Uint8Array(1);
55
- prefixByte[0] = isPostElectra ? PrefixByte.POST_ELECTRA : PrefixByte.PRE_ELECTRA;
56
-
57
- const prefixedData = new Uint8Array(1 + valueBytes.length);
58
- prefixedData.set(prefixByte, 0);
59
- prefixedData.set(valueBytes, 1);
60
-
61
- return prefixedData;
85
+ return prefixData(isPostElectra ? PrefixByte.POST_ELECTRA : PrefixByte.PRE_ELECTRA, valueBytes);
62
86
  }
63
87
 
64
88
  decodeValue(data: Uint8Array): SyncCommitteeWitness {
65
89
  // First byte is written
66
90
  const prefix = data.subarray(0, 1);
91
+ const isPostGloas = prefix[0] === PrefixByte.POST_GLOAS;
92
+
93
+ if (isPostGloas) {
94
+ const type = new ContainerType({
95
+ currentSyncCommitteeBranch: new VectorCompositeType(ssz.Root, CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS),
96
+ nextSyncCommitteeBranch: new VectorCompositeType(ssz.Root, NEXT_SYNC_COMMITTEE_DEPTH_GLOAS),
97
+ currentSyncCommitteeRoot: ssz.Root,
98
+ nextSyncCommitteeRoot: ssz.Root,
99
+ });
100
+
101
+ return {witness: [], ...type.deserialize(data.subarray(1))};
102
+ }
103
+
67
104
  const isPostElectra = prefix[0] === PrefixByte.POST_ELECTRA;
68
105
 
69
106
  const type = new ContainerType({
@@ -75,3 +112,14 @@ export class SyncCommitteeWitnessRepository extends Repository<Uint8Array, SyncC
75
112
  return type.deserialize(data.subarray(1));
76
113
  }
77
114
  }
115
+
116
+ function prefixData(prefix: PrefixByte, valueBytes: Uint8Array): Uint8Array {
117
+ const prefixByte = new Uint8Array(1);
118
+ prefixByte[0] = prefix;
119
+
120
+ const prefixedData = new Uint8Array(1 + valueBytes.length);
121
+ prefixedData.set(prefixByte, 0);
122
+ prefixedData.set(valueBytes, 1);
123
+
124
+ return prefixedData;
125
+ }
@@ -9,7 +9,7 @@ import {ForkName} from "@lodestar/params";
9
9
  import {intToBytes} from "@lodestar/utils";
10
10
  import {MESSAGE_DOMAIN_VALID_SNAPPY} from "./constants.js";
11
11
  import {Eth2GossipsubMetrics} from "./metrics.js";
12
- import {GossipTopicCache, getGossipSSZType} from "./topic.js";
12
+ import {GossipTopicCache, getGossipSSZMaxSize, getGossipSSZType} from "./topic.js";
13
13
 
14
14
  // Load WASM
15
15
  const xxhash = await xxhashFactory();
@@ -98,7 +98,7 @@ export function msgIdFn(gossipTopicCache: GossipTopicCache, msg: Message): Uint8
98
98
  export class DataTransformSnappy implements DataTransform {
99
99
  constructor(
100
100
  private readonly gossipTopicCache: GossipTopicCache,
101
- private readonly maxSizePerMessage: number,
101
+ private readonly maxPayloadSize: number,
102
102
  private readonly metrics: Eth2GossipsubMetrics | null
103
103
  ) {}
104
104
 
@@ -111,17 +111,18 @@ export class DataTransformSnappy implements DataTransform {
111
111
  inboundTransform(topicStr: string, data: Uint8Array): Uint8Array {
112
112
  // check uncompressed data length before we actually decompress
113
113
  const uncompressedDataLength = snappyWasm.decompress_len(data);
114
- if (uncompressedDataLength > this.maxSizePerMessage) {
115
- throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${this.maxSizePerMessage}`);
116
- }
117
114
 
118
115
  const topic = this.gossipTopicCache.getTopic(topicStr);
119
116
  const sszType = getGossipSSZType(topic);
117
+ const maxSize = getGossipSSZMaxSize(topic, this.maxPayloadSize, sszType);
120
118
  this.metrics?.dataTransform.inbound.inc({type: topic.type});
121
119
 
122
120
  if (uncompressedDataLength < sszType.minSize) {
123
121
  throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} < ${sszType.minSize}`);
124
122
  }
123
+ if (uncompressedDataLength > maxSize) {
124
+ throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${maxSize}`);
125
+ }
125
126
  if (uncompressedDataLength > sszType.maxSize) {
126
127
  throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${sszType.maxSize}`);
127
128
  }
@@ -139,9 +140,14 @@ export class DataTransformSnappy implements DataTransform {
139
140
  */
140
141
  outboundTransform(topicStr: string, data: Uint8Array): Uint8Array {
141
142
  const topic = this.gossipTopicCache.getTopic(topicStr);
143
+ const sszType = getGossipSSZType(topic);
144
+ const maxSize = getGossipSSZMaxSize(topic, this.maxPayloadSize, sszType);
142
145
  this.metrics?.dataTransform.outbound.inc({type: topic.type});
143
- if (data.length > this.maxSizePerMessage) {
144
- throw Error(`ssz_snappy encoded data length ${data.length} > ${this.maxSizePerMessage}`);
146
+ if (data.length > maxSize) {
147
+ throw Error(`ssz_snappy encoded data length ${data.length} > ${maxSize}`);
148
+ }
149
+ if (data.length > sszType.maxSize) {
150
+ throw Error(`ssz_snappy encoded data length ${data.length} > ${sszType.maxSize}`);
145
151
  }
146
152
 
147
153
  // Using Buffer.alloc() instead of Buffer.allocUnsafe() to mitigate high GC pressure observed in some environments
@@ -15,7 +15,12 @@ import {type Multiaddr, multiaddr} from "@multiformats/multiaddr";
15
15
  import {ENR} from "@chainsafe/enr";
16
16
  import {routes} from "@lodestar/api";
17
17
  import {BeaconConfig, ForkBoundary} from "@lodestar/config";
18
- import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params";
18
+ import {
19
+ ATTESTATION_SUBNET_COUNT,
20
+ MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE,
21
+ SLOTS_PER_EPOCH,
22
+ SYNC_COMMITTEE_SUBNET_COUNT,
23
+ } from "@lodestar/params";
19
24
  import {SubnetID} from "@lodestar/types";
20
25
  import {Logger, Map2d, Map2dArr} from "@lodestar/utils";
21
26
  import {RegistryMetricCreator} from "../../metrics/index.js";
@@ -179,7 +184,7 @@ export class Eth2Gossipsub {
179
184
  // Only send IDONTWANT messages if the message size is larger than this
180
185
  // This should be large enough to not send IDONTWANT for "small" messages
181
186
  // See https://github.com/ChainSafe/lodestar/pull/7077#issuecomment-2383679472
182
- idontwantMinDataSize: 16829,
187
+ idontwantMinDataSize: MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE,
183
188
  })(modules.libp2p.services.components) as GossipSubInternal;
184
189
 
185
190
  if (metrics) {
@@ -1,12 +1,18 @@
1
+ import {type CompositeTypeAny} from "@chainsafe/ssz";
1
2
  import {ForkDigestContext} from "@lodestar/config";
2
3
  import {
3
4
  ATTESTATION_SUBNET_COUNT,
4
5
  ForkName,
5
6
  ForkSeq,
7
+ MAX_ATTESTER_SLASHING_SIZE,
8
+ MAX_DATA_COLUMN_SIDECAR_SIZE,
9
+ MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE,
10
+ MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE,
6
11
  SYNC_COMMITTEE_SUBNET_COUNT,
7
12
  isForkPostAltair,
8
13
  isForkPostElectra,
9
14
  isForkPostFulu,
15
+ isForkPostGloas,
10
16
  } from "@lodestar/params";
11
17
  import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types";
12
18
  import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js";
@@ -130,6 +136,30 @@ export function getGossipSSZType(topic: GossipTopic) {
130
136
  }
131
137
  }
132
138
 
139
+ /**
140
+ * Return the maximum uncompressed SSZ byte length accepted for a gossip object.
141
+ */
142
+ export function getGossipSSZMaxSize(topic: GossipTopic, maxPayloadSize: number, sszType?: CompositeTypeAny): number {
143
+ const {fork} = topic.boundary;
144
+ // Gloas progressive containers have broad theoretical SSZ max sizes; use the preset p2p bounds instead.
145
+ switch (topic.type) {
146
+ case GossipType.beacon_block:
147
+ return maxPayloadSize;
148
+ case GossipType.beacon_aggregate_and_proof:
149
+ return isForkPostGloas(fork) ? MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize;
150
+ case GossipType.attester_slashing:
151
+ return isForkPostGloas(fork) ? MAX_ATTESTER_SLASHING_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize;
152
+ case GossipType.data_column_sidecar:
153
+ return isForkPostGloas(fork) ? MAX_DATA_COLUMN_SIDECAR_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize;
154
+ case GossipType.execution_payload:
155
+ return maxPayloadSize;
156
+ case GossipType.execution_payload_bid:
157
+ return MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE;
158
+ default:
159
+ return (sszType ?? getGossipSSZType(topic)).maxSize;
160
+ }
161
+ }
162
+
133
163
  /**
134
164
  * Deserialize a gossip serialized data into an ssz object.
135
165
  */
@@ -103,7 +103,9 @@ export interface INetwork extends INetworkCorePublic {
103
103
  publishBlobSidecar(blobSidecar: deneb.BlobSidecar): Promise<number>;
104
104
  publishBeaconAggregateAndProof(aggregateAndProof: SignedAggregateAndProof): Promise<number>;
105
105
  publishBeaconAttestation(attestation: SingleAttestation, subnet: SubnetID): Promise<number>;
106
- publishDataColumnSidecar(dataColumnSideCar: DataColumnSidecar): Promise<number>;
106
+ publishDataColumnSidecar(
107
+ dataColumnSideCar: DataColumnSidecar
108
+ ): Promise<{sentPeers: number; alreadyPublished: boolean}>;
107
109
  publishVoluntaryExit(voluntaryExit: phase0.SignedVoluntaryExit): Promise<number>;
108
110
  publishBlsToExecutionChange(blsToExecutionChange: capella.SignedBLSToExecutionChange): Promise<number>;
109
111
  publishProposerSlashing(proposerSlashing: phase0.ProposerSlashing): Promise<number>;
@@ -65,7 +65,7 @@ import {
65
65
  } from "./reqresp/utils/collect.js";
66
66
  import {collectSequentialBlocksInRange} from "./reqresp/utils/collectSequentialBlocksInRange.js";
67
67
  import {CommitteeSubscription} from "./subnets/index.js";
68
- import {isPublishToZeroPeersError, prettyPrintPeerIdStr} from "./util.js";
68
+ import {isPublishDuplicateError, isPublishToZeroPeersError, prettyPrintPeerIdStr} from "./util.js";
69
69
 
70
70
  type NetworkModules = {
71
71
  opts: NetworkOptions;
@@ -366,7 +366,9 @@ export class Network implements INetwork {
366
366
  });
367
367
  }
368
368
 
369
- async publishDataColumnSidecar(dataColumnSidecar: DataColumnSidecar): Promise<number> {
369
+ async publishDataColumnSidecar(
370
+ dataColumnSidecar: DataColumnSidecar
371
+ ): Promise<{sentPeers: number; alreadyPublished: boolean}> {
370
372
  const slot = isGloasDataColumnSidecar(dataColumnSidecar)
371
373
  ? dataColumnSidecar.slot
372
374
  : dataColumnSidecar.signedBlockHeader.message.slot;
@@ -374,18 +376,25 @@ export class Network implements INetwork {
374
376
  const boundary = this.config.getForkBoundaryAtEpoch(epoch);
375
377
 
376
378
  const subnet = computeSubnetForDataColumnSidecar(this.config, dataColumnSidecar);
377
- return this.publishGossip<GossipType.data_column_sidecar>(
378
- {type: GossipType.data_column_sidecar, boundary, subnet},
379
- dataColumnSidecar,
380
- {
381
- ignoreDuplicatePublishError: true,
382
- // we ensure having all topic peers via prioritizePeers() function
383
- // in the worse case, if there is 0 peer on the topic, the overall publish operation could be still a success
384
- // because supernode will rebuild and publish missing data column sidecars for us
385
- // hence we want to track sent peers as 0 instead of an error
386
- allowPublishToZeroTopicPeers: true,
379
+ try {
380
+ const sentPeers = await this.publishGossip<GossipType.data_column_sidecar>(
381
+ {type: GossipType.data_column_sidecar, boundary, subnet},
382
+ dataColumnSidecar,
383
+ {
384
+ // we ensure having all topic peers via prioritizePeers() function
385
+ // in the worse case, if there is 0 peer on the topic, the overall publish operation could be still a success
386
+ // because supernode will rebuild and publish missing data column sidecars for us
387
+ // hence we want to track sent peers as 0 instead of an error
388
+ allowPublishToZeroTopicPeers: true,
389
+ }
390
+ );
391
+ return {sentPeers, alreadyPublished: false};
392
+ } catch (e) {
393
+ if (isPublishDuplicateError(e as Error)) {
394
+ return {sentPeers: 0, alreadyPublished: true};
387
395
  }
388
- );
396
+ throw e;
397
+ }
389
398
  }
390
399
 
391
400
  async publishBeaconAggregateAndProof(aggregateAndProof: SignedAggregateAndProof): Promise<number> {
@@ -860,8 +869,8 @@ export class Network implements INetwork {
860
869
  this.core.setTargetGroupCount(count);
861
870
  };
862
871
 
863
- private onPublishDataColumns = (sidecars: DataColumnSidecar[]): Promise<number[]> => {
864
- return promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishDataColumnSidecar(sidecar)));
872
+ private onPublishDataColumns = async (sidecars: DataColumnSidecar[]): Promise<void> => {
873
+ await promiseAllMaybeAsync(sidecars.map((sidecar) => () => this.publishDataColumnSidecar(sidecar)));
865
874
  };
866
875
 
867
876
  private onPublishBlobSidecars = (sidecars: deneb.BlobSidecar[]): Promise<number[]> => {
@@ -176,7 +176,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
176
176
 
177
177
  // optimistically add gossip block to the seen cache
178
178
  // if validation fails, we will NOT forward this gossip block to peers
179
- // - if PARENT_UNKNOWN error, blockInput will then be queued inside BlockInputSync. If the gossip block is really invalid, it will be pruned there
179
+ // - if PARENT_BLOCK_UNKNOWN error, blockInput will then be queued inside BlockInputSync. If the gossip block is really invalid, it will be pruned there
180
180
  // - if other validator errors, blockInput will stay in the seen cache and will be pruned on finalization
181
181
  const blockInput = chain.seenBlockInputCache.getByBlock({
182
182
  block: signedBlock,
@@ -187,7 +187,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
187
187
  });
188
188
 
189
189
  // Optimistically seed the payload-envelope cache too, mirroring seenBlockInputCache above.
190
- // This ensures we have PayloadEnvelopeInput, even through "PARENT_UNKNOWN" error
190
+ // This ensures we have PayloadEnvelopeInput, even through "PARENT_BLOCK_UNKNOWN" error
191
191
  // see https://github.com/ChainSafe/lodestar/issues/9475
192
192
  if (isForkPostGloas(fork)) {
193
193
  chain.seenPayloadEnvelopeInputCache.add({
@@ -227,7 +227,8 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
227
227
  if (e instanceof BlockGossipError) {
228
228
  logger.debug("Gossip block has error", {slot, root: blockShortHex, code: e.type.code});
229
229
  if (
230
- (e.type.code === BlockErrorCode.PARENT_UNKNOWN || e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) &&
230
+ (e.type.code === BlockErrorCode.PARENT_BLOCK_UNKNOWN ||
231
+ e.type.code === BlockErrorCode.PARENT_PAYLOAD_UNKNOWN) &&
231
232
  blockInput
232
233
  ) {
233
234
  chain.emitter.emit(ChainEvent.blockUnknownParent, {
@@ -661,14 +662,19 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
661
662
  break;
662
663
  }
663
664
  // ALREADY_KNOWN should not happen with ignoreIfKnown=true above
664
- // PARENT_UNKNOWN should not happen, we handled this in validateBeaconBlock() function above
665
+ // PARENT_BLOCK_UNKNOWN should not happen, we handled this in validateBeaconBlock() function above
665
666
  case BlockErrorCode.ALREADY_KNOWN:
666
- case BlockErrorCode.PARENT_UNKNOWN:
667
+ case BlockErrorCode.PARENT_BLOCK_UNKNOWN:
667
668
  case BlockErrorCode.PRESTATE_MISSING:
668
669
  case BlockErrorCode.EXECUTION_ENGINE_ERROR:
669
- // Errors might indicate an issue with our node or the connected EL client
670
+ // Errors might indicate an issue with our node or the connected EL client.
670
671
  logLevel = LogLevel.error;
671
672
  break;
673
+ case BlockErrorCode.EXECUTION_ENGINE_INVALID:
674
+ // the peer served a bad block
675
+ core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "ExecutionEngineInvalid");
676
+ logLevel = LogLevel.warn;
677
+ break;
672
678
  default:
673
679
  // TODO: Should it use PeerId or string?
674
680
  core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipBlock");
@@ -107,8 +107,10 @@ export function validateExecutionPayloadEnvelopesByRangeRequest(
107
107
  // The gloas req/resp spec uses MIN_EPOCHS_FOR_BLOCK_REQUESTS to define the minimum range peers MUST serve.
108
108
  // Archival nodes may still serve older retained payloads to allow genesis sync.
109
109
 
110
- if (count > config.MAX_REQUEST_BLOCKS_DENEB) {
111
- count = config.MAX_REQUEST_BLOCKS_DENEB;
110
+ // Spec: EnvelopesByRange response is bounded by MAX_REQUEST_PAYLOADS (consensus-specs #5383),
111
+ // distinct from the MAX_REQUEST_BLOCKS_DENEB cap used for block-by-range.
112
+ if (count > config.MAX_REQUEST_PAYLOADS) {
113
+ count = config.MAX_REQUEST_PAYLOADS;
112
114
  }
113
115
 
114
116
  return {startSlot, count};
@@ -1,4 +1,5 @@
1
- import {ProtocolHandler} from "@lodestar/reqresp";
1
+ import type {Type} from "@chainsafe/ssz";
2
+ import {ProtocolHandler, RespStatus, ResponseError} from "@lodestar/reqresp";
2
3
  import {ssz} from "@lodestar/types";
3
4
  import {IBeaconChain} from "../../../chain/index.js";
4
5
  import {IBeaconDb} from "../../../db/index.js";
@@ -29,6 +30,14 @@ function notImplemented(method: ReqRespMethod): ProtocolHandler {
29
30
  };
30
31
  }
31
32
 
33
+ function deserializeRequestBody<T>(type: Type<T>, data: Uint8Array): T {
34
+ try {
35
+ return type.deserialize(data);
36
+ } catch (e) {
37
+ throw new ResponseError(RespStatus.INVALID_REQUEST, e instanceof Error ? e.message : String(e));
38
+ }
39
+ }
40
+
32
41
  /**
33
42
  * The ReqRespHandler module handles app-level requests / responses from other peers,
34
43
  * fetching state from the chain and database as needed.
@@ -40,51 +49,51 @@ export function getReqRespHandlers({db, chain}: {db: IBeaconDb; chain: IBeaconCh
40
49
  [ReqRespMethod.Ping]: notImplemented(ReqRespMethod.Ping),
41
50
  [ReqRespMethod.Metadata]: notImplemented(ReqRespMethod.Metadata),
42
51
  [ReqRespMethod.BeaconBlocksByRange]: (req, peerId, peerClient) => {
43
- const body = ssz.phase0.BeaconBlocksByRangeRequest.deserialize(req.data);
52
+ const body = deserializeRequestBody(ssz.phase0.BeaconBlocksByRangeRequest, req.data);
44
53
  return onBeaconBlocksByRange(body, chain, db, peerId, peerClient);
45
54
  },
46
55
  [ReqRespMethod.BeaconBlocksByRoot]: (req) => {
47
56
  const fork = chain.config.getForkName(chain.clock.currentSlot);
48
- const body = BeaconBlocksByRootRequestType(fork, chain.config).deserialize(req.data);
57
+ const body = deserializeRequestBody(BeaconBlocksByRootRequestType(fork, chain.config), req.data);
49
58
  return onBeaconBlocksByRoot(body, chain);
50
59
  },
51
60
  [ReqRespMethod.BeaconBlocksByHead]: (req, peerId, peerClient) => {
52
- const body = ssz.fulu.BeaconBlocksByHeadRequest.deserialize(req.data);
61
+ const body = deserializeRequestBody(ssz.fulu.BeaconBlocksByHeadRequest, req.data);
53
62
  return onBeaconBlocksByHead(body, chain, peerId, peerClient);
54
63
  },
55
64
  [ReqRespMethod.BlobSidecarsByRoot]: (req) => {
56
65
  const fork = chain.config.getForkName(chain.clock.currentSlot);
57
- const body = BlobSidecarsByRootRequestType(fork, chain.config).deserialize(req.data);
66
+ const body = deserializeRequestBody(BlobSidecarsByRootRequestType(fork, chain.config), req.data);
58
67
  return onBlobSidecarsByRoot(body, chain);
59
68
  },
60
69
  [ReqRespMethod.BlobSidecarsByRange]: (req) => {
61
- const body = ssz.deneb.BlobSidecarsByRangeRequest.deserialize(req.data);
70
+ const body = deserializeRequestBody(ssz.deneb.BlobSidecarsByRangeRequest, req.data);
62
71
  return onBlobSidecarsByRange(body, chain, db);
63
72
  },
64
73
  [ReqRespMethod.DataColumnSidecarsByRange]: (req, peerId, peerClient) => {
65
- const body = ssz.fulu.DataColumnSidecarsByRangeRequest.deserialize(req.data);
74
+ const body = deserializeRequestBody(ssz.fulu.DataColumnSidecarsByRangeRequest, req.data);
66
75
  return onDataColumnSidecarsByRange(body, chain, db, peerId, peerClient);
67
76
  },
68
77
  [ReqRespMethod.DataColumnSidecarsByRoot]: (req, peerId, peerClient) => {
69
- const body = DataColumnSidecarsByRootRequestType(chain.config).deserialize(req.data);
78
+ const body = deserializeRequestBody(DataColumnSidecarsByRootRequestType(chain.config), req.data);
70
79
  return onDataColumnSidecarsByRoot(body, chain, db, peerId, peerClient);
71
80
  },
72
81
 
73
82
  [ReqRespMethod.ExecutionPayloadEnvelopesByRoot]: (req, peerId, peerClient) => {
74
- const body = ExecutionPayloadEnvelopesByRootRequestType(chain.config).deserialize(req.data);
83
+ const body = deserializeRequestBody(ExecutionPayloadEnvelopesByRootRequestType(chain.config), req.data);
75
84
  return onExecutionPayloadEnvelopesByRoot(body, chain, db, peerId, peerClient);
76
85
  },
77
86
  [ReqRespMethod.ExecutionPayloadEnvelopesByRange]: (req, peerId, peerClient) => {
78
- const body = ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest.deserialize(req.data);
87
+ const body = deserializeRequestBody(ssz.gloas.ExecutionPayloadEnvelopesByRangeRequest, req.data);
79
88
  return onExecutionPayloadEnvelopesByRange(body, chain, db, peerId, peerClient);
80
89
  },
81
90
 
82
91
  [ReqRespMethod.LightClientBootstrap]: (req) => {
83
- const body = ssz.Root.deserialize(req.data);
92
+ const body = deserializeRequestBody(ssz.Root, req.data);
84
93
  return onLightClientBootstrap(body, chain);
85
94
  },
86
95
  [ReqRespMethod.LightClientUpdatesByRange]: (req) => {
87
- const body = ssz.altair.LightClientUpdatesByRange.deserialize(req.data);
96
+ const body = deserializeRequestBody(ssz.altair.LightClientUpdatesByRange, req.data);
88
97
  return onLightClientUpdatesByRange(body, chain);
89
98
  },
90
99
  [ReqRespMethod.LightClientFinalityUpdate]: () => onLightClientFinalityUpdate(chain),
@@ -27,3 +27,8 @@ export function getConnection(libp2p: Libp2p, peerIdStr: string): Connection | u
27
27
  export function isPublishToZeroPeersError(e: Error): boolean {
28
28
  return e.message.includes("PublishError.NoPeersSubscribedToTopic");
29
29
  }
30
+
31
+ // https://github.com/libp2p/js-libp2p/blob/f87cba928991736d9646b3e054c367f55cab315c/packages/gossipsub/src/gossipsub.ts#L2076
32
+ export function isPublishDuplicateError(e: Error): boolean {
33
+ return e.message.includes("PublishError.Duplicate");
34
+ }
package/src/sync/sync.ts CHANGED
@@ -75,6 +75,11 @@ export class BeaconSync implements IBeaconSync {
75
75
  if (metrics) {
76
76
  metrics.syncStatus.addCollect(() => this.scrapeMetrics(metrics));
77
77
  }
78
+
79
+ // A from-scratch or large-gap start boots not synced; updateSyncState resumes once synced
80
+ if (this.state !== SyncState.Synced) {
81
+ this.chain.forkChoice.pauseFastConfirmation();
82
+ }
78
83
  }
79
84
 
80
85
  close(): void {
@@ -228,6 +233,8 @@ export class BeaconSync implements IBeaconSync {
228
233
 
229
234
  // We have become synced, subscribe to all the gossip core topics
230
235
  if (state === SyncState.Synced && this.chain.clock.currentEpoch >= MIN_EPOCH_TO_START_GOSSIP) {
236
+ this.chain.forkChoice.resumeFastConfirmation();
237
+
231
238
  if (!this.network.isSubscribedToGossipCoreTopics()) {
232
239
  this.network
233
240
  .subscribeGossipCoreTopics()
@@ -251,6 +258,9 @@ export class BeaconSync implements IBeaconSync {
251
258
  else if (state !== SyncState.Synced) {
252
259
  const syncDiff = this.chain.clock.currentSlot - this.chain.forkChoice.getHead().slot;
253
260
  if (syncDiff > this.slotImportTolerance * 2) {
261
+ // Same debounce as gossip: transient blips keep the rule running, only a real gap pauses it
262
+ this.chain.forkChoice.pauseFastConfirmation();
263
+
254
264
  if (this.network.isSubscribedToGossipCoreTopics()) {
255
265
  this.logger.warn(`Node sync has fallen behind by ${syncDiff} slots`);
256
266
  this.network