@lodestar/beacon-node 1.43.0-dev.be5f70c0c8 → 1.43.0-dev.c98da75ec7
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.
- package/lib/api/impl/beacon/pool/index.d.ts.map +1 -1
- package/lib/api/impl/beacon/pool/index.js +46 -5
- package/lib/api/impl/beacon/pool/index.js.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.d.ts +1 -3
- package/lib/chain/blocks/utils/chainSegment.d.ts.map +1 -1
- package/lib/chain/blocks/utils/chainSegment.js +29 -26
- package/lib/chain/blocks/utils/chainSegment.js.map +1 -1
- package/lib/chain/chain.d.ts +2 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +3 -1
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/errors/executionPayloadBid.d.ts +19 -1
- package/lib/chain/errors/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/errors/executionPayloadBid.js +3 -0
- package/lib/chain/errors/executionPayloadBid.js.map +1 -1
- package/lib/chain/interface.d.ts +2 -1
- package/lib/chain/interface.d.ts.map +1 -1
- package/lib/chain/interface.js.map +1 -1
- package/lib/chain/lightClient/index.d.ts.map +1 -1
- package/lib/chain/lightClient/index.js +1 -1
- package/lib/chain/lightClient/index.js.map +1 -1
- package/lib/chain/opPools/index.d.ts +1 -0
- package/lib/chain/opPools/index.d.ts.map +1 -1
- package/lib/chain/opPools/index.js +1 -0
- package/lib/chain/opPools/index.js.map +1 -1
- package/lib/chain/opPools/payloadAttestationPool.d.ts +1 -1
- package/lib/chain/opPools/payloadAttestationPool.d.ts.map +1 -1
- package/lib/chain/opPools/payloadAttestationPool.js +30 -10
- package/lib/chain/opPools/payloadAttestationPool.js.map +1 -1
- package/lib/chain/opPools/proposerPreferencesPool.d.ts +29 -0
- package/lib/chain/opPools/proposerPreferencesPool.d.ts.map +1 -0
- package/lib/chain/opPools/proposerPreferencesPool.js +56 -0
- package/lib/chain/opPools/proposerPreferencesPool.js.map +1 -0
- package/lib/chain/validation/executionPayloadBid.d.ts.map +1 -1
- package/lib/chain/validation/executionPayloadBid.js +64 -16
- package/lib/chain/validation/executionPayloadBid.js.map +1 -1
- package/lib/chain/validation/payloadAttestationMessage.d.ts +1 -1
- package/lib/chain/validation/payloadAttestationMessage.d.ts.map +1 -1
- package/lib/chain/validation/payloadAttestationMessage.js +5 -3
- package/lib/chain/validation/payloadAttestationMessage.js.map +1 -1
- package/lib/metrics/metrics/lodestar.d.ts +4 -0
- package/lib/metrics/metrics/lodestar.d.ts.map +1 -1
- package/lib/metrics/metrics/lodestar.js +5 -0
- package/lib/metrics/metrics/lodestar.js.map +1 -1
- package/lib/network/interface.d.ts +1 -0
- package/lib/network/interface.d.ts.map +1 -1
- package/lib/network/network.d.ts +1 -0
- package/lib/network/network.d.ts.map +1 -1
- package/lib/network/network.js +5 -0
- package/lib/network/network.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +39 -4
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/sync/range/batch.d.ts +20 -3
- package/lib/sync/range/batch.d.ts.map +1 -1
- package/lib/sync/range/batch.js +54 -10
- package/lib/sync/range/batch.js.map +1 -1
- package/lib/sync/range/chain.d.ts.map +1 -1
- package/lib/sync/range/chain.js +3 -1
- package/lib/sync/range/chain.js.map +1 -1
- package/lib/sync/range/range.d.ts.map +1 -1
- package/lib/sync/range/range.js +40 -2
- package/lib/sync/range/range.js.map +1 -1
- package/lib/sync/range/utils/peerBalancer.d.ts +2 -1
- package/lib/sync/range/utils/peerBalancer.d.ts.map +1 -1
- package/lib/sync/range/utils/peerBalancer.js +8 -4
- package/lib/sync/range/utils/peerBalancer.js.map +1 -1
- package/lib/sync/utils/downloadByRange.d.ts +7 -1
- package/lib/sync/utils/downloadByRange.d.ts.map +1 -1
- package/lib/sync/utils/downloadByRange.js +2 -0
- package/lib/sync/utils/downloadByRange.js.map +1 -1
- package/lib/util/dependentRoot.d.ts +6 -2
- package/lib/util/dependentRoot.d.ts.map +1 -1
- package/lib/util/dependentRoot.js +20 -16
- package/lib/util/dependentRoot.js.map +1 -1
- package/package.json +14 -15
- package/src/api/impl/beacon/pool/index.ts +56 -3
- package/src/chain/blocks/utils/chainSegment.ts +30 -27
- package/src/chain/chain.ts +3 -0
- package/src/chain/errors/executionPayloadBid.ts +22 -1
- package/src/chain/interface.ts +2 -0
- package/src/chain/lightClient/index.ts +6 -6
- package/src/chain/opPools/index.ts +1 -0
- package/src/chain/opPools/payloadAttestationPool.ts +34 -10
- package/src/chain/opPools/proposerPreferencesPool.ts +59 -0
- package/src/chain/validation/executionPayloadBid.ts +67 -17
- package/src/chain/validation/payloadAttestationMessage.ts +6 -4
- package/src/metrics/metrics/lodestar.ts +6 -0
- package/src/network/interface.ts +1 -0
- package/src/network/network.ts +11 -0
- package/src/network/processor/gossipHandlers.ts +43 -3
- package/src/sync/range/batch.ts +71 -11
- package/src/sync/range/chain.ts +3 -1
- package/src/sync/range/range.ts +52 -2
- package/src/sync/range/utils/peerBalancer.ts +9 -3
- package/src/sync/utils/downloadByRange.ts +8 -0
- package/src/util/dependentRoot.ts +22 -18
|
@@ -57,7 +57,7 @@ export class PayloadAttestationPool {
|
|
|
57
57
|
add(
|
|
58
58
|
message: gloas.PayloadAttestationMessage,
|
|
59
59
|
payloadAttDataRootHex: RootHex,
|
|
60
|
-
|
|
60
|
+
validatorCommitteeIndices: number[]
|
|
61
61
|
): InsertOutcome {
|
|
62
62
|
const slot = message.data.slot;
|
|
63
63
|
const lowestPermissibleSlot = this.lowestPermissibleSlot;
|
|
@@ -85,10 +85,10 @@ export class PayloadAttestationPool {
|
|
|
85
85
|
const aggregate = aggregateByDataRoot.get(payloadAttDataRootHex);
|
|
86
86
|
if (aggregate) {
|
|
87
87
|
// Aggregate msg into aggregate
|
|
88
|
-
return aggregateMessageInto(message,
|
|
88
|
+
return aggregateMessageInto(message, validatorCommitteeIndices, aggregate);
|
|
89
89
|
}
|
|
90
90
|
// Create a new aggregate with data
|
|
91
|
-
aggregateByDataRoot.set(payloadAttDataRootHex, messageToAggregate(message,
|
|
91
|
+
aggregateByDataRoot.set(payloadAttDataRootHex, messageToAggregate(message, validatorCommitteeIndices));
|
|
92
92
|
|
|
93
93
|
return InsertOutcome.NewData;
|
|
94
94
|
}
|
|
@@ -150,25 +150,49 @@ export class PayloadAttestationPool {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function messageToAggregate(
|
|
153
|
+
function messageToAggregate(
|
|
154
|
+
message: gloas.PayloadAttestationMessage,
|
|
155
|
+
validatorCommitteeIndices: number[]
|
|
156
|
+
): AggregateFast {
|
|
157
|
+
const aggregationBits = BitArray.fromBitLen(PTC_SIZE);
|
|
158
|
+
for (const index of validatorCommitteeIndices) {
|
|
159
|
+
aggregationBits.set(index, true);
|
|
160
|
+
}
|
|
161
|
+
const sig = signatureFromBytesNoCheck(message.signature);
|
|
162
|
+
// The validator signed once but occupies `validatorCommitteeIndices.length` PTC positions.
|
|
163
|
+
// Verification aggregates the pubkey once per set bit, so the signature must be aggregated
|
|
164
|
+
// the same number of times for the BLS check to balance — same pattern as sync committee.
|
|
165
|
+
const signature =
|
|
166
|
+
validatorCommitteeIndices.length === 1
|
|
167
|
+
? sig
|
|
168
|
+
: aggregateSignatures(new Array(validatorCommitteeIndices.length).fill(sig));
|
|
154
169
|
return {
|
|
155
|
-
aggregationBits
|
|
170
|
+
aggregationBits,
|
|
156
171
|
data: message.data,
|
|
157
|
-
signature
|
|
172
|
+
signature,
|
|
158
173
|
};
|
|
159
174
|
}
|
|
160
175
|
|
|
161
176
|
function aggregateMessageInto(
|
|
162
177
|
message: gloas.PayloadAttestationMessage,
|
|
163
|
-
|
|
178
|
+
validatorCommitteeIndices: number[],
|
|
164
179
|
aggregate: AggregateFast
|
|
165
180
|
): InsertOutcome {
|
|
166
|
-
|
|
181
|
+
// Gossip dedup via `seenPayloadAttesters` is keyed by (epoch, validatorIndex), so the same
|
|
182
|
+
// validator's message is never processed twice — all of its bits are set together or none.
|
|
183
|
+
// Checking the first index is sufficient.
|
|
184
|
+
if (aggregate.aggregationBits.get(validatorCommitteeIndices[0]) === true) {
|
|
167
185
|
return InsertOutcome.AlreadyKnown;
|
|
168
186
|
}
|
|
169
187
|
|
|
170
|
-
|
|
171
|
-
|
|
188
|
+
for (const index of validatorCommitteeIndices) {
|
|
189
|
+
aggregate.aggregationBits.set(index, true);
|
|
190
|
+
}
|
|
191
|
+
const sig = signatureFromBytesNoCheck(message.signature);
|
|
192
|
+
aggregate.signature = aggregateSignatures([
|
|
193
|
+
aggregate.signature,
|
|
194
|
+
...new Array(validatorCommitteeIndices.length).fill(sig),
|
|
195
|
+
]);
|
|
172
196
|
|
|
173
197
|
return InsertOutcome.Aggregated;
|
|
174
198
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {RootHex, Slot, gloas} from "@lodestar/types";
|
|
2
|
+
import {toRootHex} from "@lodestar/utils";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pool of validated `SignedProposerPreferences` indexed by `(slot, dependent_root)`.
|
|
6
|
+
*
|
|
7
|
+
* The primary consumer is `validateExecutionPayloadBid`, which looks up the matching
|
|
8
|
+
* preferences via `get(bid.slot, dependent_root)` to enforce the IGNORE-existence and
|
|
9
|
+
* REJECT-equality rules from the gloas spec. The beacon API `/pool/proposer_preferences`
|
|
10
|
+
* GET endpoint reads from the same pool via `getAll`.
|
|
11
|
+
*
|
|
12
|
+
* `validator_index` is intentionally not part of the key: gossip validation enforces
|
|
13
|
+
* `proposers[proposalSlot % SLOTS_PER_EPOCH] === validatorIndex` against the shuffling
|
|
14
|
+
* implied by `dependent_root`, so once a preference has been validated `(slot, dependent_root)`
|
|
15
|
+
* already pins down the validator.
|
|
16
|
+
*/
|
|
17
|
+
export class ProposerPreferencesPool {
|
|
18
|
+
private readonly bySlot = new Map<Slot, Map<RootHex, gloas.SignedProposerPreferences>>();
|
|
19
|
+
|
|
20
|
+
/** Lookup for bid validation: matches `(bid.slot, get_proposer_dependent_root(parent_state, ...))`. */
|
|
21
|
+
get(slot: Slot, dependentRootHex: RootHex): gloas.SignedProposerPreferences | null {
|
|
22
|
+
return this.bySlot.get(slot)?.get(dependentRootHex) ?? null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
add(signed: gloas.SignedProposerPreferences): void {
|
|
26
|
+
const {proposalSlot, dependentRoot} = signed.message;
|
|
27
|
+
const rootHex = toRootHex(dependentRoot);
|
|
28
|
+
let byRoot = this.bySlot.get(proposalSlot);
|
|
29
|
+
if (!byRoot) {
|
|
30
|
+
byRoot = new Map();
|
|
31
|
+
this.bySlot.set(proposalSlot, byRoot);
|
|
32
|
+
}
|
|
33
|
+
byRoot.set(rootHex, signed);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** API read-out: flatten across branches, optionally filtered by slot. */
|
|
37
|
+
getAll(slot?: Slot): gloas.SignedProposerPreferences[] {
|
|
38
|
+
if (slot !== undefined) {
|
|
39
|
+
const byRoot = this.bySlot.get(slot);
|
|
40
|
+
return byRoot ? Array.from(byRoot.values()) : [];
|
|
41
|
+
}
|
|
42
|
+
const out: gloas.SignedProposerPreferences[] = [];
|
|
43
|
+
for (const byRoot of this.bySlot.values()) {
|
|
44
|
+
for (const v of byRoot.values()) out.push(v);
|
|
45
|
+
}
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Entries are only load-bearing while `proposal_slot >= current_slot`. Once the slot has
|
|
51
|
+
* passed the `[IGNORE] proposal_slot > current_slot` gossip rule takes over, so drop them
|
|
52
|
+
* on each slot tick.
|
|
53
|
+
*/
|
|
54
|
+
prune(currentSlot: Slot): void {
|
|
55
|
+
for (const slot of this.bySlot.keys()) {
|
|
56
|
+
if (slot < currentSlot) this.bySlot.delete(slot);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
isStatePostGloas,
|
|
8
8
|
} from "@lodestar/state-transition";
|
|
9
9
|
import {gloas} from "@lodestar/types";
|
|
10
|
-
import {toRootHex} from "@lodestar/utils";
|
|
10
|
+
import {byteArrayEquals, toHex, toRootHex} from "@lodestar/utils";
|
|
11
|
+
import {getShufflingDependentRoot} from "../../util/dependentRoot.js";
|
|
11
12
|
import {ExecutionPayloadBidError, ExecutionPayloadBidErrorCode, GossipAction} from "../errors/index.js";
|
|
12
13
|
import {IBeaconChain} from "../index.js";
|
|
13
14
|
import {RegenCaller} from "../regen/index.js";
|
|
@@ -48,12 +49,55 @@ async function validateExecutionPayloadBid(
|
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
51
|
|
|
52
|
+
// [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon block in fork choice.
|
|
53
|
+
// Moved earlier than the spec ordering so we can derive the proposer dependent root for the
|
|
54
|
+
// proposer-preferences lookup below from a known fork-choice block.
|
|
55
|
+
const parentBlock = chain.forkChoice.getBlockHexDefaultStatus(parentBlockRootHex);
|
|
56
|
+
if (parentBlock === null) {
|
|
57
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
58
|
+
code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
|
|
59
|
+
parentBlockRoot: parentBlockRootHex,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
// [IGNORE] A `SignedProposerPreferences` matching `bid.slot` and the bid's branch has been
|
|
52
64
|
// seen — i.e. `proposal_slot == bid.slot` AND `dependent_root ==
|
|
53
|
-
// get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
//
|
|
65
|
+
// get_proposer_dependent_root(parent_state, compute_epoch_at_slot(bid.slot))`.
|
|
66
|
+
const bidEpoch = computeEpochAtSlot(bid.slot);
|
|
67
|
+
// gloas is always post-Fulu, so `get_proposer_dependent_root` is the post-Fulu (deterministic
|
|
68
|
+
// proposer lookahead) form `block_root_at(start_slot(epoch - MIN_SEED_LOOKAHEAD) - 1)` with
|
|
69
|
+
// `MIN_SEED_LOOKAHEAD == 1` — identical to the attester-shuffling dependent root for the same
|
|
70
|
+
// epoch (both 1-epoch lookahead), hence `getShufflingDependentRoot`. `null` on a
|
|
71
|
+
// unknown/finalized-pruned ancestor or genesis edge → degrade to IGNORE below instead of
|
|
72
|
+
// letting a raw `ForkChoiceError` escape the `GossipActionError` contract.
|
|
73
|
+
const dependentRootHex = (() => {
|
|
74
|
+
try {
|
|
75
|
+
return getShufflingDependentRoot(chain.forkChoice, bidEpoch, computeEpochAtSlot(parentBlock.slot), parentBlock);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
})();
|
|
80
|
+
|
|
81
|
+
if (dependentRootHex === null) {
|
|
82
|
+
// Could not derive the dependent root for this branch (unknown/finalized-pruned ancestor,
|
|
83
|
+
// genesis edge, etc.) → definitionally no matching `SignedProposerPreferences`.
|
|
84
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
85
|
+
code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
|
|
86
|
+
slot: bid.slot,
|
|
87
|
+
parentBlockRoot: parentBlockRootHex,
|
|
88
|
+
dependentRoot: "unknown",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const proposerPreferences = chain.proposerPreferencesPool.get(bid.slot, dependentRootHex);
|
|
93
|
+
if (proposerPreferences === null) {
|
|
94
|
+
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
95
|
+
code: ExecutionPayloadBidErrorCode.NO_MATCHING_PROPOSER_PREFERENCES,
|
|
96
|
+
slot: bid.slot,
|
|
97
|
+
parentBlockRoot: parentBlockRootHex,
|
|
98
|
+
dependentRoot: dependentRootHex,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
57
101
|
|
|
58
102
|
// [REJECT] `bid.builder_index` is a valid/active builder index -- i.e.
|
|
59
103
|
// `is_active_builder(state, bid.builder_index)` returns `True`.
|
|
@@ -75,10 +119,25 @@ async function validateExecutionPayloadBid(
|
|
|
75
119
|
}
|
|
76
120
|
|
|
77
121
|
// [REJECT] `bid.fee_recipient == proposer_preferences.fee_recipient`.
|
|
122
|
+
if (!byteArrayEquals(bid.feeRecipient, proposerPreferences.message.feeRecipient)) {
|
|
123
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
124
|
+
code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH,
|
|
125
|
+
builderIndex: bid.builderIndex,
|
|
126
|
+
bidFeeRecipient: toHex(bid.feeRecipient),
|
|
127
|
+
expectedFeeRecipient: toHex(proposerPreferences.message.feeRecipient),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
78
131
|
// [REJECT] `bid.gas_limit == proposer_preferences.gas_limit`.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
132
|
+
const bidGasLimit = Number(bid.gasLimit);
|
|
133
|
+
if (bidGasLimit !== proposerPreferences.message.gasLimit) {
|
|
134
|
+
throw new ExecutionPayloadBidError(GossipAction.REJECT, {
|
|
135
|
+
code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_GAS_LIMIT_MISMATCH,
|
|
136
|
+
builderIndex: bid.builderIndex,
|
|
137
|
+
bidGasLimit,
|
|
138
|
+
expectedGasLimit: proposerPreferences.message.gasLimit,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
82
141
|
|
|
83
142
|
// [REJECT] The length of KZG commitments is less than or equal to the limitation defined in the
|
|
84
143
|
// consensus layer -- i.e. validate that
|
|
@@ -128,15 +187,6 @@ async function validateExecutionPayloadBid(
|
|
|
128
187
|
// payload in fork choice.
|
|
129
188
|
// TODO GLOAS: implement this
|
|
130
189
|
|
|
131
|
-
// [IGNORE] `bid.parent_block_root` is the hash tree root of a known beacon
|
|
132
|
-
// block in fork choice.
|
|
133
|
-
if (!chain.forkChoice.hasBlock(bid.parentBlockRoot)) {
|
|
134
|
-
throw new ExecutionPayloadBidError(GossipAction.IGNORE, {
|
|
135
|
-
code: ExecutionPayloadBidErrorCode.UNKNOWN_BLOCK_ROOT,
|
|
136
|
-
parentBlockRoot: parentBlockRootHex,
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
190
|
// [REJECT] `signed_execution_payload_bid.signature` is valid with respect to the `bid.builder_index`.
|
|
141
191
|
const signatureSet = createSingleSignatureSetFromComponents(
|
|
142
192
|
PublicKey.fromBytes(builder.pubkey),
|
|
@@ -11,7 +11,7 @@ import {IBeaconChain} from "../index.js";
|
|
|
11
11
|
|
|
12
12
|
export type PayloadAttestationValidationResult = {
|
|
13
13
|
attDataRootHex: RootHex;
|
|
14
|
-
|
|
14
|
+
validatorCommitteeIndices: number[];
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export async function validateApiPayloadAttestationMessage(
|
|
@@ -80,9 +80,11 @@ async function validatePayloadAttestationMessage(
|
|
|
80
80
|
// [REJECT] The message's validator index is within the payload committee in
|
|
81
81
|
// `get_ptc(state, data.slot)`. The `state` is the head state corresponding to
|
|
82
82
|
// processing the block up to the current slot as determined by the fork choice.
|
|
83
|
-
|
|
83
|
+
// The validator may occupy multiple PTC positions because `compute_ptc` samples
|
|
84
|
+
// by effective balance — collect all of them so duplicate votes are counted.
|
|
85
|
+
const validatorCommitteeIndices = state.getIndicesInPayloadTimelinessCommittee(validatorIndex, data.slot);
|
|
84
86
|
|
|
85
|
-
if (
|
|
87
|
+
if (validatorCommitteeIndices.length === 0) {
|
|
86
88
|
throw new PayloadAttestationError(GossipAction.REJECT, {
|
|
87
89
|
code: PayloadAttestationErrorCode.INVALID_ATTESTER,
|
|
88
90
|
attesterIndex: validatorIndex,
|
|
@@ -115,6 +117,6 @@ async function validatePayloadAttestationMessage(
|
|
|
115
117
|
|
|
116
118
|
return {
|
|
117
119
|
attDataRootHex: toRootHex(ssz.gloas.PayloadAttestationData.hashTreeRoot(data)),
|
|
118
|
-
|
|
120
|
+
validatorCommitteeIndices,
|
|
119
121
|
};
|
|
120
122
|
}
|
|
@@ -3,6 +3,7 @@ import {NotReorgedReason} from "@lodestar/fork-choice";
|
|
|
3
3
|
import {ArchiveStoreTask} from "../../chain/archiveStore/archiveStore.js";
|
|
4
4
|
import {FrequencyStateArchiveStep} from "../../chain/archiveStore/strategies/frequencyStateArchiveStrategy.js";
|
|
5
5
|
import {BlockInputSource} from "../../chain/blocks/blockInput/index.js";
|
|
6
|
+
import {PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
|
|
6
7
|
import {PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
|
|
7
8
|
import {JobQueueItemType} from "../../chain/bls/index.js";
|
|
8
9
|
import {AttestationErrorCode, BlockErrorCode} from "../../chain/errors/index.js";
|
|
@@ -890,6 +891,11 @@ export function createLodestarMetrics(
|
|
|
890
891
|
labelNames: ["source"],
|
|
891
892
|
buckets: [0.5, 1, 2, 4, 6, 12],
|
|
892
893
|
}),
|
|
894
|
+
processPayloadErrors: register.gauge<{error: PayloadErrorCode | "NOT_PAYLOAD_ERROR"}>({
|
|
895
|
+
name: "lodestar_gossip_execution_payload_envelope_process_payload_errors",
|
|
896
|
+
help: "Count of errors, by error type, while processing execution payload envelopes",
|
|
897
|
+
labelNames: ["error"],
|
|
898
|
+
}),
|
|
893
899
|
},
|
|
894
900
|
// recovery in the case of specific blob rows required
|
|
895
901
|
recoverBlobSidecars: {
|
package/src/network/interface.ts
CHANGED
|
@@ -114,6 +114,7 @@ export interface INetwork extends INetworkCorePublic {
|
|
|
114
114
|
publishLightClientOptimisticUpdate(update: LightClientOptimisticUpdate): Promise<number>;
|
|
115
115
|
publishSignedExecutionPayloadEnvelope(signedEnvelope: gloas.SignedExecutionPayloadEnvelope): Promise<number>;
|
|
116
116
|
publishPayloadAttestationMessage(payloadAttestationMessage: gloas.PayloadAttestationMessage): Promise<number>;
|
|
117
|
+
publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number>;
|
|
117
118
|
|
|
118
119
|
// Debug
|
|
119
120
|
dumpGossipQueue(gossipType: GossipType): Promise<PendingGossipsubMessage[]>;
|
package/src/network/network.ts
CHANGED
|
@@ -526,6 +526,17 @@ export class Network implements INetwork {
|
|
|
526
526
|
);
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
async publishProposerPreferences(signedProposerPreferences: gloas.SignedProposerPreferences): Promise<number> {
|
|
530
|
+
const epoch = computeEpochAtSlot(signedProposerPreferences.message.proposalSlot);
|
|
531
|
+
const boundary = this.config.getForkBoundaryAtEpoch(epoch);
|
|
532
|
+
|
|
533
|
+
return this.publishGossip<GossipType.proposer_preferences>(
|
|
534
|
+
{type: GossipType.proposer_preferences, boundary},
|
|
535
|
+
signedProposerPreferences,
|
|
536
|
+
{ignoreDuplicatePublishError: true}
|
|
537
|
+
);
|
|
538
|
+
}
|
|
539
|
+
|
|
529
540
|
private async publishGossip<K extends GossipType>(
|
|
530
541
|
topic: GossipTopicMap[K],
|
|
531
542
|
object: GossipTypeMap[K],
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
IBlockInput,
|
|
36
36
|
isBlockInputColumns,
|
|
37
37
|
} from "../../chain/blocks/blockInput/index.js";
|
|
38
|
+
import {PayloadError, PayloadErrorCode} from "../../chain/blocks/importExecutionPayload.js";
|
|
38
39
|
import {PayloadEnvelopeInput, PayloadEnvelopeInputSource} from "../../chain/blocks/payloadEnvelopeInput/index.js";
|
|
39
40
|
import {BlobSidecarValidation} from "../../chain/blocks/types.js";
|
|
40
41
|
import {ChainEvent} from "../../chain/emitter.js";
|
|
@@ -1148,7 +1149,40 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1148
1149
|
});
|
|
1149
1150
|
|
|
1150
1151
|
chain.processExecutionPayload(payloadInput, {validSignature: true}).catch((e) => {
|
|
1151
|
-
|
|
1152
|
+
// Adjust verbosity based on error type
|
|
1153
|
+
let logLevel: LogLevel;
|
|
1154
|
+
|
|
1155
|
+
if (e instanceof PayloadError) {
|
|
1156
|
+
switch (e.type.code) {
|
|
1157
|
+
// BLOCK_NOT_IN_FORK_CHOICE should not happen, validateGossipExecutionPayloadEnvelope above
|
|
1158
|
+
// already verified the block is in fork choice
|
|
1159
|
+
case PayloadErrorCode.BLOCK_NOT_IN_FORK_CHOICE:
|
|
1160
|
+
case PayloadErrorCode.MISS_BLOCK_STATE:
|
|
1161
|
+
case PayloadErrorCode.EXECUTION_ENGINE_ERROR:
|
|
1162
|
+
// Errors might indicate an issue with our node or the connected EL client
|
|
1163
|
+
logLevel = LogLevel.error;
|
|
1164
|
+
break;
|
|
1165
|
+
// INVALID_SIGNATURE should not happen, signature is verified during gossip validation
|
|
1166
|
+
case PayloadErrorCode.INVALID_SIGNATURE:
|
|
1167
|
+
case PayloadErrorCode.ENVELOPE_VERIFICATION_ERROR:
|
|
1168
|
+
case PayloadErrorCode.EXECUTION_ENGINE_INVALID:
|
|
1169
|
+
core.reportPeer(peerIdStr, PeerAction.LowToleranceError, "BadGossipPayload");
|
|
1170
|
+
// Misbehaving peer, but could highlight an issue in another client
|
|
1171
|
+
logLevel = LogLevel.warn;
|
|
1172
|
+
break;
|
|
1173
|
+
}
|
|
1174
|
+
} else {
|
|
1175
|
+
// Any unexpected error
|
|
1176
|
+
logLevel = LogLevel.error;
|
|
1177
|
+
}
|
|
1178
|
+
metrics?.gossipExecutionPayloadEnvelope.processPayloadErrors.inc({
|
|
1179
|
+
error: e instanceof PayloadError ? e.type.code : "NOT_PAYLOAD_ERROR",
|
|
1180
|
+
});
|
|
1181
|
+
chain.logger[logLevel](
|
|
1182
|
+
"Error processing execution payload from gossip",
|
|
1183
|
+
{slot, peer: peerIdStr, root: blockRootHex},
|
|
1184
|
+
e as Error
|
|
1185
|
+
);
|
|
1152
1186
|
});
|
|
1153
1187
|
},
|
|
1154
1188
|
[GossipType.payload_attestation_message]: async ({
|
|
@@ -1163,7 +1197,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1163
1197
|
const insertOutcome = chain.payloadAttestationPool.add(
|
|
1164
1198
|
payloadAttestationMessage,
|
|
1165
1199
|
validationResult.attDataRootHex,
|
|
1166
|
-
validationResult.
|
|
1200
|
+
validationResult.validatorCommitteeIndices
|
|
1167
1201
|
);
|
|
1168
1202
|
metrics?.opPool.payloadAttestationPool.gossipInsertOutcome.inc({insertOutcome});
|
|
1169
1203
|
} catch (e) {
|
|
@@ -1171,7 +1205,7 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1171
1205
|
}
|
|
1172
1206
|
chain.forkChoice.notifyPtcMessages(
|
|
1173
1207
|
toRootHex(payloadAttestationMessage.data.beaconBlockRoot),
|
|
1174
|
-
|
|
1208
|
+
validationResult.validatorCommitteeIndices,
|
|
1175
1209
|
payloadAttestationMessage.data.payloadPresent
|
|
1176
1210
|
);
|
|
1177
1211
|
},
|
|
@@ -1203,6 +1237,12 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
1203
1237
|
const {serializedData} = gossipData;
|
|
1204
1238
|
const signedProposerPreferences = sszDeserialize(topic, serializedData);
|
|
1205
1239
|
await validateGossipProposerPreferences(chain, signedProposerPreferences);
|
|
1240
|
+
|
|
1241
|
+
chain.proposerPreferencesPool.add(signedProposerPreferences);
|
|
1242
|
+
chain.emitter.emit(routes.events.EventType.proposerPreferences, {
|
|
1243
|
+
version: ForkName.gloas,
|
|
1244
|
+
data: signedProposerPreferences,
|
|
1245
|
+
});
|
|
1206
1246
|
},
|
|
1207
1247
|
};
|
|
1208
1248
|
}
|
package/src/sync/range/batch.ts
CHANGED
|
@@ -45,6 +45,15 @@ export type Attempt = {
|
|
|
45
45
|
hash: RootHex;
|
|
46
46
|
};
|
|
47
47
|
|
|
48
|
+
type TrackedRequest = {
|
|
49
|
+
/** only happen for the 1st batch in checkpoint sync */
|
|
50
|
+
parentPayload: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* we always issue by_range before parent_payload, so we don't model this as null
|
|
53
|
+
*/
|
|
54
|
+
byRangeColumns: Set<number>;
|
|
55
|
+
};
|
|
56
|
+
|
|
48
57
|
export type AwaitingDownloadState = {
|
|
49
58
|
status: BatchStatus.AwaitingDownload;
|
|
50
59
|
blocks: IBlockInput[];
|
|
@@ -62,6 +71,7 @@ export type BatchState =
|
|
|
62
71
|
| {
|
|
63
72
|
status: BatchStatus.Downloading;
|
|
64
73
|
peer: PeerIdStr;
|
|
74
|
+
request: TrackedRequest;
|
|
65
75
|
blocks: IBlockInput[];
|
|
66
76
|
payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null;
|
|
67
77
|
}
|
|
@@ -110,6 +120,13 @@ function formatColumnsReq(req: {startSlot: Slot; count: number; columns: number[
|
|
|
110
120
|
return `startSlot=${req.startSlot},count=${req.count},cols=${prettyPrintIndices(req.columns)}`;
|
|
111
121
|
}
|
|
112
122
|
|
|
123
|
+
function getTrackedRequest({parentPayloadRequest, columnsRequest}: DownloadByRangeRequests): TrackedRequest {
|
|
124
|
+
return {
|
|
125
|
+
parentPayload: parentPayloadRequest != null,
|
|
126
|
+
byRangeColumns: new Set(parentPayloadRequest == null ? (columnsRequest?.columns ?? []) : []),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
113
130
|
/**
|
|
114
131
|
* Batches are downloaded at the first block of the epoch.
|
|
115
132
|
*
|
|
@@ -131,8 +148,8 @@ export class Batch {
|
|
|
131
148
|
requests: DownloadByRangeRequests;
|
|
132
149
|
/** State of the batch. */
|
|
133
150
|
state: BatchState = {status: BatchStatus.AwaitingDownload, blocks: [], payloadEnvelopes: null};
|
|
134
|
-
/** Peers that provided good data */
|
|
135
|
-
|
|
151
|
+
/** Peers that provided good data, with column coverage for by_range requests */
|
|
152
|
+
private readonly successfulDownloads = new Map<PeerIdStr, TrackedRequest>();
|
|
136
153
|
/** The `Attempts` that have been made and failed to send us this batch. */
|
|
137
154
|
readonly failedProcessingAttempts: Attempt[] = [];
|
|
138
155
|
/** The `Attempts` that have been made and failed because of execution malfunction. */
|
|
@@ -406,6 +423,35 @@ export class Batch {
|
|
|
406
423
|
return [...this.failedDownloadAttempts, ...this.failedProcessingAttempts.flatMap((a) => a.peers)];
|
|
407
424
|
}
|
|
408
425
|
|
|
426
|
+
/**
|
|
427
|
+
* True only if the peer has already returned a successful response for the current request.
|
|
428
|
+
* A by_range success may update `this.requests` to parent_payload, and the same peer is then
|
|
429
|
+
* still eligible for the newly discovered parent payload data.
|
|
430
|
+
* For by_range, a peer that previously succeeded with a superset of requested columns is skipped.
|
|
431
|
+
*/
|
|
432
|
+
hasPeerSucceededCurrentRequest(peer: PeerSyncMeta): boolean {
|
|
433
|
+
const successfulDownload = this.successfulDownloads.get(peer.peerId);
|
|
434
|
+
if (successfulDownload == null) return false;
|
|
435
|
+
|
|
436
|
+
const request = getTrackedRequest(this.getRequestsForPeer(peer));
|
|
437
|
+
if (request.parentPayload) return successfulDownload.parentPayload;
|
|
438
|
+
|
|
439
|
+
const requestByRangeColumns = request.byRangeColumns;
|
|
440
|
+
|
|
441
|
+
if (requestByRangeColumns.size === 0) {
|
|
442
|
+
// this means a download blocks/envelops by_range only
|
|
443
|
+
// don't do that again if we already did it
|
|
444
|
+
// see https://github.com/ChainSafe/lodestar/issues/9357
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return [...requestByRangeColumns].every((column) => successfulDownload.byRangeColumns.has(column));
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private getSuccessfulPeers(): PeerIdStr[] {
|
|
452
|
+
return Array.from(this.successfulDownloads.keys());
|
|
453
|
+
}
|
|
454
|
+
|
|
409
455
|
getMetadata(): BatchMetadata {
|
|
410
456
|
const {blocksRequest, blobsRequest, columnsRequest, envelopesRequest} = this.requests;
|
|
411
457
|
const failedProcessingPeerList = this.failedProcessingAttempts.flatMap((a) => a.peers);
|
|
@@ -440,14 +486,17 @@ export class Batch {
|
|
|
440
486
|
/**
|
|
441
487
|
* AwaitingDownload -> Downloading
|
|
442
488
|
*/
|
|
443
|
-
startDownloading(peer:
|
|
489
|
+
startDownloading(peer: PeerSyncMeta): void {
|
|
444
490
|
if (this.state.status !== BatchStatus.AwaitingDownload) {
|
|
445
491
|
throw new BatchError(this.wrongStatusErrorType(BatchStatus.AwaitingDownload));
|
|
446
492
|
}
|
|
447
493
|
|
|
494
|
+
const request = getTrackedRequest(this.getRequestsForPeer(peer));
|
|
495
|
+
|
|
448
496
|
this.state = {
|
|
449
497
|
status: BatchStatus.Downloading,
|
|
450
|
-
peer,
|
|
498
|
+
peer: peer.peerId,
|
|
499
|
+
request,
|
|
451
500
|
blocks: this.state.blocks,
|
|
452
501
|
payloadEnvelopes: this.state.payloadEnvelopes,
|
|
453
502
|
};
|
|
@@ -468,7 +517,17 @@ export class Batch {
|
|
|
468
517
|
// ensure that blocks are always sorted before getting stored on the batch.state or being used to getRequests
|
|
469
518
|
blocks.sort((a, b) => a.slot - b.slot);
|
|
470
519
|
|
|
471
|
-
this.
|
|
520
|
+
const successfulDownload = this.successfulDownloads.get(peer) ?? {
|
|
521
|
+
parentPayload: false,
|
|
522
|
+
byRangeColumns: new Set<number>(),
|
|
523
|
+
};
|
|
524
|
+
successfulDownload.parentPayload ||= this.state.request.parentPayload;
|
|
525
|
+
if (!this.state.request.parentPayload) {
|
|
526
|
+
for (const column of this.state.request.byRangeColumns) {
|
|
527
|
+
successfulDownload.byRangeColumns.add(column);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
this.successfulDownloads.set(peer, successfulDownload);
|
|
472
531
|
|
|
473
532
|
let allComplete = true;
|
|
474
533
|
const slots = new Set<number>();
|
|
@@ -497,8 +556,9 @@ export class Batch {
|
|
|
497
556
|
if (allComplete && isForkPostGloas(this.forkName)) {
|
|
498
557
|
for (const block of blocks) {
|
|
499
558
|
const payloadInput = newPayloadEnvelopes?.get(block.slot);
|
|
500
|
-
//
|
|
501
|
-
|
|
559
|
+
// only need to make sure envelope has all columns, not all blocks have payload
|
|
560
|
+
// assertLinearChainSegment() was called before reaching this
|
|
561
|
+
if (payloadInput?.hasPayloadEnvelope() && !payloadInput.hasComputedAllData()) {
|
|
502
562
|
allComplete = false;
|
|
503
563
|
break;
|
|
504
564
|
}
|
|
@@ -589,10 +649,10 @@ export class Batch {
|
|
|
589
649
|
const blocks = this.state.blocks;
|
|
590
650
|
const payloadEnvelopes = this.state.payloadEnvelopes;
|
|
591
651
|
const hash = hashBlocks(blocks, this.config); // tracks blocks to report peer on processing error
|
|
592
|
-
// Reset
|
|
593
|
-
// that the data came from will be handled by the Attempt that goes for processing
|
|
594
|
-
const peers = this.
|
|
595
|
-
this.
|
|
652
|
+
// Reset successfulDownloads in case another download attempt needs to be made. When Attempt is successful or not
|
|
653
|
+
// the peers that the data came from will be handled by the Attempt that goes for processing.
|
|
654
|
+
const peers = this.getSuccessfulPeers();
|
|
655
|
+
this.successfulDownloads.clear();
|
|
596
656
|
this.state = {status: BatchStatus.Processing, blocks, payloadEnvelopes, attempt: {peers, hash}};
|
|
597
657
|
return {blocks, payloadEnvelopes, peers};
|
|
598
658
|
}
|
package/src/sync/range/chain.ts
CHANGED
|
@@ -551,7 +551,7 @@ export class SyncChain {
|
|
|
551
551
|
peer: prettyPrintPeerIdStr(peer.peerId),
|
|
552
552
|
});
|
|
553
553
|
try {
|
|
554
|
-
batch.startDownloading(peer
|
|
554
|
+
batch.startDownloading(peer);
|
|
555
555
|
|
|
556
556
|
// wrapError ensures to never call both batch success() and batch error()
|
|
557
557
|
const res = await wrapError(this.downloadByRange(peer, batch, this.syncType));
|
|
@@ -581,6 +581,8 @@ export class SyncChain {
|
|
|
581
581
|
case DownloadByRangeErrorCode.OUT_OF_ORDER_BLOCKS:
|
|
582
582
|
case DownloadByRangeErrorCode.OUT_OF_RANGE_BLOCKS:
|
|
583
583
|
case DownloadByRangeErrorCode.PARENT_ROOT_MISMATCH:
|
|
584
|
+
case DownloadByRangeErrorCode.INVALID_ENVELOPE_BEACON_BLOCK_ROOT:
|
|
585
|
+
case DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT:
|
|
584
586
|
case BlobSidecarErrorCode.INCLUSION_PROOF_INVALID:
|
|
585
587
|
case BlobSidecarErrorCode.INVALID_KZG_PROOF_BATCH:
|
|
586
588
|
case DataColumnSidecarErrorCode.INCORRECT_KZG_COMMITMENTS_COUNT:
|
package/src/sync/range/range.ts
CHANGED
|
@@ -3,14 +3,21 @@ import {StrictEventEmitter} from "strict-event-emitter-types";
|
|
|
3
3
|
import {BeaconConfig} from "@lodestar/config";
|
|
4
4
|
import {IBeaconStateViewGloas, computeStartSlotAtEpoch, isStatePostGloas} from "@lodestar/state-transition";
|
|
5
5
|
import {Epoch, Status, fulu} from "@lodestar/types";
|
|
6
|
-
import {Logger, toRootHex} from "@lodestar/utils";
|
|
6
|
+
import {Logger, prettyPrintIndices, toRootHex} from "@lodestar/utils";
|
|
7
7
|
import {IBlockInput} from "../../chain/blocks/blockInput/types.js";
|
|
8
8
|
import {AttestationImportOpt, ImportBlockOpts} from "../../chain/blocks/index.js";
|
|
9
|
+
import {assertLinearChainSegment} from "../../chain/blocks/utils/chainSegment.js";
|
|
10
|
+
import {BlockError} from "../../chain/errors/index.js";
|
|
9
11
|
import {IBeaconChain} from "../../chain/index.js";
|
|
10
12
|
import {Metrics} from "../../metrics/index.js";
|
|
11
13
|
import {INetwork} from "../../network/index.js";
|
|
12
14
|
import {PeerIdStr} from "../../util/peerId.js";
|
|
13
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
DownloadByRangeError,
|
|
17
|
+
DownloadByRangeErrorCode,
|
|
18
|
+
cacheByRangeResponses,
|
|
19
|
+
downloadByRange,
|
|
20
|
+
} from "../utils/downloadByRange.js";
|
|
14
21
|
import {RangeSyncType, getRangeSyncTarget, rangeSyncTypes} from "../utils/remoteSyncType.js";
|
|
15
22
|
import {ChainTarget, SyncChain, SyncChainDebugState, SyncChainFns} from "./chain.js";
|
|
16
23
|
import {updateChains} from "./utils/index.js";
|
|
@@ -231,6 +238,49 @@ export class RangeSync extends (EventEmitter as {new (): RangeSyncEmitter}) {
|
|
|
231
238
|
custodyConfig: this.chain.custodyConfig,
|
|
232
239
|
seenTimestampSec: Date.now() / 1000,
|
|
233
240
|
});
|
|
241
|
+
|
|
242
|
+
const segmentBlocks = blocks.filter((b) => b.hasBlock()).sort((a, b) => a.slot - b.slot);
|
|
243
|
+
const envelopeSlots = payloadEnvelopes
|
|
244
|
+
? Array.from(payloadEnvelopes.entries())
|
|
245
|
+
.filter(([, pi]) => pi.hasPayloadEnvelope())
|
|
246
|
+
.map(([slot]) => slot)
|
|
247
|
+
.sort((a, b) => a - b)
|
|
248
|
+
: [];
|
|
249
|
+
this.logger.verbose("downloadByRange batch ready", {
|
|
250
|
+
peer: peer.peerId,
|
|
251
|
+
blockSlots: prettyPrintIndices(segmentBlocks.map((b) => b.slot)),
|
|
252
|
+
envelopeSlots: prettyPrintIndices(envelopeSlots),
|
|
253
|
+
...batch.getMetadata(),
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (segmentBlocks.length > 1) {
|
|
257
|
+
try {
|
|
258
|
+
assertLinearChainSegment(this.config, segmentBlocks, payloadEnvelopes, null);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (err instanceof BlockError) {
|
|
261
|
+
this.logger.debug(
|
|
262
|
+
"downloadByRange segment validation failed",
|
|
263
|
+
{
|
|
264
|
+
peer: peer.peerId,
|
|
265
|
+
reason: err.type.code,
|
|
266
|
+
slot: err.signedBlock.message.slot,
|
|
267
|
+
detail: JSON.stringify(err.type),
|
|
268
|
+
...batch.getMetadata(),
|
|
269
|
+
},
|
|
270
|
+
err
|
|
271
|
+
);
|
|
272
|
+
// with this error, the peer will be penalized inside SyncChain
|
|
273
|
+
throw new DownloadByRangeError({
|
|
274
|
+
code: DownloadByRangeErrorCode.INVALID_CHAIN_SEGMENT,
|
|
275
|
+
slot: err.signedBlock.message.slot,
|
|
276
|
+
reason: err.type.code,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
234
284
|
return {result: {blocks, payloadEnvelopes}, warnings};
|
|
235
285
|
};
|
|
236
286
|
|