Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: migrate to blob side cars validation from blobs side car #5687

Merged
merged 4 commits into from
Jun 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/blocks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export type ImportBlockOpts = {
*/
validSignatures?: boolean;
/** Set to true if already run `validateBlobsSidecar()` sucessfully on the blobs */
validBlobsSidecar?: boolean;
validBlobSidecars?: boolean;
/** Seen timestamp seconds */
seenTimestampSec?: number;
/** Set to true if persist block right at verification time */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {Slot, deneb} from "@lodestar/types";
import {toHexString} from "@lodestar/utils";
import {IClock} from "../../util/clock.js";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {validateBlobsSidecar} from "../validation/blobsSidecar.js";
// TODO freetheblobs: disable the following exception once blockinput changes
/* eslint-disable @typescript-eslint/no-unused-vars */
import {validateBlobSidecars} from "../validation/blobSidecar.js";
import {BlockInput, BlockInputType, ImportBlockOpts} from "./types.js";

/**
Expand Down Expand Up @@ -126,7 +128,7 @@ function maybeValidateBlobs(
// TODO Deneb: Make switch verify it's exhaustive
switch (blockInput.type) {
case BlockInputType.postDeneb: {
if (opts.validBlobsSidecar) {
if (opts.validBlobSidecars) {
return DataAvailableStatus.available;
}

Expand All @@ -135,7 +137,8 @@ function maybeValidateBlobs(
const {blobKzgCommitments} = (block as deneb.SignedBeaconBlock).message.body;
const beaconBlockRoot = config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block.message);
// TODO Deneb: This function throws un-typed errors
validateBlobsSidecar(blockSlot, beaconBlockRoot, blobKzgCommitments, blobs);
// TODO freetheblobs: enable the following validation once blockinput is migrated
// validateBlobSidecars(blockSlot, beaconBlockRoot, blobKzgCommitments, blobs);

return DataAvailableStatus.available;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {Slot} from "@lodestar/types";
import {GossipActionError} from "./gossipValidation.js";

export enum BlobsSidecarErrorCode {
export enum BlobSidecarErrorCode {
INVALID_INDEX = "BLOBS_SIDECAR_ERROR_INVALID_INDEX",
/** !bls.KeyValidate(block.body.blob_kzg_commitments[i]) */
INVALID_KZG = "BLOBS_SIDECAR_ERROR_INVALID_KZG",
/** !verify_kzg_commitments_against_transactions(block.body.execution_payload.transactions, block.body.blob_kzg_commitments) */
Expand All @@ -14,11 +15,12 @@ export enum BlobsSidecarErrorCode {
INVALID_KZG_PROOF = "BLOBS_SIDECAR_ERROR_INVALID_KZG_PROOF",
}

export type BlobsSidecarErrorType =
| {code: BlobsSidecarErrorCode.INVALID_KZG; kzgIdx: number}
| {code: BlobsSidecarErrorCode.INVALID_KZG_TXS}
| {code: BlobsSidecarErrorCode.INCORRECT_SLOT; blockSlot: Slot; blobSlot: Slot}
| {code: BlobsSidecarErrorCode.INVALID_BLOB; blobIdx: number}
| {code: BlobsSidecarErrorCode.INVALID_KZG_PROOF};
export type BlobSidecarErrorType =
| {code: BlobSidecarErrorCode.INVALID_INDEX; blobIdx: number; gossipIndex: number}
| {code: BlobSidecarErrorCode.INVALID_KZG; blobIdx: number}
| {code: BlobSidecarErrorCode.INVALID_KZG_TXS}
| {code: BlobSidecarErrorCode.INCORRECT_SLOT; blockSlot: Slot; blobSlot: Slot; blobIdx: number}
| {code: BlobSidecarErrorCode.INVALID_BLOB; blobIdx: number}
| {code: BlobSidecarErrorCode.INVALID_KZG_PROOF; blobIdx: number};

export class BlobsSidecarError extends GossipActionError<BlobsSidecarErrorType> {}
export class BlobSidecarError extends GossipActionError<BlobSidecarErrorType> {}
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/errors/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export * from "./attestationError.js";
export * from "./attesterSlashingError.js";
export * from "./blobsSidecarError.js";
export * from "./blobSidecarError.js";
export * from "./blockError.js";
export * from "./gossipValidation.js";
export * from "./proposerSlashingError.js";
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/src/chain/regen/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum RegenCaller {
processBlock = "processBlock",
produceBlock = "produceBlock",
validateGossipBlock = "validateGossipBlock",
validateGossipBlob = "validateGossipBlob",
precomputeEpoch = "precomputeEpoch",
produceAttestationData = "produceAttestationData",
processBlocksInEpoch = "processBlocksInEpoch",
Expand Down
198 changes: 198 additions & 0 deletions packages/beacon-node/src/chain/validation/blobSidecar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import {ChainForkConfig} from "@lodestar/config";
import {deneb, Root, Slot} from "@lodestar/types";
import {toHex} from "@lodestar/utils";
import {getBlobProposerSignatureSet, computeStartSlotAtEpoch} from "@lodestar/state-transition";

import {BlobSidecarError, BlobSidecarErrorCode} from "../errors/blobSidecarError.js";
import {GossipAction} from "../errors/gossipValidation.js";
import {ckzg} from "../../util/kzg.js";
import {byteArrayEquals} from "../../util/bytes.js";
import {IBeaconChain} from "../interface.js";
import {RegenCaller} from "../regen/index.js";

// TODO: freetheblobs define blobs own gossip error
import {BlockGossipError, BlockErrorCode} from "../errors/index.js";

export async function validateGossipBlobSidecar(
config: ChainForkConfig,
chain: IBeaconChain,
signedBlob: deneb.SignedBlobSidecar,
gossipIndex: number
): Promise<void> {
const blobSidecar = signedBlob.message;
const blobSlot = blobSidecar.slot;

// [REJECT] The sidecar is for the correct topic -- i.e. sidecar.index matches the topic {index}.
if (blobSidecar.index !== gossipIndex) {
throw new BlobSidecarError(GossipAction.REJECT, {
code: BlobSidecarErrorCode.INVALID_INDEX,
blobIdx: blobSidecar.index,
gossipIndex,
});
}

// [IGNORE] The sidecar is not from a future slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance) --
// i.e. validate that sidecar.slot <= current_slot (a client MAY queue future blocks for processing at
// the appropriate slot).
const currentSlotWithGossipDisparity = chain.clock.currentSlotWithGossipDisparity;
if (currentSlotWithGossipDisparity < blobSlot) {
throw new BlockGossipError(GossipAction.IGNORE, {
code: BlockErrorCode.FUTURE_SLOT,
currentSlot: currentSlotWithGossipDisparity,
blockSlot: blobSlot,
});
}

// [IGNORE] The sidecar is from a slot greater than the latest finalized slot -- i.e. validate that
// sidecar.slot > compute_start_slot_at_epoch(state.finalized_checkpoint.epoch)
const finalizedCheckpoint = chain.forkChoice.getFinalizedCheckpoint();
const finalizedSlot = computeStartSlotAtEpoch(finalizedCheckpoint.epoch);
if (blobSlot <= finalizedSlot) {
throw new BlockGossipError(GossipAction.IGNORE, {
code: BlockErrorCode.WOULD_REVERT_FINALIZED_SLOT,
blockSlot: blobSlot,
finalizedSlot,
});
}

// Check if the block is already known. We know it is post-finalization, so it is sufficient to check the fork choice.
//
// In normal operation this isn't necessary, however it is useful immediately after a
// reboot if the `observed_block_producers` cache is empty. In that case, without this
// check, we will load the parent and state from disk only to find out later that we
// already know this block.
const blockRoot = toHex(blobSidecar.blockRoot);
if (chain.forkChoice.getBlockHex(blockRoot) !== null) {
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.ALREADY_KNOWN, root: blockRoot});
}

// TODO: freetheblobs - check for badblock
// TODO: freetheblobs - check that its first blob with valid signature

// _[IGNORE]_ The blob's block's parent (defined by `sidecar.block_parent_root`) has been seen (via both
// gossip and non-gossip sources) (a client MAY queue blocks for processing once the parent block is
// retrieved).
const parentRoot = toHex(blobSidecar.blockParentRoot);
const parentBlock = chain.forkChoice.getBlockHex(parentRoot);
if (parentBlock === null) {
// If fork choice does *not* consider the parent to be a descendant of the finalized block,
// then there are two more cases:
//
// 1. We have the parent stored in our database. Because fork-choice has confirmed the
// parent is *not* in our post-finalization DAG, all other blocks must be either
// pre-finalization or conflicting with finalization.
// 2. The parent is unknown to us, we probably want to download it since it might actually
// descend from the finalized root.
// (Non-Lighthouse): Since we prune all blocks non-descendant from finalized checking the `db.block` database won't be useful to guard
// against known bad fork blocks, so we throw PARENT_UNKNOWN for cases (1) and (2)
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot});
}

// [REJECT] The blob is from a higher slot than its parent.
if (parentBlock.slot >= blobSlot) {
throw new BlockGossipError(GossipAction.IGNORE, {
code: BlockErrorCode.NOT_LATER_THAN_PARENT,
parentSlot: parentBlock.slot,
slot: blobSlot,
});
}

// getBlockSlotState also checks for whether the current finalized checkpoint is an ancestor of the block.
// As a result, we throw an IGNORE (whereas the spec says we should REJECT for this scenario).
// this is something we should change this in the future to make the code airtight to the spec.
// _[IGNORE]_ The blob's block's parent (defined by `sidecar.block_parent_root`) has been seen (via both
// gossip and non-gossip sources) // _[REJECT]_ The blob's block's parent (defined by `sidecar.block_parent_root`) passes validation
// The above validation will happen while importing
const blockState = await chain.regen
.getBlockSlotState(parentRoot, blobSlot, {dontTransferCache: true}, RegenCaller.validateGossipBlob)
.catch(() => {
throw new BlockGossipError(GossipAction.IGNORE, {code: BlockErrorCode.PARENT_UNKNOWN, parentRoot});
});

// _[REJECT]_ The proposer signature, `signed_blob_sidecar.signature`, is valid with respect to the
// `sidecar.proposer_index` pubkey.
const signatureSet = getBlobProposerSignatureSet(blockState, signedBlob);
// Don't batch so verification is not delayed
if (!(await chain.bls.verifySignatureSets([signatureSet], {verifyOnMainThread: true}))) {
throw new BlockGossipError(GossipAction.REJECT, {
code: BlockErrorCode.PROPOSAL_SIGNATURE_INVALID,
});
}

// _[IGNORE]_ The sidecar is the only sidecar with valid signature received for the tuple
// `(sidecar.block_root, sidecar.index)`
//
// This is already taken care of by the way we group the blobs in getFullBlockInput helper
// but may be an error can be thrown there for this

// _[REJECT]_ The sidecar is proposed by the expected `proposer_index` for the block's slot in the
// context of the current shuffling (defined by `block_parent_root`/`slot`)
// If the `proposer_index` cannot immediately be verified against the expected shuffling, the sidecar
// MAY be queued for later processing while proposers for the block's branch are calculated -- in such
// a case _do not_ `REJECT`, instead `IGNORE` this message.
const proposerIndex = blobSidecar.proposerIndex;
if (blockState.epochCtx.getBeaconProposer(blobSlot) !== proposerIndex) {
throw new BlockGossipError(GossipAction.REJECT, {code: BlockErrorCode.INCORRECT_PROPOSER, proposerIndex});
}

// blob, proof and commitment as a valid BLS G1 point gets verified in batch validation
validateBlobsAndProofs([blobSidecar.kzgCommitment], [blobSidecar.blob], [blobSidecar.kzgProof]);
}

// https://github.com/ethereum/consensus-specs/blob/dev/specs/eip4844/beacon-chain.md#validate_blobs_sidecar
export function validateBlobSidecars(
blockSlot: Slot,
blockRoot: Root,
expectedKzgCommitments: deneb.BlobKzgCommitments,
blobSidecars: deneb.BlobSidecars
): void {
// assert len(expected_kzg_commitments) == len(blobs)
if (expectedKzgCommitments.length !== blobSidecars.length) {
throw new Error(
`blobSidecars length to commitments length mismatch. Blob length: ${blobSidecars.length}, Expected commitments length ${expectedKzgCommitments.length}`
);
}

// No need to verify the aggregate proof of zero blobs
if (blobSidecars.length > 0) {
// Verify the blob slot and root matches
const blobs = [];
const proofs = [];
for (let index = 0; index < blobSidecars.length; index++) {
const blobSidecar = blobSidecars[index];
if (
blobSidecar.slot !== blockSlot ||
!byteArrayEquals(blobSidecar.blockRoot, blockRoot) ||
blobSidecar.index !== index ||
!byteArrayEquals(expectedKzgCommitments[index], blobSidecar.kzgCommitment)
) {
throw new Error(
`Invalid blob with slot=${blobSidecar.slot} blockRoot=${toHex(blockRoot)} index=${
blobSidecar.index
} for the block root=${toHex(blockRoot)} slot=${blockSlot} index=${index}`
);
}
blobs.push(blobSidecar.blob);
proofs.push(blobSidecar.kzgProof);
}
validateBlobsAndProofs(expectedKzgCommitments, blobs, proofs);
}
}

function validateBlobsAndProofs(
expectedKzgCommitments: deneb.BlobKzgCommitments,
blobs: deneb.Blobs,
proofs: deneb.KZGProofs
): void {
// assert verify_aggregate_kzg_proof(blobs, expected_kzg_commitments, kzg_aggregated_proof)
let isProofValid: boolean;
try {
isProofValid = ckzg.verifyBlobKzgProofBatch(blobs, expectedKzgCommitments, proofs);
} catch (e) {
(e as Error).message = `Error on verifyBlobKzgProofBatch: ${(e as Error).message}`;
throw e;
}
if (!isProofValid) {
throw Error("Invalid verifyBlobKzgProofBatch");
}
}
Loading