Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
dapplion committed Nov 15, 2022
1 parent 611a609 commit ffb6336
Show file tree
Hide file tree
Showing 24 changed files with 349 additions and 119 deletions.
15 changes: 0 additions & 15 deletions packages/api/src/utils/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import {isBasicType, ListBasicType, Type, isCompositeType, ListCompositeType, Ar
import {ForkName} from "@lodestar/params";
import {IChainForkConfig} from "@lodestar/config";
import {objectToExpectedCase} from "@lodestar/utils";
import {ssz} from "@lodestar/types";
import {Blobs} from "@lodestar/types/eip4844";
import {Schema, SchemaDefinition} from "./schema.js";

// See /packages/api/src/routes/index.ts for reasoning
Expand Down Expand Up @@ -196,19 +194,6 @@ export function WithExecutionOptimistic<T extends {data: unknown}>(
};
}

export function WithBlobs<T extends {data: unknown}>(type: TypeJson<T>): TypeJson<T & {blobs: Blobs}> {
return {
toJson: ({blobs, ...data}) => ({
...(type.toJson((data as unknown) as T) as Record<string, unknown>),
blobs: ssz.eip4844.Blobs.toJson(blobs),
}),
fromJson: ({blobs, ...data}: T & {blobs: Blobs}) => ({
...type.fromJson(data),
blobs: ssz.eip4844.Blobs.fromJson(blobs),
}),
};
}

type JsonCase = "snake" | "constant" | "camel" | "param" | "header" | "pascal" | "dot" | "notransform";

/** Helper to only translate casing */
Expand Down
1 change: 1 addition & 0 deletions packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
"@multiformats/multiaddr": "^11.0.0",
"@types/datastore-level": "^3.0.0",
"buffer-xor": "^2.0.2",
"c-kzg": "^1.0.0",
"cross-fetch": "^3.1.4",
"datastore-core": "^8.0.1",
"datastore-level": "^9.0.1",
Expand Down
39 changes: 19 additions & 20 deletions packages/beacon-node/src/api/impl/beacon/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {computeTimeAtSlot} from "@lodestar/state-transition";
import {SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params";
import {sleep} from "@lodestar/utils";
import {fromHexString, toHexString} from "@chainsafe/ssz";
import {promiseAllMaybeAsync} from "../../../../util/promises.js";
import {BlockError, BlockErrorCode} from "../../../../chain/errors/index.js";
import {OpSource} from "../../../../metrics/validatorMonitor.js";
import {NetworkEvent} from "../../../../network/index.js";
Expand All @@ -23,16 +24,6 @@ export function getBeaconBlockApi({
network,
db,
}: Pick<ApiModules, "chain" | "config" | "metrics" | "network" | "db">): routes.beacon.block.Api {
const waitForSlot = async (slot: number): Promise<void> => {
// Simple implementation of a pending block queue. Keeping the block here recycles the API logic, and keeps the
// REST request promise without any extra infrastructure.
const msToBlockSlot = computeTimeAtSlot(config, slot, chain.genesisTime) * 1000 - Date.now();
if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
// If block is a bit early, hold it in a promise. Equivalent to a pending queue.
await sleep(msToBlockSlot);
}
};

return {
async getBlockHeaders(filters) {
// TODO - SLOW CODE: This code seems like it could be improved
Expand Down Expand Up @@ -183,23 +174,31 @@ export function getBeaconBlockApi({

async publishBlock(signedBlock) {
const seenTimestampSec = Date.now() / 1000;
await waitForSlot(signedBlock.message.slot);

// Simple implementation of a pending block queue. Keeping the block here recycles the API logic, and keeps the
// REST request promise without any extra infrastructure.
const msToBlockSlot = computeTimeAtSlot(config, signedBlock.message.slot, chain.genesisTime) * 1000 - Date.now();
if (msToBlockSlot <= MAX_API_CLOCK_DISPARITY_MS && msToBlockSlot > 0) {
// If block is a bit early, hold it in a promise. Equivalent to a pending queue.
await sleep(msToBlockSlot);
}

// TODO: Validate block

metrics?.registerBeaconBlock(OpSource.api, seenTimestampSec, signedBlock.message);

await Promise.all([
await promiseAllMaybeAsync([
// Send the block, regardless of whether or not it is valid. The API
// specification is very clear that this is the desired behaviour.
network.gossip.publishBeaconBlock(signedBlock),

chain.processBlock(signedBlock).catch((e) => {
if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) {
network.events.emit(NetworkEvent.unknownBlockParent, signedBlock, network.peerId.toString());
}
throw e;
}),
() => network.publishBeaconBlockMaybeBlobs(signedBlock),

() =>
chain.processBlock(signedBlock).catch((e) => {
if (e instanceof BlockError && e.type.code === BlockErrorCode.PARENT_UNKNOWN) {
network.events.emit(NetworkEvent.unknownBlockParent, signedBlock, network.peerId.toString());
}
throw e;
}),
]);
},
};
Expand Down
15 changes: 9 additions & 6 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class BeaconChain implements IBeaconChain {
private successfulExchangeTransition = false;
private readonly exchangeTransitionConfigurationEverySlots: number;

/** Map keyed by executionPayload.blockHash of the block for those blobs */
private readonly producedBlobsCache = new Map<RootHex, eip4844.Blobs>();

private readonly faultInspectionWindow: number;
Expand Down Expand Up @@ -383,7 +384,7 @@ export class BeaconChain implements IBeaconChain {

// Cache for latter broadcasting
if (blobs.type === BlobsResultType.produced) {
this.producedBlobsCache.set(blobs.blockRoot, blobs.blobs);
this.producedBlobsCache.set(blobs.blockHash, blobs.blobs);
}

return block;
Expand All @@ -399,15 +400,17 @@ export class BeaconChain implements IBeaconChain {
* kzg_aggregated_proof=compute_proof_from_blobs(blobs),
* )
*/
getBlobsSidecar(beaconBlockRoot: RootHex): eip4844.BlobsSidecar {
const blobs = this.producedBlobsCache.get(beaconBlockRoot);
getBlobsSidecar(beaconBlock: eip4844.BeaconBlock): eip4844.BlobsSidecar {
const blockHash = toHex(beaconBlock.body.executionPayload.blockHash);
const blobs = this.producedBlobsCache.get(blockHash);
if (!blobs) {
throw Error(`No blobs for beaconBlockRoot ${beaconBlockRoot}`);
throw Error(`No blobs for beaconBlockRoot ${blockHash}`);
}

return {
beaconBlockRoot: beaconBlockRoot,
beaconBlockSlot: blobs.slot,
// TODO EIP-4844: Optimize, hashing the full block is not free.
beaconBlockRoot: this.config.getForkTypes(beaconBlock.slot).BeaconBlock.hashTreeRoot(beaconBlock),
beaconBlockSlot: beaconBlock.slot,
blobs: blobs,
kzgAggregatedProof: computeAggregateKzgProof(blobs),
};
Expand Down
23 changes: 23 additions & 0 deletions packages/beacon-node/src/chain/errors/blobsSidecarError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Slot} from "@lodestar/types";
import {GossipActionError} from "./gossipValidation.js";

export enum BlobsSidecarErrorCode {
/** !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) */
INVALID_KZG_TXS = "BLOBS_SIDECAR_ERROR_INVALID_KZG_TXS",
/** sidecar.beacon_block_slot != block.slot */
INCORRECT_SLOT = "BLOBS_SIDECAR_ERROR_INCORRECT_SLOT",
/** BLSFieldElement in valid range (x < BLS_MODULUS) */
INVALID_BLOB = "BLOBS_SIDECAR_ERROR_INVALID_BLOB",
/** !bls.KeyValidate(blobs_sidecar.kzg_aggregated_proof) */
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_KZG_PROOF};

export class BlobsSidecarError extends GossipActionError<BlobsSidecarErrorType> {}
2 changes: 2 additions & 0 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export interface IBeaconChain {
produceBlock(blockAttributes: BlockAttributes): Promise<allForks.BeaconBlock>;
produceBlindedBlock(blockAttributes: BlockAttributes): Promise<allForks.BlindedBeaconBlock>;

getBlobsSidecar(beaconBlock: eip4844.BeaconBlock): eip4844.BlobsSidecar;

/** Process a block until complete */
processBlock(block: allForks.SignedBeaconBlock, opts?: ImportBlockOpts): Promise<void>;
/** Process a chain of blocks until complete */
Expand Down
27 changes: 19 additions & 8 deletions packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {blobToKzgCommitment} from "c-kzg";
import {
Bytes32,
phase0,
Expand All @@ -22,6 +23,7 @@ import {
getRandaoMix,
getCurrentEpoch,
isMergeTransitionComplete,
verifyKzgCommitmentsAgainstTransactions,
} from "@lodestar/state-transition";
import {IChainForkConfig} from "@lodestar/config";
import {ForkName, ForkSeq} from "@lodestar/params";
Expand Down Expand Up @@ -64,7 +66,9 @@ export enum BlobsResultType {
produced,
}

export type BlobsResult = {type: BlobsResultType.preEIP4844} | {type: BlobsResultType.produced; blobs: eip4844.Blobs};
export type BlobsResult =
| {type: BlobsResultType.preEIP4844}
| {type: BlobsResultType.produced; blobs: eip4844.Blobs; blockHash: RootHex};

export async function produceBlockBody<T extends BlockType>(
this: BeaconChain,
Expand Down Expand Up @@ -102,7 +106,7 @@ export async function produceBlockBody<T extends BlockType>(
const {eth1Data, deposits} = await this.eth1.getEth1DataAndDeposits(currentState);

// We assign this in an EIP-4844 branch below and return it
let blobs: eip4844.Blobs | null = null;
let blobs: {blobs: eip4844.Blobs; blockHash: RootHex} | null = null;

const blockBody: phase0.BeaconBlockBody = {
randaoReveal,
Expand Down Expand Up @@ -157,11 +161,12 @@ export async function produceBlockBody<T extends BlockType>(
// For MeV boost integration, this is where the execution header will be
// fetched from the payload id and a blinded block will be produced instead of
// fullblock for the validator to sign
(blockBody as allForks.BlindedBeaconBlockBody).executionPayloadHeader = await prepareExecutionPayloadHeader(
const executionPayloadHeader = await prepareExecutionPayloadHeader(
this,
currentState as CachedBeaconStateBellatrix,
proposerPubKey
);
(blockBody as allForks.BlindedBeaconBlockBody).executionPayloadHeader = executionPayloadHeader;

// Capella and later forks have withdrawalRoot on their ExecutionPayloadHeader
// TODO Capella: Remove this. It will come from the execution client.
Expand All @@ -174,7 +179,7 @@ export async function produceBlockBody<T extends BlockType>(
if (forkName === ForkName.eip4844) {
// Empty blobs for now
(blockBody as eip4844.BeaconBlockBody).blobKzgCommitments = [];
blobs = [];
blobs = {blobs: [], blockHash: executionPayloadHeader.blockHash};
}
}

Expand Down Expand Up @@ -224,9 +229,15 @@ export async function produceBlockBody<T extends BlockType>(
// payload_id to retrieve blobs and blob_kzg_commitments via get_blobs_and_kzg_commitments(payload_id)
const blobsBundle = await this.executionEngine.getBlobsBundle(payloadId);

// Sanity check consistency between getPayload() and getBlobsBundle()
const blockHash = toHex(payload.blockHash);
if (blobsBundle.blockHash !== blockHash) {
throw Error(`blobsBundle incorrect blockHash ${blobsBundle.blockHash} != ${blockHash}`);
}

if (this.opts.sanityCheckExecutionEngineBlocks) {
// Optionally sanity-check that the KZG commitments match the versioned hashes in the transactions
verify_kzg_commitments_against_transactions(payload.transactions, blobsBundle.kzgs);
verifyKzgCommitmentsAgainstTransactions(payload.transactions, blobsBundle.kzgs);

// Optionally sanity-check that the KZG commitments match the blobs (as produced by the execution engine)
if (blobsBundle.blobs.length !== blobsBundle.kzgs.length) {
Expand All @@ -236,15 +247,15 @@ export async function produceBlockBody<T extends BlockType>(
}

for (let i = 0; i < blobsBundle.blobs.length; i++) {
const kzg = blob_to_kzg_commitment(blobsBundle.blobs[i]) as eip4844.KZGCommitment;
const kzg = blobToKzgCommitment(blobsBundle.blobs[i]) as eip4844.KZGCommitment;
if (!byteArrayEquals(kzg, blobsBundle.kzgs[i])) {
throw Error(`Wrong KZG[${i}] ${toHex(blobsBundle.kzgs[i])} expected ${toHex(kzg)}`);
}
}
}

(blockBody as eip4844.BeaconBlockBody).blobKzgCommitments = blobsBundle.kzgs;
blobs = blobsBundle.blobs;
blobs = {blobs: blobsBundle.blobs, blockHash};
}

const fetchedTime = Date.now() / 1000 - computeTimeAtSlot(this.config, blockSlot, this.genesisTime);
Expand Down Expand Up @@ -284,7 +295,7 @@ export async function produceBlockBody<T extends BlockType>(
if (!blobs) {
throw Error("Blobs are null post eip4844");
}
blobsResult = {type: BlobsResultType.produced, blobs};
blobsResult = {type: BlobsResultType.produced, ...blobs};
} else {
blobsResult = {type: BlobsResultType.preEIP4844};
}
Expand Down
68 changes: 68 additions & 0 deletions packages/beacon-node/src/chain/validation/blobsSidecar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {IChainForkConfig} from "@lodestar/config";
import {eip4844} from "@lodestar/types";
import {verifyKzgCommitmentsAgainstTransactions} from "@lodestar/state-transition";
import {IBeaconChain} from "../interface.js";
import {BlobsSidecarError, BlobsSidecarErrorCode} from "../errors/blobsSidecarError.js";
import {GossipAction} from "../errors/gossipValidation.js";

export async function validateGossipBlobsSidecar(
config: IChainForkConfig,
chain: IBeaconChain,
signedBlock: eip4844.SignedBeaconBlock,
blobsSidecar: eip4844.BlobsSidecar
): Promise<void> {
const block = signedBlock.message;

// Spec: https://github.com/ethereum/consensus-specs/blob/4cb6fd1c8c8f190d147d15b182c2510d0423ec61/specs/eip4844/p2p-interface.md#beacon_block_and_blobs_sidecar
// [REJECT] The KZG commitments of the blobs are all correctly encoded compressed BLS G1 Points.
// -- i.e. all(bls.KeyValidate(commitment) for commitment in block.body.blob_kzg_commitments)
const {blobKzgCommitments} = block.body;
for (let i = 0; i < blobKzgCommitments.length; i++) {
if (!bls.keyValidate(blobKzgCommitments[i])) {
throw new BlobsSidecarError(GossipAction.REJECT, {code: BlobsSidecarErrorCode.INVALID_KZG, kzgIdx: i});
}
}

// [REJECT] The KZG commitments correspond to the versioned hashes in the transactions list.
// -- i.e. verify_kzg_commitments_against_transactions(block.body.execution_payload.transactions, block.body.blob_kzg_commitments)
if (
!verifyKzgCommitmentsAgainstTransactions(block.body.executionPayload.transactions, block.body.blobKzgCommitments)
) {
throw new BlobsSidecarError(GossipAction.REJECT, {code: BlobsSidecarErrorCode.INVALID_KZG_TXS});
}

// [IGNORE] the sidecar.beacon_block_slot is for the current slot (with a MAXIMUM_GOSSIP_CLOCK_DISPARITY allowance)
// -- i.e. sidecar.beacon_block_slot == block.slot.
if (blobsSidecar.beaconBlockSlot !== block.slot) {
throw new BlobsSidecarError(GossipAction.IGNORE, {
code: BlobsSidecarErrorCode.INCORRECT_SLOT,
blobSlot: blobsSidecar.beaconBlockSlot,
blockSlot: block.slot,
});
}

// [REJECT] the sidecar.blobs are all well formatted, i.e. the BLSFieldElement in valid range (x < BLS_MODULUS).
TODO;

// [REJECT] The KZG proof is a correctly encoded compressed BLS G1 Point
// -- i.e. bls.KeyValidate(blobs_sidecar.kzg_aggregated_proof)
if (!bls.KeyValidate(blobsSidecar.kzgAggregatedProof)) {
throw new BlobsSidecarError(GossipAction.REJECT, {code: BlobsSidecarErrorCode.INVALID_KZG_PROOF});
}
}

type Result<T> = {ok: true; result: T} | {ok: false; error: Error};

function rustOk(): Result<string>;

function Ok<T>(result: T): Result<T> {
return {ok: true, result};
}

function okUser(): Result<number> {
const res = rustOk();
if (!res.ok) return res;
const resStr = res.result;

return Ok(parseInt(resStr));
}
2 changes: 1 addition & 1 deletion packages/beacon-node/src/execution/engine/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ export function parseExecutionPayload(data: ExecutionPayloadRpc): allForks.Execu

export function parseBlobsBundle(data: BlobsBundleRpc): BlobsBundle {
return {
blockHash: dataToBytes(data.blockHash),
blockHash: data.blockHash,
kzgs: data.kzgs.map((kzg) => dataToBytes(kzg)),
blobs: data.blobs.map((blob) => dataToBytes(blob)),
};
Expand Down
6 changes: 5 additions & 1 deletion packages/beacon-node/src/execution/engine/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,11 @@ export type TransitionConfigurationV1 = {
};

export type BlobsBundle = {
blockHash: Uint8Array;
/**
* Execution payload `blockHash` for the caller to sanity-check the consistency with the `engine_getPayload` call
* https://github.com/protolambda/execution-apis/blob/bf44a8d08ab34b861ef97fa9ef5c5e7806194547/src/engine/blob-extension.md?plain=1#L49
*/
blockHash: RootHex;
kzgs: KZGCommitment[];
blobs: Blob[];
};
Expand Down
Loading

0 comments on commit ffb6336

Please sign in to comment.