UNPKG

@lodestar/beacon-node

Version:

A Typescript implementation of the beacon chain

325 lines 11.7 kB
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