Skip to content

Commit f3703b7

Browse files
authored
feat: signature verification for reqresp DA (#8580)
**Motivation** Spec will be updated to have check of signatures via reqresp. Proacative fix inline with Lighthouse and Prysm sigp/lighthouse#7650
1 parent 6832b02 commit f3703b7

File tree

16 files changed

+237
-111
lines changed

16 files changed

+237
-111
lines changed

packages/beacon-node/src/chain/errors/blobSidecarError.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,19 @@ export type BlobSidecarErrorType =
5050
| {code: BlobSidecarErrorCode.FUTURE_SLOT; blockSlot: Slot; currentSlot: Slot}
5151
| {code: BlobSidecarErrorCode.WOULD_REVERT_FINALIZED_SLOT; blockSlot: Slot; finalizedSlot: Slot}
5252
| {code: BlobSidecarErrorCode.ALREADY_KNOWN; root: RootHex}
53-
| {code: BlobSidecarErrorCode.PARENT_UNKNOWN; parentRoot: RootHex}
53+
| {
54+
code: BlobSidecarErrorCode.PARENT_UNKNOWN;
55+
parentRoot: RootHex;
56+
slot: Slot;
57+
blockRoot: RootHex;
58+
}
5459
| {code: BlobSidecarErrorCode.NOT_LATER_THAN_PARENT; parentSlot: Slot; slot: Slot}
55-
| {code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID}
60+
| {
61+
code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID;
62+
blockRoot: RootHex;
63+
slot: Slot;
64+
index: number;
65+
}
5666
| {code: BlobSidecarErrorCode.INCLUSION_PROOF_INVALID; slot: Slot; blobIdx: number}
5767
| {code: BlobSidecarErrorCode.INCORRECT_PROPOSER; proposerIndex: ValidatorIndex};
5868

packages/beacon-node/src/chain/errors/dataColumnSidecarError.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,17 @@ export type DataColumnSidecarErrorType =
5858
| {code: DataColumnSidecarErrorCode.ALREADY_KNOWN; columnIndex: number; slot: Slot}
5959
| {code: DataColumnSidecarErrorCode.FUTURE_SLOT; blockSlot: Slot; currentSlot: Slot}
6060
| {code: DataColumnSidecarErrorCode.WOULD_REVERT_FINALIZED_SLOT; blockSlot: Slot; finalizedSlot: Slot}
61-
| {code: DataColumnSidecarErrorCode.PARENT_UNKNOWN; parentRoot: RootHex}
62-
| {code: DataColumnSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID}
61+
| {
62+
code: DataColumnSidecarErrorCode.PARENT_UNKNOWN;
63+
parentRoot: RootHex;
64+
slot: Slot;
65+
}
66+
| {
67+
code: DataColumnSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID;
68+
slot: Slot;
69+
blockRoot: RootHex;
70+
index: number;
71+
}
6372
| {code: DataColumnSidecarErrorCode.NOT_LATER_THAN_PARENT; parentSlot: Slot; slot: Slot}
6473
| {code: DataColumnSidecarErrorCode.INCLUSION_PROOF_INVALID; slot: Slot; columnIndex: number}
6574
| {code: DataColumnSidecarErrorCode.INVALID_KZG_PROOF; slot: Slot; columnIndex: number}

packages/beacon-node/src/chain/validation/blobSidecar.ts

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import {
88
import {
99
computeEpochAtSlot,
1010
computeStartSlotAtEpoch,
11-
getBlockHeaderProposerSignatureSet,
11+
getBlockHeaderProposerSignatureSetByHeaderSlot,
12+
getBlockHeaderProposerSignatureSetByParentStateSlot,
1213
} from "@lodestar/state-transition";
1314
import {BlobIndex, Root, Slot, SubnetID, deneb, ssz} from "@lodestar/types";
1415
import {toRootHex, verifyMerkleBranch} from "@lodestar/utils";
@@ -100,7 +101,12 @@ export async function validateGossipBlobSidecar(
100101
// descend from the finalized root.
101102
// (Non-Lighthouse): Since we prune all blocks non-descendant from finalized checking the `db.block` database won't be useful to guard
102103
// against known bad fork blocks, so we throw PARENT_UNKNOWN for cases (1) and (2)
103-
throw new BlobSidecarGossipError(GossipAction.IGNORE, {code: BlobSidecarErrorCode.PARENT_UNKNOWN, parentRoot});
104+
throw new BlobSidecarGossipError(GossipAction.IGNORE, {
105+
code: BlobSidecarErrorCode.PARENT_UNKNOWN,
106+
parentRoot,
107+
blockRoot: blockHex,
108+
slot: blobSlot,
109+
});
104110
}
105111

106112
// [REJECT] The blob is from a higher slot than its parent.
@@ -120,15 +126,23 @@ export async function validateGossipBlobSidecar(
120126
const blockState = await chain.regen
121127
.getBlockSlotState(parentRoot, blobSlot, {dontTransferCache: true}, RegenCaller.validateGossipBlock)
122128
.catch(() => {
123-
throw new BlobSidecarGossipError(GossipAction.IGNORE, {code: BlobSidecarErrorCode.PARENT_UNKNOWN, parentRoot});
129+
throw new BlobSidecarGossipError(GossipAction.IGNORE, {
130+
code: BlobSidecarErrorCode.PARENT_UNKNOWN,
131+
parentRoot,
132+
blockRoot: blockHex,
133+
slot: blobSlot,
134+
});
124135
});
125136

126137
// [REJECT] The proposer signature, signed_beacon_block.signature, is valid with respect to the proposer_index pubkey.
127-
const signatureSet = getBlockHeaderProposerSignatureSet(blockState, blobSidecar.signedBlockHeader);
138+
const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot(blockState, blobSidecar.signedBlockHeader);
128139
// Don't batch so verification is not delayed
129140
if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) {
130141
throw new BlobSidecarGossipError(GossipAction.REJECT, {
131142
code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID,
143+
blockRoot: blockHex,
144+
index: blobSidecar.index,
145+
slot: blobSlot,
132146
});
133147
}
134148

@@ -175,8 +189,12 @@ export async function validateGossipBlobSidecar(
175189
* Validate some blob sidecars in a block
176190
*
177191
* Requires the block to be known to the node
192+
*
193+
* NOTE: chain is optional to skip signature verification. Helpful for testing purposes and so that can control whether
194+
* signature gets checked depending on the reqresp method that is being checked
178195
*/
179196
export async function validateBlockBlobSidecars(
197+
chain: IBeaconChain | null,
180198
blockSlot: Slot,
181199
blockRoot: Root,
182200
blockBlobCount: number,
@@ -196,7 +214,8 @@ export async function validateBlockBlobSidecars(
196214
}
197215

198216
// Hash the first sidecar block header and compare the rest via (cheaper) equality
199-
const firstSidecarBlockHeader = blobSidecars[0].signedBlockHeader.message;
217+
const firstSidecarSignedBlockHeader = blobSidecars[0].signedBlockHeader;
218+
const firstSidecarBlockHeader = firstSidecarSignedBlockHeader.message;
200219
const firstBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(firstSidecarBlockHeader);
201220
if (Buffer.compare(blockRoot, firstBlockRoot) !== 0) {
202221
throw new BlobSidecarValidationError(
@@ -211,17 +230,42 @@ export async function validateBlockBlobSidecars(
211230
);
212231
}
213232

233+
if (chain !== null) {
234+
const headState = await chain.getHeadState();
235+
const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot(headState, firstSidecarSignedBlockHeader);
236+
237+
if (
238+
!(await chain.bls.verifySignatureSets([signatureSet], {
239+
batchable: true,
240+
priority: true,
241+
verifyOnMainThread: false,
242+
}))
243+
) {
244+
throw new BlobSidecarValidationError({
245+
code: BlobSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID,
246+
blockRoot: toRootHex(blockRoot),
247+
slot: blockSlot,
248+
index: blobSidecars[0].index,
249+
});
250+
}
251+
}
252+
214253
const commitments = [];
215254
const blobs = [];
216255
const proofs = [];
217-
for (const blobSidecar of blobSidecars) {
218-
const blobIdx = blobSidecar.index;
219-
if (!ssz.phase0.BeaconBlockHeader.equals(blobSidecar.signedBlockHeader.message, firstSidecarBlockHeader)) {
256+
for (let i = 0; i < blobSidecars.length; i++) {
257+
const blobSidecar = blobSidecars[i];
258+
const blobIndex = blobSidecar.index;
259+
260+
if (
261+
i !== 0 &&
262+
!ssz.phase0.SignedBeaconBlockHeader.equals(blobSidecar.signedBlockHeader, firstSidecarSignedBlockHeader)
263+
) {
220264
throw new BlobSidecarValidationError(
221265
{
222266
code: BlobSidecarErrorCode.INCORRECT_BLOCK,
223267
slot: blockSlot,
224-
blobIdx,
268+
blobIdx: blobIndex,
225269
expected: toRootHex(blockRoot),
226270
actual: "unknown - compared via equality",
227271
},
@@ -234,7 +278,7 @@ export async function validateBlockBlobSidecars(
234278
{
235279
code: BlobSidecarErrorCode.INCLUSION_PROOF_INVALID,
236280
slot: blockSlot,
237-
blobIdx,
281+
blobIdx: blobIndex,
238282
},
239283
"BlobSidecar inclusion proof invalid"
240284
);

packages/beacon-node/src/chain/validation/dataColumnSidecar.ts

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import {
77
import {
88
computeEpochAtSlot,
99
computeStartSlotAtEpoch,
10-
getBlockHeaderProposerSignatureSet,
10+
getBlockHeaderProposerSignatureSetByHeaderSlot,
11+
getBlockHeaderProposerSignatureSetByParentStateSlot,
1112
} from "@lodestar/state-transition";
1213
import {Root, Slot, SubnetID, fulu, ssz} from "@lodestar/types";
1314
import {toRootHex, verifyMerkleBranch} from "@lodestar/utils";
@@ -86,6 +87,7 @@ export async function validateGossipDataColumnSidecar(
8687
throw new DataColumnSidecarGossipError(GossipAction.IGNORE, {
8788
code: DataColumnSidecarErrorCode.PARENT_UNKNOWN,
8889
parentRoot,
90+
slot: blockHeader.slot,
8991
});
9092
}
9193

@@ -108,6 +110,7 @@ export async function validateGossipDataColumnSidecar(
108110
throw new DataColumnSidecarGossipError(GossipAction.IGNORE, {
109111
code: DataColumnSidecarErrorCode.PARENT_UNKNOWN,
110112
parentRoot,
113+
slot: blockHeader.slot,
111114
});
112115
});
113116

@@ -128,15 +131,23 @@ export async function validateGossipDataColumnSidecar(
128131
}
129132

130133
// 5) [REJECT] The proposer signature of sidecar.signed_block_header, is valid with respect to the block_header.proposer_index pubkey.
131-
const signatureSet = getBlockHeaderProposerSignatureSet(blockState, dataColumnSidecar.signedBlockHeader);
134+
const signatureSet = getBlockHeaderProposerSignatureSetByParentStateSlot(
135+
blockState,
136+
dataColumnSidecar.signedBlockHeader
137+
);
132138
// Don't batch so verification is not delayed
133139
if (
134140
!(await chain.bls.verifySignatureSets([signatureSet], {
135141
verifyOnMainThread: blockHeader.slot > chain.forkChoice.getHead().slot,
136142
}))
137143
) {
144+
const blockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(dataColumnSidecar.signedBlockHeader.message);
145+
const blockRootHex = toRootHex(blockRoot);
138146
throw new DataColumnSidecarGossipError(GossipAction.REJECT, {
139147
code: DataColumnSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID,
148+
blockRoot: blockRootHex,
149+
index: dataColumnSidecar.index,
150+
slot: blockHeader.slot,
140151
});
141152
}
142153

@@ -271,8 +282,12 @@ export function verifyDataColumnSidecarInclusionProof(dataColumnSidecar: fulu.Da
271282
* Validate a subset of data column sidecars in a block
272283
*
273284
* Requires the block to be known to the node
285+
*
286+
* NOTE: chain is optional to skip signature verification. Helpful for testing purposes and so that can control whether
287+
* signature gets checked depending on the reqresp method that is being checked
274288
*/
275289
export async function validateBlockDataColumnSidecars(
290+
chain: IBeaconChain | null,
276291
blockSlot: Slot,
277292
blockRoot: Root,
278293
blockBlobCount: number,
@@ -294,7 +309,8 @@ export async function validateBlockDataColumnSidecars(
294309
);
295310
}
296311
// Hash the first sidecar block header and compare the rest via (cheaper) equality
297-
const firstSidecarBlockHeader = dataColumnSidecars[0].signedBlockHeader.message;
312+
const firstSidecarSignedBlockHeader = dataColumnSidecars[0].signedBlockHeader;
313+
const firstSidecarBlockHeader = firstSidecarSignedBlockHeader.message;
298314
const firstBlockRoot = ssz.phase0.BeaconBlockHeader.hashTreeRoot(firstSidecarBlockHeader);
299315
if (Buffer.compare(blockRoot, firstBlockRoot) !== 0) {
300316
throw new DataColumnSidecarValidationError(
@@ -309,14 +325,37 @@ export async function validateBlockDataColumnSidecars(
309325
);
310326
}
311327

328+
if (chain !== null) {
329+
const headState = await chain.getHeadState();
330+
const signatureSet = getBlockHeaderProposerSignatureSetByHeaderSlot(headState, firstSidecarSignedBlockHeader);
331+
332+
if (
333+
!(await chain.bls.verifySignatureSets([signatureSet], {
334+
batchable: true,
335+
priority: true,
336+
verifyOnMainThread: false,
337+
}))
338+
) {
339+
throw new DataColumnSidecarValidationError({
340+
code: DataColumnSidecarErrorCode.PROPOSAL_SIGNATURE_INVALID,
341+
blockRoot: toRootHex(blockRoot),
342+
slot: blockSlot,
343+
index: dataColumnSidecars[0].index,
344+
});
345+
}
346+
}
347+
312348
const commitments: Uint8Array[] = [];
313349
const cellIndices: number[] = [];
314350
const cells: Uint8Array[] = [];
315351
const proofs: Uint8Array[] = [];
316352
for (let i = 0; i < dataColumnSidecars.length; i++) {
317353
const columnSidecar = dataColumnSidecars[i];
318354

319-
if (!ssz.phase0.BeaconBlockHeader.equals(firstSidecarBlockHeader, columnSidecar.signedBlockHeader.message)) {
355+
if (
356+
i !== 0 &&
357+
!ssz.phase0.SignedBeaconBlockHeader.equals(firstSidecarSignedBlockHeader, columnSidecar.signedBlockHeader)
358+
) {
320359
throw new DataColumnSidecarValidationError({
321360
code: DataColumnSidecarErrorCode.INCORRECT_HEADER_ROOT,
322361
slot: blockSlot,

packages/beacon-node/src/sync/unknownBlock.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,7 @@ export class BlockInputSync {
532532
const downloadResult = await downloadByRoot({
533533
config: this.config,
534534
network: this.network,
535-
seenCache: this.chain.seenBlockInputCache,
535+
chain: this.chain,
536536
emitter: this.chain.emitter,
537537
peerMeta,
538538
cacheItem,

packages/beacon-node/src/sync/utils/downloadByRange.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ export type DownloadByRangeResponses = {
3737

3838
export type DownloadAndCacheByRangeProps = DownloadByRangeRequests & {
3939
config: ChainForkConfig;
40-
cache: SeenBlockInput;
4140
network: INetwork;
4241
logger: Logger;
4342
peerIdStr: string;
@@ -203,7 +202,7 @@ export async function downloadByRange({
203202
blocksRequest,
204203
blobsRequest,
205204
columnsRequest,
206-
}: Omit<DownloadAndCacheByRangeProps, "cache">): Promise<WarnResult<ValidatedResponses, DownloadByRangeError>> {
205+
}: DownloadAndCacheByRangeProps): Promise<WarnResult<ValidatedResponses, DownloadByRangeError>> {
207206
let response: DownloadByRangeResponses;
208207
try {
209208
response = await requestByRange({
@@ -555,9 +554,13 @@ export async function validateBlobsByRangeResponse(
555554
}
556555

557556
validateSidecarsPromises.push(
558-
validateBlockBlobSidecars(block.message.slot, blockRoot, blockKzgCommitments.length, blockBlobSidecars).then(
559-
() => ({blockRoot, blobSidecars: blockBlobSidecars})
560-
)
557+
validateBlockBlobSidecars(
558+
null, // do not pass chain here so we do not validate header signature
559+
block.message.slot,
560+
blockRoot,
561+
blockKzgCommitments.length,
562+
blockBlobSidecars
563+
).then(() => ({blockRoot, blobSidecars: blockBlobSidecars}))
561564
);
562565
}
563566

@@ -768,7 +771,13 @@ export async function validateColumnsByRangeResponse(
768771
}
769772

770773
validationPromises.push(
771-
validateBlockDataColumnSidecars(slot, blockRoot, blobCount, columnSidecars).then(() => ({
774+
validateBlockDataColumnSidecars(
775+
null, // do not pass chain here so we do not validate header signature
776+
slot,
777+
blockRoot,
778+
blobCount,
779+
columnSidecars
780+
).then(() => ({
772781
blockRoot,
773782
columnSidecars,
774783
}))

0 commit comments

Comments
 (0)