@lodestar/beacon-node 1.46.0 → 1.47.0-dev.0bcaaaebb5
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/lodestar/index.d.ts.map +1 -1
- package/lib/api/impl/lodestar/index.js +11 -12
- package/lib/api/impl/lodestar/index.js.map +1 -1
- package/lib/api/impl/validator/index.d.ts.map +1 -1
- package/lib/api/impl/validator/index.js +11 -21
- package/lib/api/impl/validator/index.js.map +1 -1
- package/lib/api/impl/validator/utils.d.ts +5 -0
- package/lib/api/impl/validator/utils.d.ts.map +1 -1
- package/lib/api/impl/validator/utils.js +18 -12
- package/lib/api/impl/validator/utils.js.map +1 -1
- package/lib/chain/blocks/importBlock.js +1 -1
- package/lib/chain/blocks/importBlock.js.map +1 -1
- package/lib/chain/blocks/importExecutionPayload.js +1 -1
- package/lib/chain/blocks/importExecutionPayload.js.map +1 -1
- package/lib/chain/blocks/index.d.ts.map +1 -1
- package/lib/chain/blocks/index.js +3 -5
- package/lib/chain/blocks/index.js.map +1 -1
- package/lib/chain/blocks/verifyBlock.d.ts.map +1 -1
- package/lib/chain/blocks/verifyBlock.js +11 -16
- package/lib/chain/blocks/verifyBlock.js.map +1 -1
- package/lib/chain/chain.d.ts +1 -1
- package/lib/chain/chain.d.ts.map +1 -1
- package/lib/chain/chain.js +8 -2
- package/lib/chain/chain.js.map +1 -1
- package/lib/chain/errors/blockError.d.ts +1 -0
- package/lib/chain/errors/blockError.d.ts.map +1 -1
- package/lib/chain/options.d.ts +0 -2
- package/lib/chain/options.d.ts.map +1 -1
- package/lib/chain/options.js.map +1 -1
- package/lib/chain/prepareNextSlot.js +1 -1
- package/lib/chain/prepareNextSlot.js.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.d.ts +2 -0
- package/lib/chain/produceBlock/produceBlockBody.d.ts.map +1 -1
- package/lib/chain/produceBlock/produceBlockBody.js +8 -3
- package/lib/chain/produceBlock/produceBlockBody.js.map +1 -1
- package/lib/chain/seenCache/seenBlockProposers.d.ts +5 -1
- package/lib/chain/seenCache/seenBlockProposers.d.ts.map +1 -1
- package/lib/chain/seenCache/seenBlockProposers.js +17 -7
- package/lib/chain/seenCache/seenBlockProposers.js.map +1 -1
- package/lib/chain/validation/block.d.ts.map +1 -1
- package/lib/chain/validation/block.js +21 -7
- package/lib/chain/validation/block.js.map +1 -1
- package/lib/network/processor/gossipHandlers.d.ts.map +1 -1
- package/lib/network/processor/gossipHandlers.js +25 -3
- package/lib/network/processor/gossipHandlers.js.map +1 -1
- package/lib/sync/range/batch.js +1 -1
- package/lib/sync/range/batch.js.map +1 -1
- package/lib/sync/range/utils/hashBlocks.d.ts +16 -4
- package/lib/sync/range/utils/hashBlocks.d.ts.map +1 -1
- package/lib/sync/range/utils/hashBlocks.js +49 -18
- package/lib/sync/range/utils/hashBlocks.js.map +1 -1
- package/package.json +16 -16
- package/src/api/impl/lodestar/index.ts +11 -12
- package/src/api/impl/validator/index.ts +21 -26
- package/src/api/impl/validator/utils.ts +30 -15
- package/src/chain/blocks/importBlock.ts +1 -1
- package/src/chain/blocks/importExecutionPayload.ts +1 -1
- package/src/chain/blocks/index.ts +3 -5
- package/src/chain/blocks/verifyBlock.ts +21 -25
- package/src/chain/chain.ts +8 -1
- package/src/chain/errors/blockError.ts +1 -1
- package/src/chain/options.ts +0 -2
- package/src/chain/prepareNextSlot.ts +1 -1
- package/src/chain/produceBlock/produceBlockBody.ts +15 -2
- package/src/chain/seenCache/seenBlockProposers.ts +24 -7
- package/src/chain/validation/block.ts +21 -7
- package/src/network/processor/gossipHandlers.ts +26 -3
- package/src/sync/range/batch.ts +1 -1
- package/src/sync/range/utils/hashBlocks.ts +56 -21
|
@@ -3,7 +3,7 @@ import {Epoch, RootHex, Slot, ValidatorIndex, phase0} from "@lodestar/types";
|
|
|
3
3
|
import {MapDef} from "@lodestar/utils";
|
|
4
4
|
|
|
5
5
|
/** Two distinct block roots signed by the same proposer for the same slot are sufficient to establish an equivocation */
|
|
6
|
-
const
|
|
6
|
+
const MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL = 2;
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Keeps a cache to filter block proposals from the same validator in the same slot.
|
|
@@ -19,7 +19,9 @@ const MAX_BLOCK_ROOTS_PER_PROPOSAL = 2;
|
|
|
19
19
|
* The cache is pruned on finalization and bounds the number of roots stored per proposer and slot
|
|
20
20
|
*/
|
|
21
21
|
export class SeenBlockProposers {
|
|
22
|
-
private readonly proposerIndexesBySlot = new MapDef<Slot,
|
|
22
|
+
private readonly proposerIndexesBySlot = new MapDef<Slot, Map<ValidatorIndex, RootHex>>(
|
|
23
|
+
() => new Map<ValidatorIndex, RootHex>()
|
|
24
|
+
);
|
|
23
25
|
private readonly signedBlockHeadersBySlot = new MapDef<
|
|
24
26
|
Slot,
|
|
25
27
|
MapDef<ValidatorIndex, Map<RootHex, phase0.SignedBeaconBlockHeader>>
|
|
@@ -30,13 +32,22 @@ export class SeenBlockProposers {
|
|
|
30
32
|
return this.proposerIndexesBySlot.get(blockSlot)?.has(proposerIndex) === true;
|
|
31
33
|
}
|
|
32
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The block proposer is known at slot with a different root.
|
|
37
|
+
*/
|
|
38
|
+
isRepeatProposal(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): boolean {
|
|
39
|
+
const knownRoot = this.proposerIndexesBySlot.get(blockSlot)?.get(proposerIndex);
|
|
40
|
+
return knownRoot !== undefined && knownRoot !== blockRoot;
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
hasBlockRoot(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): boolean {
|
|
34
44
|
return this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.has(blockRoot) === true;
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
isEquivocating(blockSlot: Slot, proposerIndex: ValidatorIndex): boolean {
|
|
38
48
|
return (
|
|
39
|
-
(this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >=
|
|
49
|
+
(this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex)?.size ?? 0) >=
|
|
50
|
+
MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL
|
|
40
51
|
);
|
|
41
52
|
}
|
|
42
53
|
|
|
@@ -53,7 +64,10 @@ export class SeenBlockProposers {
|
|
|
53
64
|
proposerIndex: ValidatorIndex
|
|
54
65
|
): [phase0.SignedBeaconBlockHeader, phase0.SignedBeaconBlockHeader] | null {
|
|
55
66
|
const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.get(blockSlot)?.get(proposerIndex);
|
|
56
|
-
if (
|
|
67
|
+
if (
|
|
68
|
+
signedBlockHeaderByRoot === undefined ||
|
|
69
|
+
signedBlockHeaderByRoot.size < MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL
|
|
70
|
+
) {
|
|
57
71
|
return null;
|
|
58
72
|
}
|
|
59
73
|
const [signedHeader1, signedHeader2] = signedBlockHeaderByRoot.values();
|
|
@@ -72,18 +86,21 @@ export class SeenBlockProposers {
|
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
const signedBlockHeaderByRoot = this.signedBlockHeadersBySlot.getOrDefault(blockSlot).getOrDefault(proposerIndex);
|
|
75
|
-
if (
|
|
89
|
+
if (
|
|
90
|
+
signedBlockHeaderByRoot.size < MIN_EQUIVOCATION_BLOCK_ROOTS_PER_PROPOSAL &&
|
|
91
|
+
!signedBlockHeaderByRoot.has(blockRoot)
|
|
92
|
+
) {
|
|
76
93
|
signedBlockHeaderByRoot.set(blockRoot, signedBlockHeader);
|
|
77
94
|
}
|
|
78
95
|
}
|
|
79
96
|
|
|
80
97
|
/** Mark a block as known from gossip or another block import path */
|
|
81
|
-
add(blockSlot: Slot, proposerIndex: ValidatorIndex): void {
|
|
98
|
+
add(blockSlot: Slot, proposerIndex: ValidatorIndex, blockRoot: RootHex): void {
|
|
82
99
|
if (blockSlot < this.finalizedSlot) {
|
|
83
100
|
throw Error(`blockSlot ${blockSlot} < finalizedSlot ${this.finalizedSlot}`);
|
|
84
101
|
}
|
|
85
102
|
|
|
86
|
-
this.proposerIndexesBySlot.getOrDefault(blockSlot).
|
|
103
|
+
this.proposerIndexesBySlot.getOrDefault(blockSlot).set(proposerIndex, blockRoot);
|
|
87
104
|
}
|
|
88
105
|
|
|
89
106
|
prune(finalizedSlot: Slot): void {
|
|
@@ -89,13 +89,20 @@ export async function validateGossipBlock(
|
|
|
89
89
|
|
|
90
90
|
// [IGNORE] The block is the first block with valid signature received for the proposer for the slot, signed_beacon_block.message.slot.
|
|
91
91
|
const proposerIndex = block.proposerIndex;
|
|
92
|
-
const hasBlockRoot = chain.seenBlockProposers.hasBlockRoot(blockSlot, proposerIndex, blockRoot);
|
|
93
92
|
if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
|
|
94
|
-
if (
|
|
95
|
-
|
|
96
|
-
chain.seenBlockProposers.
|
|
93
|
+
if (chain.seenBlockProposers.isRepeatProposal(blockSlot, proposerIndex, blockRoot)) {
|
|
94
|
+
const hasBlockRoot = chain.seenBlockProposers.hasBlockRoot(blockSlot, proposerIndex, blockRoot);
|
|
95
|
+
if (!hasBlockRoot && !chain.seenBlockProposers.isEquivocating(blockSlot, proposerIndex)) {
|
|
96
|
+
await verifyBlockProposerSignature(chain, signedBlock, blockRoot, {verifyOnMainThread: false});
|
|
97
|
+
chain.seenBlockProposers.observeBlockRoot(blockSlot, proposerIndex, blockRoot, signedBlockHeader);
|
|
98
|
+
}
|
|
99
|
+
throw new BlockGossipError(GossipAction.IGNORE, {
|
|
100
|
+
code: BlockErrorCode.REPEAT_PROPOSAL,
|
|
101
|
+
proposerIndex,
|
|
102
|
+
root: blockRoot,
|
|
103
|
+
});
|
|
97
104
|
}
|
|
98
|
-
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.
|
|
105
|
+
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot});
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
// [REJECT] The current finalized_checkpoint is an ancestor of block -- i.e.
|
|
@@ -301,10 +308,17 @@ export async function validateGossipBlock(
|
|
|
301
308
|
|
|
302
309
|
// Check again after all async validation and the early-block delay so concurrent proposals cannot both pass
|
|
303
310
|
if (chain.seenBlockProposers.isKnown(blockSlot, proposerIndex)) {
|
|
304
|
-
|
|
311
|
+
if (chain.seenBlockProposers.isRepeatProposal(blockSlot, proposerIndex, blockRoot)) {
|
|
312
|
+
throw new BlockGossipError(GossipAction.IGNORE, {
|
|
313
|
+
code: BlockErrorCode.REPEAT_PROPOSAL,
|
|
314
|
+
proposerIndex,
|
|
315
|
+
root: blockRoot,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot});
|
|
305
319
|
}
|
|
306
320
|
|
|
307
|
-
chain.seenBlockProposers.add(blockSlot, proposerIndex);
|
|
321
|
+
chain.seenBlockProposers.add(blockSlot, proposerIndex, blockRoot);
|
|
308
322
|
|
|
309
323
|
return {skippedSlots};
|
|
310
324
|
}
|
|
@@ -713,9 +713,32 @@ function getSequentialHandlers(modules: ValidatorFnsModules, options: GossipHand
|
|
|
713
713
|
const {serializedData} = gossipData;
|
|
714
714
|
|
|
715
715
|
const signedBlock = sszDeserialize(topic, serializedData);
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
716
|
+
try {
|
|
717
|
+
const blockInput = await validateBeaconBlock(signedBlock, topic.boundary.fork, peerIdStr, seenTimestampSec);
|
|
718
|
+
chain.serializedCache.set(signedBlock, serializedData);
|
|
719
|
+
handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
|
|
720
|
+
} catch (e) {
|
|
721
|
+
// Spec: IGNORE the block, ie not to re-publish to peers
|
|
722
|
+
// but we should still import an equivocating (REPEAT_PROPOSAL) block into fork choice because we don't
|
|
723
|
+
// know if this block (or the 1st known block with same slot) will become canonical yet
|
|
724
|
+
if (
|
|
725
|
+
e instanceof BlockGossipError &&
|
|
726
|
+
e.type.code === BlockErrorCode.REPEAT_PROPOSAL &&
|
|
727
|
+
// this is make sure the block's proposer signature was verified, it should be true anyway
|
|
728
|
+
chain.seenBlockProposers.hasBlockRoot(signedBlock.message.slot, e.type.proposerIndex, e.type.root)
|
|
729
|
+
) {
|
|
730
|
+
// blockInput was optimistically seeded in validateBeaconBlock and retained on IGNORE
|
|
731
|
+
const blockInput = chain.seenBlockInputCache.get(e.type.root);
|
|
732
|
+
if (blockInput) {
|
|
733
|
+
chain.serializedCache.set(signedBlock, serializedData);
|
|
734
|
+
// this is technically not a valid gossip block but gossip validation is a cheap subset of checks
|
|
735
|
+
// this runs the full state transition, so importing an equivocating-but-valid block here is safe.
|
|
736
|
+
handleValidBeaconBlock(blockInput, peerIdStr, seenTimestampSec);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
// rethrow so gossipValidatorFn maps IGNORE -> TopicValidatorResult.Ignore (message not forwarded)
|
|
740
|
+
throw e;
|
|
741
|
+
}
|
|
719
742
|
},
|
|
720
743
|
|
|
721
744
|
[GossipType.blob_sidecar]: async ({
|
package/src/sync/range/batch.ts
CHANGED
|
@@ -593,7 +593,7 @@ export class Batch {
|
|
|
593
593
|
if (allComplete) {
|
|
594
594
|
const attempt: Attempt = {
|
|
595
595
|
peers: this.getSuccessfulPeers(),
|
|
596
|
-
hash: hashBlocks(blocks,
|
|
596
|
+
hash: hashBlocks(blocks, payloadEnvelopes),
|
|
597
597
|
};
|
|
598
598
|
this.state = {status: BatchStatus.AwaitingProcessing, blocks, payloadEnvelopes: newPayloadEnvelopes, attempt};
|
|
599
599
|
} else {
|
|
@@ -1,27 +1,62 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {RootHex,
|
|
3
|
-
import {toRootHex} from "@lodestar/utils";
|
|
1
|
+
import {digest} from "@chainsafe/as-sha256";
|
|
2
|
+
import {RootHex, Slot, ssz} from "@lodestar/types";
|
|
3
|
+
import {fromHex, toRootHex} from "@lodestar/utils";
|
|
4
4
|
import {IBlockInput} from "../../../chain/blocks/blockInput/types.js";
|
|
5
|
+
import {PayloadEnvelopeInput} from "../../../chain/blocks/payloadEnvelopeInput/payloadEnvelopeInput.js";
|
|
6
|
+
|
|
7
|
+
const ROOT_SIZE = 32;
|
|
8
|
+
const SIGNATURE_SIZE = 96;
|
|
9
|
+
/** Every block and every payload envelope contributes a fixed-size (root, signature) entry */
|
|
10
|
+
const ENTRY_SIZE = ROOT_SIZE + SIGNATURE_SIZE;
|
|
11
|
+
/** uint32 LE counts of block and envelope entries, so the variable-length sections are self-describing */
|
|
12
|
+
const COUNT_SIZE = 4;
|
|
13
|
+
const HEADER_SIZE = COUNT_SIZE * 2;
|
|
5
14
|
|
|
6
15
|
/**
|
|
7
|
-
*
|
|
16
|
+
* Root uniquely identifying a batch attempt (its blocks AND payloads). Used for peer scoring and to
|
|
17
|
+
* compare if two attempts are equivalent.
|
|
18
|
+
*
|
|
19
|
+
* Signatures are part of the id for BOTH blocks and payload envelopes, not just their message roots:
|
|
20
|
+
* a peer serving a correct message with a garbage signature would otherwise produce the same id as
|
|
21
|
+
* the honest attempt and escape scoring in `SyncChain.advanceChain`. Signatures are not verified at
|
|
22
|
+
* download time, only during processChainSegment (block signatures in verifyBlocksSignatures,
|
|
23
|
+
* envelope signatures in importExecutionPayload).
|
|
24
|
+
*
|
|
25
|
+
* Entries are written as raw bytes into a single exactly-sized buffer and digested once, so an
|
|
26
|
+
* `Attempt` retains 32 bytes rather than the ~16 KB a full 32-slot gloas batch would need if the
|
|
27
|
+
* roots and signatures were concatenated as hex. sha256 (not a 64-bit hash) because a peer that
|
|
28
|
+
* could force a collision with the winning attempt would escape peer scoring.
|
|
8
29
|
*/
|
|
9
|
-
export function hashBlocks(blocks: IBlockInput[],
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
export function hashBlocks(blocks: IBlockInput[], payloadEnvelopes: Map<Slot, PayloadEnvelopeInput> | null): RootHex {
|
|
31
|
+
// Envelopes without a payload carry no attributable data, so they are skipped entirely. A `null`
|
|
32
|
+
// map and a map whose entries all lack a payload are therefore equivalent, which is intended.
|
|
33
|
+
const envelopes =
|
|
34
|
+
payloadEnvelopes && payloadEnvelopes.size > 0
|
|
35
|
+
? Array.from(payloadEnvelopes.entries())
|
|
36
|
+
.filter(([, envelope]) => envelope.hasPayloadEnvelope())
|
|
37
|
+
.sort(([slotA], [slotB]) => slotA - slotB)
|
|
38
|
+
: [];
|
|
39
|
+
|
|
40
|
+
const buf = Buffer.allocUnsafe(HEADER_SIZE + (blocks.length + envelopes.length) * ENTRY_SIZE);
|
|
41
|
+
buf.writeUInt32LE(blocks.length, 0);
|
|
42
|
+
buf.writeUInt32LE(envelopes.length, COUNT_SIZE);
|
|
43
|
+
let offset = HEADER_SIZE;
|
|
44
|
+
|
|
45
|
+
for (const block of blocks) {
|
|
46
|
+
buf.set(fromHex(block.blockRootHex), offset);
|
|
47
|
+
offset += ROOT_SIZE;
|
|
48
|
+
buf.set(block.getBlock().signature, offset);
|
|
49
|
+
offset += SIGNATURE_SIZE;
|
|
26
50
|
}
|
|
51
|
+
|
|
52
|
+
for (const [, envelope] of envelopes) {
|
|
53
|
+
const signedEnvelope = envelope.getPayloadEnvelope();
|
|
54
|
+
// envelope's root is cached thanks to cachePermanentRootStruct, so this avoids re-hashing
|
|
55
|
+
buf.set(ssz.gloas.ExecutionPayloadEnvelope.hashTreeRoot(signedEnvelope.message), offset);
|
|
56
|
+
offset += ROOT_SIZE;
|
|
57
|
+
buf.set(signedEnvelope.signature, offset);
|
|
58
|
+
offset += SIGNATURE_SIZE;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return toRootHex(digest(buf));
|
|
27
62
|
}
|