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: add inclusion lists for censorship resistance #6551

Draft
wants to merge 7 commits into
base: unstable
Choose a base branch
from
Draft
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
73 changes: 54 additions & 19 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import {fromHexString, toHexString} from "@chainsafe/ssz";
import {routes, ServerApi, ResponseFormat} from "@lodestar/api";
import {computeTimeAtSlot, reconstructFullBlockOrContents} from "@lodestar/state-transition";
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {SLOTS_PER_HISTORICAL_ROOT, isForkBlobs, isForkILs} from "@lodestar/params";
import {sleep, toHex} from "@lodestar/utils";
import {allForks, deneb, isSignedBlockContents, ProducedBlockSource} from "@lodestar/types";
import {BlockSource, getBlockInput, ImportBlockOpts, BlockInput} from "../../../../chain/blocks/types.js";
import {allForks, deneb, electra, isSignedBlockContents, ProducedBlockSource, ssz} from "@lodestar/types";
import {
BlockSource,
getBlockInput,
ImportBlockOpts,
BlockInput,
BlockInputILType,
BlockInputDataBlobs,
BlockInputDataIls,
} from "../../../../chain/blocks/types.js";
import {promiseAllMaybeAsync} from "../../../../util/promises.js";
import {isOptimisticBlock} from "../../../../util/forkChoice.js";
import {computeBlobSidecars} from "../../../../util/blobs.js";
Expand Down Expand Up @@ -42,34 +50,59 @@ export function getBeaconBlockApi({
opts: PublishBlockOpts = {}
) => {
const seenTimestampSec = Date.now() / 1000;
let blockForImport: BlockInput, signedBlock: allForks.SignedBeaconBlock, blobSidecars: deneb.BlobSidecars;
const signedBlock = isSignedBlockContents(signedBlockOrContents)
? signedBlockOrContents.signedBlock
: signedBlockOrContents;
// if block is locally produced, full or blinded, it already is 'consensus' validated as it went through
// state transition to produce the stateRoot
const slot = signedBlock.message.slot;
const fork = config.getForkName(slot);

let blockForImport: BlockInput,
blobSidecars: deneb.BlobSidecars,
signedInclusionList: electra.SignedInclusionList | null;
if (isSignedBlockContents(signedBlockOrContents)) {
({signedBlock} = signedBlockOrContents);
blobSidecars = computeBlobSidecars(config, signedBlock, signedBlockOrContents);
blockForImport = getBlockInput.postDeneb(
config,
signedBlock,
BlockSource.api,
blobSidecars,
// don't bundle any bytes for block and blobs
null,
blobSidecars.map(() => null)
);
let blockData;
if (!isForkBlobs(fork)) {
throw Error(`Invalid fork=${fork} for publishing signedBlockOrContents`);
} else if (!isForkILs(fork)) {
signedInclusionList = null;
blockData = {fork, blobs: blobSidecars, blobsBytes: [null]} as BlockInputDataBlobs;
} else {
// pick the IL withsout signing anything for now
signedInclusionList = ssz.electra.SignedInclusionList.defaultValue();
const blockHash = toHexString(
(signedBlock as electra.SignedBeaconBlock).message.body.executionPayload.blockHash
);
const producedList = chain.producedInclusionList.get(blockHash);
if (producedList === undefined) {
throw Error("No Inclusion List produced for the block");
}

signedInclusionList.message.signedSummary.message = producedList.ilSummary;
signedInclusionList.message.transactions = producedList.ilTransactions;

blockData = {
fork,
blobs: blobSidecars,
blobsBytes: [null],
ilType: BlockInputILType.actualIL,
inclusionList: signedInclusionList.message,
} as BlockInputDataIls;
}

blockForImport = getBlockInput.postDeneb(config, signedBlock, null, blockData, BlockSource.api);
} else {
signedBlock = signedBlockOrContents;
blobSidecars = [];
signedInclusionList = null;
// TODO: Once API supports submitting data as SSZ, replace null with blockBytes
blockForImport = getBlockInput.preDeneb(config, signedBlock, BlockSource.api, null);
}

// check what validations have been requested before broadcasting and publishing the block
// TODO: add validation time to metrics
const broadcastValidation = opts.broadcastValidation ?? routes.beacon.BroadcastValidation.gossip;
// if block is locally produced, full or blinded, it already is 'consensus' validated as it went through
// state transition to produce the stateRoot
const slot = signedBlock.message.slot;
const fork = config.getForkName(slot);
const blockRoot = toHex(chain.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(signedBlock.message));
// bodyRoot should be the same to produced block
const bodyRoot = toHex(chain.config.getForkTypes(slot).BeaconBlockBody.hashTreeRoot(signedBlock.message.body));
Expand Down Expand Up @@ -193,6 +226,8 @@ export function getBeaconBlockApi({
// b) they might require more hops to reach recipients in peerDAS kind of setup where
// blobs might need to hop between nodes because of partial subnet subscription
...blobSidecars.map((blobSidecar) => () => network.publishBlobSidecar(blobSidecar)),
// republish the inclusion list even though it might have been already published
() => (signedInclusionList !== null ? network.publishInclusionList(signedInclusionList) : Promise.resolve(0)),
() => network.publishBeaconBlock(signedBlock) as Promise<unknown>,
() =>
// there is no rush to persist block since we published it to gossip anyway
Expand Down
3 changes: 3 additions & 0 deletions packages/beacon-node/src/api/impl/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
DOMAIN_SYNC_COMMITTEE_SELECTION_PROOF,
DOMAIN_CONTRIBUTION_AND_PROOF,
DOMAIN_BLS_TO_EXECUTION_CHANGE,
DOMAIN_INCLUSION_LIST_SUMMARY,
DOMAIN_APPLICATION_BUILDER,
TIMELY_SOURCE_FLAG_INDEX,
TIMELY_TARGET_FLAG_INDEX,
Expand Down Expand Up @@ -100,4 +101,6 @@ export const specConstants = {
// Deneb types
BLOB_TX_TYPE,
VERSIONED_HASH_VERSION_KZG,

DOMAIN_INCLUSION_LIST_SUMMARY,
};
57 changes: 45 additions & 12 deletions packages/beacon-node/src/chain/blocks/importBlock.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {toHexString} from "@chainsafe/ssz";
import {capella, ssz, allForks, altair} from "@lodestar/types";
import {ForkSeq, INTERVALS_PER_SLOT, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH} from "@lodestar/params";
import {capella, ssz, allForks, altair, electra} from "@lodestar/types";
import {ForkSeq, INTERVALS_PER_SLOT, MAX_SEED_LOOKAHEAD, SLOTS_PER_EPOCH, isForkILs, ForkName} from "@lodestar/params";
import {
CachedBeaconStateAltair,
computeEpochAtSlot,
Expand All @@ -9,7 +9,14 @@ import {
RootCache,
} from "@lodestar/state-transition";
import {routes} from "@lodestar/api";
import {ForkChoiceError, ForkChoiceErrorCode, EpochDifference, AncestorStatus} from "@lodestar/fork-choice";
import {
ForkChoiceError,
ForkChoiceErrorCode,
EpochDifference,
AncestorStatus,
InclusionListStatus,
ExecutionStatus,
} from "@lodestar/fork-choice";
import {isErrorAborted} from "@lodestar/utils";
import {ZERO_HASH_HEX} from "../../constants/index.js";
import {toCheckpointHex} from "../stateCache/index.js";
Expand All @@ -19,7 +26,7 @@ import {kzgCommitmentToVersionedHash} from "../../util/blobs.js";
import {ChainEvent, ReorgEventData} from "../emitter.js";
import {REPROCESS_MIN_TIME_TO_NEXT_SLOT_SEC} from "../reprocess.js";
import type {BeaconChain} from "../chain.js";
import {FullyVerifiedBlock, ImportBlockOpts, AttestationImportOpt, BlockInputType} from "./types.js";
import {FullyVerifiedBlock, ImportBlockOpts, AttestationImportOpt, BlockInputType, BlockInputILType} from "./types.js";
import {getCheckpointFromState} from "./utils/checkpoint.js";
import {writeBlockInputToDb} from "./writeBlockInputToDb.js";

Expand Down Expand Up @@ -75,17 +82,43 @@ export async function importBlock(
await writeBlockInputToDb.call(this, [blockInput]);
}

let ilStatus;
let inclusionList: electra.InclusionList | undefined = undefined;
if (blockInput.type === BlockInputType.preDeneb) {
ilStatus = InclusionListStatus.PreIL;
} else {
const blockData =
blockInput.type === BlockInputType.postDeneb
? blockInput.blockData
: await blockInput.cachedData.availabilityPromise;
if (blockData.fork === ForkName.deneb) {
ilStatus = InclusionListStatus.PreIL;
} else {
if (blockData.ilType === BlockInputILType.childBlock || blockData.ilType === BlockInputILType.syncing) {
// if child is valid, the onBlock on protoArray will auto propagate the valid child upwards
ilStatus = InclusionListStatus.Syncing;
} else if (blockData.ilType === BlockInputILType.actualIL) {
// if IL was available and the block is valid, IL can be marked valid as IL would have been validated
// in the verification
//
// TODO : build and bundle te ilStatus in verify execution payload section itself
ilStatus = executionStatus === ExecutionStatus.Valid ? InclusionListStatus.Valid : InclusionListStatus.Syncing;
inclusionList = blockData.inclusionList;
} else {
throw Error("Parsing error, il");
}
}
}

// 2. Import block to fork choice

// Should compute checkpoint balances before forkchoice.onBlock
this.checkpointBalancesCache.processState(blockRootHex, postState);
const blockSummary = this.forkChoice.onBlock(
block.message,
postState,
blockDelaySec,
this.clock.currentSlot,
executionStatus
);
const blockSummary = this.forkChoice.onBlock(block.message, postState, blockDelaySec, this.clock.currentSlot, {
executionStatus,
ilStatus,
inclusionList,
});

// This adds the state necessary to process the next block
// Some block event handlers require state being in state cache so need to do this before emitting EventType.block
Expand All @@ -103,7 +136,7 @@ export async function importBlock(
});

if (blockInput.type === BlockInputType.postDeneb) {
for (const blobSidecar of blockInput.blobs) {
for (const blobSidecar of blockInput.blockData.blobs) {
const {index, kzgCommitment} = blobSidecar;
this.emitter.emit(routes.events.EventType.blobSidecar, {
blockRoot: blockRootHex,
Expand Down
75 changes: 50 additions & 25 deletions packages/beacon-node/src/chain/blocks/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {CachedBeaconStateAllForks, computeEpochAtSlot, DataAvailableStatus} from "@lodestar/state-transition";
import {MaybeValidExecutionStatus} from "@lodestar/fork-choice";
import {allForks, deneb, Slot, RootHex} from "@lodestar/types";
import {ForkSeq} from "@lodestar/params";
import {allForks, deneb, electra, Slot, RootHex} from "@lodestar/types";
import {ForkSeq, ForkName, ForkILs} from "@lodestar/params";
import {ChainForkConfig} from "@lodestar/config";

export enum BlockInputType {
Expand All @@ -21,22 +21,53 @@ export enum BlockSource {
export enum GossipedInputType {
block = "block",
blob = "blob",
ilist = "ilist",
}

export enum BlockInputILType {
childBlock = "childBlock",
actualIL = "actualIL",
syncing = "syncing",
}

type ForkBlobsInfo = {fork: ForkName.deneb};
type ForkILsInfo = {fork: ForkILs};

export type BlobsCache = Map<number, {blobSidecar: deneb.BlobSidecar; blobBytes: Uint8Array | null}>;
export type BlockInputBlobs = {blobs: deneb.BlobSidecars; blobsBytes: (Uint8Array | null)[]};
type CachedBlobs = {
blobsCache: BlobsCache;
availabilityPromise: Promise<BlockInputBlobs>;
resolveAvailability: (blobs: BlockInputBlobs) => void;

type BlobsData = {blobs: deneb.BlobSidecars; blobsBytes: (Uint8Array | null)[]};
type ILsData = BlobsData &
(
| {ilType: BlockInputILType.childBlock | BlockInputILType.syncing}
| {ilType: BlockInputILType.actualIL; inclusionList: electra.InclusionList}
);

export type BlockInputDataBlobs = ForkBlobsInfo & BlobsData;
export type BlockInputDataIls = ForkILsInfo & ILsData;
export type BlockInputData = BlockInputDataBlobs | BlockInputDataIls;

type BlobsInputCache = {blobsCache: BlobsCache};
type ForkILsCache = BlobsInputCache & {
inclusionList?: electra.InclusionList;
};

export type BlockInputCacheBlobs = ForkBlobsInfo & BlobsInputCache;
export type BlockInputCacheILs = ForkILsInfo & ForkILsCache;
export type BlockInputCache = (ForkBlobsInfo & BlobsInputCache) | (ForkILsInfo & ForkILsCache);

type Availability<T> = {availabilityPromise: Promise<T>; resolveAvailability: (data: T) => void};
export type CachedData =
| (ForkBlobsInfo & BlobsInputCache & Availability<BlockInputDataBlobs>)
| (ForkILsInfo & ForkILsCache & Availability<BlockInputDataIls>);

export type BlockInput = {block: allForks.SignedBeaconBlock; source: BlockSource; blockBytes: Uint8Array | null} & (
| {type: BlockInputType.preDeneb}
| ({type: BlockInputType.postDeneb} & BlockInputBlobs)
| ({type: BlockInputType.blobsPromise} & CachedBlobs)
| ({type: BlockInputType.postDeneb} & {blockData: BlockInputData})
| ({type: BlockInputType.blobsPromise} & {cachedData: CachedData})
);
export type NullBlockInput = {block: null; blockRootHex: RootHex; blockInputPromise: Promise<BlockInput>} & CachedBlobs;
export type NullBlockInput = {block: null; blockRootHex: RootHex; blockInputPromise: Promise<BlockInput>} & {
cachedData: CachedData;
};

export function blockRequiresBlobs(config: ChainForkConfig, blockSlot: Slot, clockSlot: Slot): boolean {
return (
Expand Down Expand Up @@ -67,49 +98,43 @@ export const getBlockInput = {
postDeneb(
config: ChainForkConfig,
block: allForks.SignedBeaconBlock,
source: BlockSource,
blobs: deneb.BlobSidecars,
blockBytes: Uint8Array | null,
blobsBytes: (Uint8Array | null)[]
blockData: BlockInputData,
source: BlockSource
): BlockInput {
if (config.getForkSeq(block.message.slot) < ForkSeq.deneb) {
throw Error(`Pre Deneb block slot ${block.message.slot}`);
}
return {
type: BlockInputType.postDeneb,
block,
source,
blobs,
blockBytes,
blobsBytes,
blockData,
source,
};
},

blobsPromise(
config: ChainForkConfig,
block: allForks.SignedBeaconBlock,
source: BlockSource,
blobsCache: BlobsCache,
blockBytes: Uint8Array | null,
availabilityPromise: Promise<BlockInputBlobs>,
resolveAvailability: (blobs: BlockInputBlobs) => void
cachedData: CachedData,
source: BlockSource
): BlockInput {
if (config.getForkSeq(block.message.slot) < ForkSeq.deneb) {
throw Error(`Pre Deneb block slot ${block.message.slot}`);
}
return {
type: BlockInputType.blobsPromise,
block,
source,
blobsCache,
blockBytes,
availabilityPromise,
resolveAvailability,
source,
cachedData,
};
},
};

export function getBlockInputBlobs(blobsCache: BlobsCache): BlockInputBlobs {
export function getBlockInputBlobs(blobsCache: BlobsCache): BlobsData {
const blobs = [];
const blobsBytes = [];

Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/chain/blocks/verifyBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export async function verifyBlocksInEpoch(
] = await Promise.all([
// Execution payloads
opts.skipVerifyExecutionPayload !== true
? verifyBlocksExecutionPayload(this, parentBlock, blocks, preState0, abortController.signal, opts)
? verifyBlocksExecutionPayload(this, parentBlock, blocksInput, preState0, abortController.signal, opts)
: Promise.resolve({
execAborted: null,
executionStatuses: blocks.map((_blk) => ExecutionStatus.Syncing),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {computeTimeAtSlot, DataAvailableStatus} from "@lodestar/state-transition";
import {ChainForkConfig} from "@lodestar/config";
import {deneb, UintNum64} from "@lodestar/types";
import {ForkName} from "@lodestar/params";
import {Logger} from "@lodestar/utils";
import {BlockError, BlockErrorCode} from "../errors/index.js";
import {validateBlobSidecars} from "../validation/blobSidecar.js";
Expand Down Expand Up @@ -78,12 +79,21 @@ async function maybeValidateBlobs(
const {block} = blockInput;
const blockSlot = block.message.slot;

const blobsData =
blockInput.type === BlockInputType.postDeneb
? blockInput
: await raceWithCutoff(chain, blockInput, blockInput.availabilityPromise);
const {blobs} = blobsData;
let blockData;
if (blockInput.type === BlockInputType.postDeneb) {
blockData = blockInput.blockData;
} else {
const {cachedData} = blockInput;
// weird that typescript is getting confused doing the same thing but with
// differing promise types, need to separate the case out
if (cachedData.fork === ForkName.deneb) {
blockData = await raceWithCutoff(chain, blockInput, cachedData.availabilityPromise);
} else {
blockData = await raceWithCutoff(chain, blockInput, cachedData.availabilityPromise);
}
}

const {blobs} = blockData;
const {blobKzgCommitments} = (block as deneb.SignedBeaconBlock).message.body;
const beaconBlockRoot = chain.config.getForkTypes(blockSlot).BeaconBlock.hashTreeRoot(block.message);

Expand Down
Loading