@lodestar/beacon-node
Version:
A Typescript implementation of the beacon chain
325 lines • 11.7 kB
JavaScript
import { NUMBER_OF_COLUMNS } from "@lodestar/params";
import { toRootHex, withTimeout } from "@lodestar/utils";
import { kzgCommitmentToVersionedHash } from "../../../util/blobs.js";
function createPromise() {
let resolve;
let reject;
const promise = new Promise((_resolve, _reject) => {
resolve = _resolve;
reject = _reject;
});
return { promise, resolve, reject };
}
/**
* Tracks bid + payload envelope + data columns for a Gloas block.
*
* Created during block import from signedExecutionPayloadBid in block body.
* Always has bid (required for creation).
*
* Completion requires: payload envelope + all sampled columns
*/
export class PayloadEnvelopeInput {
blockRootHex;
slot;
forkName;
proposerIndex;
bid;
versionedHashes;
daOutOfRange;
columnsCache = new Map();
sampledColumns;
custodyColumns;
timeCreatedSec;
payloadEnvelopeDataPromise;
allDataPromise;
columnsDataPromise;
state;
constructor(props) {
this.blockRootHex = props.blockRootHex;
this.slot = props.slot;
this.forkName = props.forkName;
this.proposerIndex = props.proposerIndex;
this.bid = props.bid;
this.versionedHashes = props.bid.blobKzgCommitments.map(kzgCommitmentToVersionedHash);
this.sampledColumns = props.sampledColumns;
this.custodyColumns = props.custodyColumns;
this.timeCreatedSec = props.timeCreatedSec;
this.daOutOfRange = props.daOutOfRange;
this.payloadEnvelopeDataPromise = createPromise();
this.allDataPromise = createPromise();
this.columnsDataPromise = createPromise();
const noBlobs = props.bid.blobKzgCommitments.length === 0;
const noSampledColumns = props.sampledColumns.length === 0;
const hasAllData = props.daOutOfRange || noBlobs || noSampledColumns;
if (hasAllData) {
this.state = { hasPayload: false, hasAllData: true, hasComputedAllData: true };
this.allDataPromise.resolve(this.getSampledColumns());
this.columnsDataPromise.resolve(this.getSampledColumns());
}
else {
this.state = { hasPayload: false, hasAllData: false, hasComputedAllData: false };
}
}
static createFromBlock(props) {
const bid = props.block.message.body.signedExecutionPayloadBid.message;
return new PayloadEnvelopeInput({
blockRootHex: props.blockRootHex,
slot: props.block.message.slot,
forkName: props.forkName,
proposerIndex: props.block.message.proposerIndex,
bid,
sampledColumns: props.sampledColumns,
custodyColumns: props.custodyColumns,
timeCreatedSec: props.timeCreatedSec,
daOutOfRange: props.daOutOfRange,
});
}
/**
* Create a `PayloadEnvelopeInput` from a state's `latestExecutionPayloadBid` (the bid
* recorded in beacon state for the latest imported block). Used when seeding the cache
* for a checkpoint anchor block — we have the bid via state but not the full
* SignedBeaconBlock body.
*/
static createFromBid(props) {
return new PayloadEnvelopeInput({
blockRootHex: props.blockRootHex,
slot: props.slot,
forkName: props.forkName,
proposerIndex: props.proposerIndex,
bid: props.bid,
sampledColumns: props.sampledColumns,
custodyColumns: props.custodyColumns,
timeCreatedSec: props.timeCreatedSec,
daOutOfRange: props.daOutOfRange,
});
}
getBid() {
return this.bid;
}
getBuilderIndex() {
return this.bid.builderIndex;
}
getBlockHashHex() {
return toRootHex(this.bid.blockHash);
}
getBlobKzgCommitments() {
return this.bid.blobKzgCommitments;
}
addPayloadEnvelope(props) {
if (this.state.hasPayload) {
throw new Error(`Payload envelope already set for block ${this.blockRootHex}`);
}
if (toRootHex(props.envelope.message.beaconBlockRoot) !== this.blockRootHex) {
throw new Error("Payload envelope beacon_block_root mismatch");
}
// TODO GLOAS: track source by metrics, maybe inside the seen cache
const source = {
source: props.source,
seenTimestampSec: props.seenTimestampSec,
peerIdStr: props.peerIdStr,
};
if (this.state.hasAllData) {
// Complete state
this.state = {
hasPayload: true,
hasAllData: true,
hasComputedAllData: this.state.hasComputedAllData,
payloadEnvelope: props.envelope,
payloadEnvelopeSource: source,
timeCompleteSec: props.seenTimestampSec,
};
this.payloadEnvelopeDataPromise.resolve(props.envelope);
}
else {
// Has payload, waiting for columns
this.state = {
hasPayload: true,
hasAllData: false,
hasComputedAllData: false,
payloadEnvelope: props.envelope,
payloadEnvelopeSource: source,
};
}
}
addColumn(columnWithSource) {
const { columnSidecar, seenTimestampSec } = columnWithSource;
if (this.columnsCache.has(columnSidecar.index)) {
return false;
}
this.columnsCache.set(columnSidecar.index, columnWithSource);
const sampledColumns = this.getSampledColumns();
const hasAllData =
// already hasAllData
this.state.hasAllData ||
// has all sampled columns
sampledColumns.length === this.sampledColumns.length ||
// has enough columns to reconstruct the rest
this.columnsCache.size >= NUMBER_OF_COLUMNS / 2;
const hasComputedAllData =
// has all sampled columns
sampledColumns.length === this.sampledColumns.length;
if (!hasAllData) {
return true;
}
// Resolve allDataPromise on the first transition to hasAllData (either sampled-complete or
// reconstruction-threshold branch). Guarded so it fires exactly once.
if (!this.state.hasAllData && hasAllData) {
this.allDataPromise.resolve(sampledColumns);
}
if (hasComputedAllData) {
this.columnsDataPromise.resolve(sampledColumns);
}
if (this.state.hasPayload) {
// Complete state
this.state = {
hasPayload: true,
hasAllData: true,
hasComputedAllData: hasComputedAllData || this.state.hasComputedAllData,
payloadEnvelope: this.state.payloadEnvelope,
payloadEnvelopeSource: this.state.payloadEnvelopeSource,
timeCompleteSec: seenTimestampSec,
};
this.payloadEnvelopeDataPromise.resolve(this.state.payloadEnvelope);
}
else {
// No payload yet, all data ready
this.state = {
hasPayload: false,
hasAllData: true,
hasComputedAllData: hasComputedAllData || this.state.hasComputedAllData,
};
}
return true;
}
hasColumn(index) {
return this.columnsCache.has(index);
}
getColumn(index) {
return this.columnsCache.get(index)?.columnSidecar;
}
getAllColumns() {
return [...this.columnsCache.values()].map(({ columnSidecar }) => columnSidecar);
}
getVersionedHashes() {
return this.versionedHashes;
}
hasPayloadEnvelope() {
return this.state.hasPayload;
}
getPayloadEnvelope() {
if (!this.state.hasPayload)
throw new Error("Payload envelope not set");
return this.state.payloadEnvelope;
}
getPayloadEnvelopeSource() {
if (!this.state.hasPayload)
throw new Error("Payload envelope source not set");
return this.state.payloadEnvelopeSource;
}
getSampledColumns() {
const columns = [];
for (const index of this.sampledColumns) {
const column = this.columnsCache.get(index);
if (column) {
columns.push(column.columnSidecar);
}
}
return columns;
}
getSampledColumnsWithSource() {
const columns = [];
for (const index of this.sampledColumns) {
const column = this.columnsCache.get(index);
if (column) {
columns.push(column);
}
}
return columns;
}
getCustodyColumns() {
const columns = [];
for (const index of this.custodyColumns) {
const column = this.columnsCache.get(index);
if (column) {
columns.push(column.columnSidecar);
}
}
return columns;
}
hasAllData() {
return this.state.hasAllData;
}
/**
* Strictly checks missing sampled columns. Does NOT short-circuit on `state.hasAllData`.
*/
getMissingSampledColumnMeta() {
if (this.state.hasComputedAllData) {
return { missing: [], versionedHashes: this.versionedHashes };
}
const missing = [];
for (const index of this.sampledColumns) {
if (!this.columnsCache.has(index)) {
missing.push(index);
}
}
return { missing, versionedHashes: this.versionedHashes };
}
hasComputedAllData() {
return this.state.hasComputedAllData;
}
waitForAllData(timeout, signal) {
if (this.state.hasAllData) {
return Promise.resolve(this.getSampledColumns());
}
return withTimeout(() => this.allDataPromise.promise, timeout, signal);
}
async waitForEnvelopeAndAllData(timeout, signal) {
if (!this.state.hasPayload || !this.state.hasAllData) {
await withTimeout(() => Promise.all([this.payloadEnvelopeDataPromise.promise, this.allDataPromise.promise]), timeout, signal);
}
return this;
}
waitForComputedAllData(timeout, signal) {
if (this.state.hasComputedAllData) {
return Promise.resolve(this.getSampledColumns());
}
return withTimeout(() => this.columnsDataPromise.promise, timeout, signal);
}
getTimeCreated() {
return this.timeCreatedSec;
}
getTimeComplete() {
if (!this.state.hasPayload || !this.state.hasAllData)
throw new Error("Not yet complete");
return this.state.timeCompleteSec;
}
isComplete() {
return this.state.hasPayload && this.state.hasAllData;
}
async waitForData() {
return this.payloadEnvelopeDataPromise.promise;
}
getSerializedCacheKeys() {
const objects = [];
if (this.state.hasPayload) {
objects.push(this.state.payloadEnvelope);
}
for (const { columnSidecar } of this.columnsCache.values()) {
objects.push(columnSidecar);
}
return objects;
}
getLogMeta() {
return {
slot: this.slot,
blockRoot: this.blockRootHex,
hasPayload: this.state.hasPayload,
hasAllData: this.state.hasAllData,
hasComputedAllData: this.state.hasComputedAllData,
isComplete: this.isComplete(),
receivedColumns: this.columnsCache.size,
sampledColumnsCount: this.sampledColumns.length,
};
}
}
//# sourceMappingURL=payloadEnvelopeInput.js.map