diff --git a/.wordlist.txt b/.wordlist.txt index 5700afd898ab..248f3fe20b27 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -169,6 +169,7 @@ flamegraphs floodsub fsSL getNetworkIdentity +gloas gnosis gpg heapdump diff --git a/package.json b/package.json index a807b5adc9e8..ae15144a4f95 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@chainsafe/benchmark": "^2.0.2", "@chainsafe/biomejs-config": "^1.0.0", "@chainsafe/caxa": "3.0.6", + "@chainsafe/ssz": "^1.6.2", "@lerna-lite/cli": "^4.9.4", "@lerna-lite/exec": "^4.9.4", "@lerna-lite/publish": "^4.9.4", diff --git a/packages/api/package.json b/packages/api/package.json index 0e866026a548..0b51f36b85f0 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -74,8 +74,8 @@ "check-readme": "pnpm exec ts-node ../../scripts/check_readme.ts" }, "dependencies": { - "@chainsafe/persistent-merkle-tree": "^1.2.5", - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/persistent-merkle-tree": "^1.3.0", + "@chainsafe/ssz": "^1.6.2", "@lodestar/config": "workspace:^", "@lodestar/params": "workspace:^", "@lodestar/types": "workspace:^", diff --git a/packages/api/src/beacon/routes/beacon/block.ts b/packages/api/src/beacon/routes/beacon/block.ts index c038747d4e4e..55537e342d96 100644 --- a/packages/api/src/beacon/routes/beacon/block.ts +++ b/packages/api/src/beacon/routes/beacon/block.ts @@ -17,6 +17,7 @@ import { SignedBlockContents, Slot, deneb, + fulu, gloas, ssz, sszTypesFor, @@ -190,8 +191,13 @@ export type Endpoints = { */ publishExecutionPayloadEnvelope: Endpoint< "POST", - {signedExecutionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope}, - {body: unknown; headers: {[MetaHeader.Version]: string}}, + { + signedExecutionPayloadEnvelope: gloas.SignedExecutionPayloadEnvelope; + blobs?: deneb.Blobs; + kzgProofs?: fulu.KZGProofs; + broadcastValidation?: BroadcastValidation; + }, + {body: unknown; headers: {[MetaHeader.Version]: string}; query: {broadcast_validation?: string}}, EmptyResponseData, EmptyMeta >; @@ -453,39 +459,98 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions { + writeReqJson: ({signedExecutionPayloadEnvelope, blobs, kzgProofs, broadcastValidation}) => { const fork = config.getForkName(signedExecutionPayloadEnvelope.message.payload.slotNumber); + const types = getPostGloasForkTypes(fork); + if ((blobs === undefined) !== (kzgProofs === undefined)) { + throw Error("blobs and kzgProofs must both be supplied or both omitted"); + } + const hasBlobs = blobs !== undefined && kzgProofs !== undefined; + const body = hasBlobs + ? types.SignedExecutionPayloadEnvelopeContents.toJson({ + signedExecutionPayloadEnvelope, + kzgProofs, + blobs, + }) + : types.SignedExecutionPayloadEnvelope.toJson(signedExecutionPayloadEnvelope); return { - body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.toJson(signedExecutionPayloadEnvelope), + body, headers: { [MetaHeader.Version]: fork, }, + query: {broadcast_validation: broadcastValidation}, }; }, - parseReqJson: ({body, headers}) => { + parseReqJson: ({body, headers, query}) => { const fork = toForkName(fromHeaders(headers, MetaHeader.Version)); + const types = getPostGloasForkTypes(fork); + // Discriminate by the wrapper's top-level key. SignedExecutionPayloadEnvelopeContents + // serializes to {signed_execution_payload_envelope, kzg_proofs, blobs}; the bare envelope + // serializes to {message, signature}. + const isContents = body !== null && typeof body === "object" && "signed_execution_payload_envelope" in body; + if (isContents) { + const contents = types.SignedExecutionPayloadEnvelopeContents.fromJson(body); + return { + signedExecutionPayloadEnvelope: contents.signedExecutionPayloadEnvelope, + blobs: contents.blobs, + kzgProofs: contents.kzgProofs, + broadcastValidation: query.broadcast_validation as BroadcastValidation, + }; + } return { - signedExecutionPayloadEnvelope: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.fromJson(body), + signedExecutionPayloadEnvelope: types.SignedExecutionPayloadEnvelope.fromJson(body), + broadcastValidation: query.broadcast_validation as BroadcastValidation, }; }, - writeReqSsz: ({signedExecutionPayloadEnvelope}) => { + writeReqSsz: ({signedExecutionPayloadEnvelope, blobs, kzgProofs, broadcastValidation}) => { const fork = config.getForkName(signedExecutionPayloadEnvelope.message.payload.slotNumber); + const types = getPostGloasForkTypes(fork); + if ((blobs === undefined) !== (kzgProofs === undefined)) { + throw Error("blobs and kzgProofs must both be supplied or both omitted"); + } + const hasBlobs = blobs !== undefined && kzgProofs !== undefined; + const body = hasBlobs + ? types.SignedExecutionPayloadEnvelopeContents.serialize({ + signedExecutionPayloadEnvelope, + kzgProofs, + blobs, + }) + : types.SignedExecutionPayloadEnvelope.serialize(signedExecutionPayloadEnvelope); return { - body: getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.serialize(signedExecutionPayloadEnvelope), + body, headers: { [MetaHeader.Version]: fork, }, + query: {broadcast_validation: broadcastValidation}, }; }, - parseReqSsz: ({body, headers}) => { + parseReqSsz: ({body, headers, query}) => { const fork = toForkName(fromHeaders(headers, MetaHeader.Version)); + const types = getPostGloasForkTypes(fork); + // SSZ has no in-band Contents-vs-envelope discriminator; spec uses a single + // `application/octet-stream` Content-Type for both shapes. Discriminate by the + // first SSZ offset: SignedExecutionPayloadEnvelopeContents has 3 variable fields + // so its first offset is 12; SignedExecutionPayloadEnvelope's variable `message` + // sits after a 96-byte signature plus the 4-byte offset header, so its first + // offset is 100. + const firstOffset = body.length >= 4 ? body[0] | (body[1] << 8) | (body[2] << 16) | (body[3] << 24) : 0; + if (firstOffset === 12) { + const contents = types.SignedExecutionPayloadEnvelopeContents.deserialize(body); + return { + signedExecutionPayloadEnvelope: contents.signedExecutionPayloadEnvelope, + blobs: contents.blobs, + kzgProofs: contents.kzgProofs, + broadcastValidation: query.broadcast_validation as BroadcastValidation, + }; + } return { - signedExecutionPayloadEnvelope: - getPostGloasForkTypes(fork).SignedExecutionPayloadEnvelope.deserialize(body), + signedExecutionPayloadEnvelope: types.SignedExecutionPayloadEnvelope.deserialize(body), + broadcastValidation: query.broadcast_validation as BroadcastValidation, }; }, schema: { body: Schema.Object, + query: {broadcast_validation: Schema.String}, headers: {[MetaHeader.Version]: Schema.String}, }, }, diff --git a/packages/api/test/unit/beacon/publishExecutionPayloadEnvelope.test.ts b/packages/api/test/unit/beacon/publishExecutionPayloadEnvelope.test.ts new file mode 100644 index 000000000000..f742a8d74915 --- /dev/null +++ b/packages/api/test/unit/beacon/publishExecutionPayloadEnvelope.test.ts @@ -0,0 +1,123 @@ +import {describe, expect, it} from "vitest"; +import {createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {ssz} from "@lodestar/types"; +import {BroadcastValidation, getDefinitions} from "../../../src/beacon/routes/beacon/block.js"; +import {WireFormat} from "../../../src/utils/wireFormat.js"; + +function lowercaseKeys>(headers: T): T { + return Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])) as T; +} + +describe("publishExecutionPayloadEnvelope route", () => { + const config = createChainForkConfig({...defaultChainConfig, GLOAS_FORK_EPOCH: 0}); + const definitions = getDefinitions(config); + const route = definitions.publishExecutionPayloadEnvelope; + + const signedExecutionPayloadEnvelope = ssz.gloas.SignedExecutionPayloadEnvelope.defaultValue(); + + describe("JSON wire format", () => { + it("round-trips a bare SignedExecutionPayloadEnvelope", () => { + const written = route.req.writeReqJson({signedExecutionPayloadEnvelope}); + expect(written.body).toHaveProperty("message"); + expect(written.body).toHaveProperty("signature"); + expect(written.body).not.toHaveProperty("signed_execution_payload_envelope"); + + const parsed = route.req.parseReqJson({ + body: written.body, + headers: lowercaseKeys(written.headers), + query: written.query ?? {}, + }); + expect(parsed.signedExecutionPayloadEnvelope).toEqual(signedExecutionPayloadEnvelope); + expect(parsed.blobs).toBeUndefined(); + expect(parsed.kzgProofs).toBeUndefined(); + }); + + it("round-trips a SignedExecutionPayloadEnvelopeContents wrapper when blobs are supplied", () => { + const blobs = [ssz.deneb.Blob.defaultValue()]; + const kzgProofs = Array.from({length: 128}, () => ssz.deneb.KZGProof.defaultValue()); + + const written = route.req.writeReqJson({ + signedExecutionPayloadEnvelope, + blobs, + kzgProofs, + broadcastValidation: BroadcastValidation.consensus, + }); + expect(written.body).toHaveProperty("signed_execution_payload_envelope"); + expect(written.body).toHaveProperty("kzg_proofs"); + expect(written.body).toHaveProperty("blobs"); + expect(written.query?.broadcast_validation).toBe(BroadcastValidation.consensus); + + const parsed = route.req.parseReqJson({ + body: written.body, + headers: lowercaseKeys(written.headers), + query: written.query ?? {}, + }); + expect(parsed.signedExecutionPayloadEnvelope).toEqual(signedExecutionPayloadEnvelope); + expect(parsed.blobs).toEqual(blobs); + expect(parsed.kzgProofs).toEqual(kzgProofs); + expect(parsed.broadcastValidation).toBe(BroadcastValidation.consensus); + }); + }); + + describe("SSZ wire format", () => { + it("round-trips a bare SignedExecutionPayloadEnvelope", () => { + const written = route.req.writeReqSsz({signedExecutionPayloadEnvelope}); + const parsed = route.req.parseReqSsz({ + body: written.body, + headers: lowercaseKeys(written.headers), + query: written.query ?? {}, + }); + expect(parsed.signedExecutionPayloadEnvelope).toEqual(signedExecutionPayloadEnvelope); + expect(parsed.blobs).toBeUndefined(); + expect(parsed.kzgProofs).toBeUndefined(); + }); + + it("round-trips a SignedExecutionPayloadEnvelopeContents wrapper", () => { + const blobs = [ssz.deneb.Blob.defaultValue()]; + const kzgProofs = Array.from({length: 128}, () => ssz.deneb.KZGProof.defaultValue()); + + const written = route.req.writeReqSsz({signedExecutionPayloadEnvelope, blobs, kzgProofs}); + const parsed = route.req.parseReqSsz({ + body: written.body, + headers: lowercaseKeys(written.headers), + query: written.query ?? {}, + }); + expect(parsed.signedExecutionPayloadEnvelope).toEqual(signedExecutionPayloadEnvelope); + expect(parsed.blobs).toEqual(blobs); + expect(parsed.kzgProofs).toEqual(kzgProofs); + }); + }); + + it("uses SSZ as the default request wire format", () => { + expect(route.init?.requestWireFormat).toBe(WireFormat.ssz); + }); + + describe("partial blobs/kzgProofs rejection", () => { + const blobs = [ssz.deneb.Blob.defaultValue()]; + const kzgProofs = Array.from({length: 128}, () => ssz.deneb.KZGProof.defaultValue()); + + it("writeReqJson throws when only blobs are supplied", () => { + expect(() => route.req.writeReqJson({signedExecutionPayloadEnvelope, blobs})).toThrow( + /blobs and kzgProofs must both be supplied/ + ); + }); + + it("writeReqJson throws when only kzgProofs are supplied", () => { + expect(() => route.req.writeReqJson({signedExecutionPayloadEnvelope, kzgProofs})).toThrow( + /blobs and kzgProofs must both be supplied/ + ); + }); + + it("writeReqSsz throws when only blobs are supplied", () => { + expect(() => route.req.writeReqSsz({signedExecutionPayloadEnvelope, blobs})).toThrow( + /blobs and kzgProofs must both be supplied/ + ); + }); + + it("writeReqSsz throws when only kzgProofs are supplied", () => { + expect(() => route.req.writeReqSsz({signedExecutionPayloadEnvelope, kzgProofs})).toThrow( + /blobs and kzgProofs must both be supplied/ + ); + }); + }); +}); diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index ff5cf52f6ebb..881c3ec40b54 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -114,10 +114,10 @@ "@chainsafe/enr": "^6.0.1", "@chainsafe/libp2p-noise": "^17.0.0", "@chainsafe/libp2p-quic": "^2.0.1", - "@chainsafe/persistent-merkle-tree": "^1.2.5", + "@chainsafe/persistent-merkle-tree": "^1.3.0", "@chainsafe/prometheus-gc-stats": "^1.0.0", "@chainsafe/snappy-wasm": "^0.5.0", - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@chainsafe/threads": "^1.11.3", "@crate-crypto/node-eth-kzg": "0.9.1", "@fastify/bearer-auth": "^10.0.1", diff --git a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts index 8a2c82192618..786f526e0de4 100644 --- a/packages/beacon-node/src/api/impl/beacon/blocks/index.ts +++ b/packages/beacon-node/src/api/impl/beacon/blocks/index.ts @@ -48,6 +48,7 @@ import { ProduceFullFulu, ProduceFullGloas, } from "../../../../chain/produceBlock/index.js"; +import {validateCellsAndKzgCommitments} from "../../../../chain/produceBlock/validateBlobsAndKzgCommitments.js"; import {validateGossipBlock} from "../../../../chain/validation/block.js"; import {validateApiExecutionPayloadBid} from "../../../../chain/validation/executionPayloadBid.js"; import {validateApiExecutionPayloadEnvelope} from "../../../../chain/validation/executionPayloadEnvelope.js"; @@ -651,7 +652,9 @@ export function getBeaconBlockApi({ publishBlockV2, publishBlindedBlockV2, - async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope}) { + async publishExecutionPayloadEnvelope({signedExecutionPayloadEnvelope, blobs, kzgProofs, broadcastValidation}) { + // TODO GLOAS: honor broadcastValidation (gossip|consensus|consensus_and_equivocation) per beacon-APIs PR #580 + void broadcastValidation; const seenTimestampSec = Date.now() / 1000; const envelope = signedExecutionPayloadEnvelope.message; const slot = envelope.payload.slotNumber; @@ -685,11 +688,52 @@ export function getBeaconBlockApi({ await validateApiExecutionPayloadEnvelope(chain, signedExecutionPayloadEnvelope); + const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); + if (!payloadInput) { + // The block is awaited above (queuing if the envelope arrived first), and both the API and + // gossip import paths seed the PayloadEnvelopeInput before importing the block, so the input + // should exist here. + throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); + } + + if ((blobs === undefined) !== (kzgProofs === undefined)) { + throw new ApiError(400, "blobs and kzgProofs must both be supplied or both omitted"); + } + const hasSuppliedBlobs = blobs !== undefined && kzgProofs !== undefined; const isSelfBuild = envelope.builderIndex === BUILDER_INDEX_SELF_BUILD; let dataColumnSidecars: gloas.DataColumnSidecar[] = []; - if (isSelfBuild) { - // For self-builds, construct and publish data column sidecars from cached block production data + if (hasSuppliedBlobs) { + const expectedBlobs = payloadInput.getBlobKzgCommitments().length; + if (blobs.length !== expectedBlobs) { + throw new ApiError(400, `Expected ${expectedBlobs} blobs to match bid kzg_commitments, got ${blobs.length}`); + } + if (kzgProofs.length !== blobs.length * NUMBER_OF_COLUMNS) { + throw new ApiError( + 400, + `Expected ${blobs.length * NUMBER_OF_COLUMNS} kzg_proofs for ${blobs.length} blobs, got ${kzgProofs.length}` + ); + } + if (blobs.length > 0) { + const timer = metrics?.peerDas.dataColumnSidecarComputationTime.startTimer(); + const cells = blobs.map((blob) => kzg.computeCells(blob)); + try { + await validateCellsAndKzgCommitments(payloadInput.getBlobKzgCommitments(), kzgProofs, cells); + } catch (e) { + throw new ApiError( + 400, + `Invalid supplied blobs/kzg_proofs against bid kzg_commitments: ${(e as Error).message}` + ); + } + const cellsAndProofs = cells.map((rowCells, rowIndex) => ({ + cells: rowCells, + proofs: kzgProofs.slice(rowIndex * NUMBER_OF_COLUMNS, (rowIndex + 1) * NUMBER_OF_COLUMNS), + })); + dataColumnSidecars = getGloasDataColumnSidecars(slot, envelope.beaconBlockRoot, cellsAndProofs); + timer?.(); + } + } else if (isSelfBuild) { + // Stateful self-build path: reconstruct data column sidecars from cached cells + proofs. const cachedResult = chain.blockProductionCache.get(blockRootHex) as ProduceFullGloas | undefined; if (cachedResult === undefined) { throw new ApiError(404, `No cached block production result found for block root ${blockRootHex}`); @@ -714,8 +758,6 @@ export function getBeaconBlockApi({ dataColumnSidecars = getGloasDataColumnSidecars(slot, envelope.beaconBlockRoot, cellsAndProofs); timer?.(); } - } else { - // TODO GLOAS: will this api be used by builders or only for self-building? } // If called near a slot boundary (e.g. late in slot N-1), hold briefly so gossip aligns with slot N. @@ -724,14 +766,6 @@ export function getBeaconBlockApi({ await sleep(msToBlockSlot); } - const payloadInput = chain.seenPayloadEnvelopeInputCache.get(blockRootHex); - if (!payloadInput) { - // The block is awaited above (queuing if the envelope arrived first), and both the API and - // gossip import paths seed the PayloadEnvelopeInput before importing the block, so the input - // should exist here. - throw new ApiError(404, `PayloadEnvelopeInput not found for block root ${blockRootHex}`); - } - payloadInput.addPayloadEnvelope({ envelope: signedExecutionPayloadEnvelope, source: PayloadEnvelopeInputSource.api, diff --git a/packages/beacon-node/src/api/impl/validator/index.ts b/packages/beacon-node/src/api/impl/validator/index.ts index 67cb38023c1e..f49dc5646b33 100644 --- a/packages/beacon-node/src/api/impl/validator/index.ts +++ b/packages/beacon-node/src/api/impl/validator/index.ts @@ -938,7 +938,10 @@ export function getValidatorApi( // support when it is implemented. const isBuildingOnFull = chain.forkChoice.shouldBuildOnFull(parentBlock, slot); const bidParentBlockHash = isBuildingOnFull ? parentBlock.executionPayloadBlockHash : parentBlock.parentBlockHash; - const builderBid = chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex); + const builderCircuitBreakerActive = chain.builderCircuitBreaker.isActive(slot); + const builderBid = builderCircuitBreakerActive + ? null + : chain.executionPayloadBidPool.getBestBid(slot, bidParentBlockHash, parentBlockRootHex); const logCtx = { slot, @@ -946,6 +949,7 @@ export function getValidatorApi( parentBlockRoot: parentBlockRootHex, parentBlockHash: parentBlock.executionPayloadBlockHash, fork, + builderCircuitBreakerActive, ...(builderBid !== null ? { bidValue: builderBid.message.value, diff --git a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts index ef4e2f56dabb..985faf5608b4 100644 --- a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts @@ -100,8 +100,8 @@ export function verifyExecutionPayloadEnvelope( } // Verify consistency with expected withdrawals - const payloadWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(payload.withdrawals); - const expectedWithdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot(state.payloadExpectedWithdrawals); + const payloadWithdrawalsRoot = ssz.gloas.Withdrawals.hashTreeRoot(payload.withdrawals); + const expectedWithdrawalsRoot = ssz.gloas.Withdrawals.hashTreeRoot(state.payloadExpectedWithdrawals); if (!byteArrayEquals(payloadWithdrawalsRoot, expectedWithdrawalsRoot)) { throw new Error( `Withdrawals mismatch between payload and expected payload=${toRootHex(payloadWithdrawalsRoot)} expected=${toRootHex(expectedWithdrawalsRoot)}` diff --git a/packages/beacon-node/src/chain/builderCircuitBreaker.ts b/packages/beacon-node/src/chain/builderCircuitBreaker.ts new file mode 100644 index 000000000000..474d9a3c2831 --- /dev/null +++ b/packages/beacon-node/src/chain/builderCircuitBreaker.ts @@ -0,0 +1,83 @@ +import {IForkChoice} from "@lodestar/fork-choice"; +import {Slot} from "@lodestar/types"; +import {Logger} from "@lodestar/utils"; +import {getFaultInspectionParams} from "../execution/builder/http.js"; +import {Metrics} from "../metrics/index.js"; + +export type BuilderCircuitBreakerOpts = { + faultInspectionWindow?: number; + allowedFaults?: number; +}; + +export type BuilderCircuitBreakerModules = { + forkChoice: IForkChoice; + logger: Logger; + metrics: Metrics | null; +}; + +/** + * Post-gloas circuit breaker for builder bids. The beacon block is produced by the proposer + * regardless of bid source, so missed blocks are not a useful builder health signal. Instead + * count blocks whose payload was never revealed and stop selecting builder bids while the + * non-reveal rate in the fault inspection window is too high. + */ +export class BuilderCircuitBreaker { + readonly faultInspectionWindow: number; + readonly allowedFaults: number; + + private active = false; + private lastUpdatedSlot = -1; + + constructor( + opts: BuilderCircuitBreakerOpts, + private readonly modules: BuilderCircuitBreakerModules + ) { + const {faultInspectionWindow, allowedFaults} = getFaultInspectionParams(opts); + this.faultInspectionWindow = faultInspectionWindow; + this.allowedFaults = allowedFaults; + } + + /** Whether builder bids must be ignored for a block produced at clockSlot */ + isActive(clockSlot: Slot): boolean { + this.update(clockSlot); + return this.active; + } + + update(clockSlot: Slot): void { + if (clockSlot <= this.lastUpdatedSlot) { + return; + } + this.lastUpdatedSlot = clockSlot; + + // Exclude clockSlot itself, its payload reveal may still be in flight + const {blocksPresent, payloadsRevealed} = this.modules.forkChoice.getPayloadRevealCounts( + Math.max(clockSlot - this.faultInspectionWindow, 0), + clockSlot - 1 + ); + const faults = blocksPresent - payloadsRevealed; + + const wasActive = this.active; + // Scale the fault budget by blocks present so sparse windows still trigger on high non-reveal rates + this.active = faults * this.faultInspectionWindow > this.allowedFaults * blocksPresent; + + this.modules.metrics?.builderCircuitBreaker.active.set(this.active ? 1 : 0); + this.modules.metrics?.builderCircuitBreaker.faults.set(faults); + + const logCtx = { + blocksPresent, + faults, + faultInspectionWindow: this.faultInspectionWindow, + allowedFaults: this.allowedFaults, + }; + if (this.active !== wasActive) { + this.modules.logger.info( + this.active + ? "Builder circuit breaker activated, ignoring builder bids" + : "Builder circuit breaker deactivated", + logCtx + ); + } else { + this.modules.logger.verbose("Builder circuit breaker status", {active: this.active, ...logCtx}); + } + } +} diff --git a/packages/beacon-node/src/chain/chain.ts b/packages/beacon-node/src/chain/chain.ts index caf578cbe47a..ec6177685753 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -75,6 +75,7 @@ import {ImportPayloadOpts} from "./blocks/types.js"; import {persistBlockInput} from "./blocks/writeBlockInputToDb.js"; import {persistPayloadEnvelopeInput} from "./blocks/writePayloadEnvelopeInputToDb.js"; import {BlsMultiThreadWorkerPool, BlsSingleThreadVerifier, IBlsVerifier} from "./bls/index.js"; +import {BuilderCircuitBreaker} from "./builderCircuitBreaker.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEvent, ChainEventEmitter} from "./emitter.js"; import {ForkchoiceCaller, initializeForkChoice} from "./forkChoice/index.js"; @@ -151,6 +152,7 @@ export class BeaconChain implements IBeaconChain { readonly genesisValidatorsRoot: Root; readonly executionEngine: IExecutionEngine; readonly executionBuilder?: IExecutionBuilder; + readonly builderCircuitBreaker: BuilderCircuitBreaker; // Expose config for convenience in modularized functions readonly config: BeaconConfig; readonly custodyConfig: CustodyConfig; @@ -425,6 +427,11 @@ export class BeaconChain implements IBeaconChain { this.forkChoice = forkChoice; + this.builderCircuitBreaker = new BuilderCircuitBreaker( + {faultInspectionWindow: opts.faultInspectionWindow, allowedFaults: opts.allowedFaults}, + {forkChoice, logger, metrics} + ); + this.seenPayloadEnvelopeInputCache = new SeenPayloadEnvelopeInput({ config, clock, diff --git a/packages/beacon-node/src/chain/errors/blockError.ts b/packages/beacon-node/src/chain/errors/blockError.ts index e4eb81dbead7..8bd396d0309f 100644 --- a/packages/beacon-node/src/chain/errors/blockError.ts +++ b/packages/beacon-node/src/chain/errors/blockError.ts @@ -67,6 +67,8 @@ export enum BlockErrorCode { TOO_MANY_KZG_COMMITMENTS = "BLOCK_ERROR_TOO_MANY_KZG_COMMITMENTS", /** Bid parent block root does not match block parent root */ BID_PARENT_ROOT_MISMATCH = "BLOCK_ERROR_BID_PARENT_ROOT_MISMATCH", + /** A block body operation or parent execution request list exceeds its per-block limit */ + TOO_MANY_BLOCK_OPERATIONS = "BLOCK_ERROR_TOO_MANY_BLOCK_OPERATIONS", /** The parent block's execution payload has been verified as invalid */ PARENT_EXECUTION_INVALID = "BLOCK_ERROR_PARENT_EXECUTION_INVALID", /** The block's parent execution payload (defined by bid.parent_block_hash) has not been seen */ @@ -118,6 +120,7 @@ export type BlockErrorType = | {code: BlockErrorCode.DATA_UNAVAILABLE} | {code: BlockErrorCode.TOO_MANY_KZG_COMMITMENTS; blobKzgCommitmentsLen: number; commitmentLimit: number} | {code: BlockErrorCode.BID_PARENT_ROOT_MISMATCH; bidParentRoot: RootHex; blockParentRoot: RootHex} + | {code: BlockErrorCode.TOO_MANY_BLOCK_OPERATIONS; name: string; count: number; limit: number} | {code: BlockErrorCode.PARENT_EXECUTION_INVALID; parentRoot: RootHex} | {code: BlockErrorCode.PARENT_PAYLOAD_UNKNOWN; parentRoot: RootHex; parentBlockHash: RootHex}; diff --git a/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts index c8a8645d6dc5..634345112e3a 100644 --- a/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/errors/executionPayloadEnvelope.ts @@ -12,6 +12,8 @@ export enum ExecutionPayloadEnvelopeErrorCode { BUILDER_INDEX_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BUILDER_INDEX_MISMATCH", BLOCK_HASH_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_BLOCK_HASH_MISMATCH", EXECUTION_REQUESTS_ROOT_MISMATCH = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_ROOT_MISMATCH", + EXECUTION_REQUESTS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_EXECUTION_REQUESTS_COUNT_EXCEEDED", + WITHDRAWALS_COUNT_EXCEEDED = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_WITHDRAWALS_COUNT_EXCEEDED", INVALID_SIGNATURE = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_INVALID_SIGNATURE", PAYLOAD_ENVELOPE_INPUT_MISSING = "EXECUTION_PAYLOAD_ENVELOPE_ERROR_PAYLOAD_ENVELOPE_INPUT_MISSING", } @@ -42,6 +44,13 @@ export type ExecutionPayloadEnvelopeErrorType = envelopeRequestsRoot: RootHex; bidRequestsRoot: RootHex; } + | { + code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED; + name: string; + count: number; + limit: number; + } + | {code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED; count: number; limit: number} | {code: ExecutionPayloadEnvelopeErrorCode.INVALID_SIGNATURE} | {code: ExecutionPayloadEnvelopeErrorCode.PAYLOAD_ENVELOPE_INPUT_MISSING; blockRoot: RootHex}; diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index effeaa8fb627..74fc30821c36 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -35,6 +35,7 @@ import {BeaconProposerCache, ProposerPreparationData} from "./beaconProposerCach import {IBlockInput} from "./blocks/blockInput/index.js"; import {ImportBlockOpts, ImportPayloadOpts} from "./blocks/types.js"; import {IBlsVerifier} from "./bls/index.js"; +import {BuilderCircuitBreaker} from "./builderCircuitBreaker.js"; import {ColumnReconstructionTracker} from "./ColumnReconstructionTracker.js"; import {ChainEventEmitter} from "./emitter.js"; import {ForkchoiceCaller} from "./forkChoice/index.js"; @@ -95,6 +96,7 @@ export interface IBeaconChain { readonly earliestAvailableSlot: Slot; readonly executionEngine: IExecutionEngine; readonly executionBuilder?: IExecutionBuilder; + readonly builderCircuitBreaker: BuilderCircuitBreaker; // Expose config for convenience in modularized functions readonly config: BeaconConfig; readonly custodyConfig: CustodyConfig; diff --git a/packages/beacon-node/src/chain/lightClient/index.ts b/packages/beacon-node/src/chain/lightClient/index.ts index c8c5ce18905d..223b6edd84a6 100644 --- a/packages/beacon-node/src/chain/lightClient/index.ts +++ b/packages/beacon-node/src/chain/lightClient/index.ts @@ -10,9 +10,8 @@ import { MIN_SYNC_COMMITTEE_PARTICIPANTS, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SIZE, - forkPostAltair, - highestFork, isForkPostElectra, + isForkPostGloas, } from "@lodestar/params"; import { type IBeaconStateViewAltair, @@ -44,7 +43,6 @@ import { electra, phase0, ssz, - sszTypesFor, } from "@lodestar/types"; import {Logger, MapDef, byteArrayEquals, pruneSetToMax, toRootHex} from "@lodestar/utils"; import {ZERO_HASH} from "../../constants/index.js"; @@ -230,8 +228,8 @@ export class LightClientServer { this.signal = signal; this.zero = { - // Assign the hightest fork's default value because it can always be typecasted down to correct fork - finalizedHeader: sszTypesFor(highestFork(forkPostAltair)).LightClientHeader.defaultValue(), + // Assign the highest pre-Gloas light-client header because post-Gloas light-client updates are skipped for now. + finalizedHeader: ssz.electra.LightClientHeader.defaultValue(), // Electra finalityBranch has fixed length of 5 whereas altair has 4. The fifth element will be ignored // when serializing as altair LightClientUpdate finalityBranch: ssz.electra.LightClientUpdate.fields.finalityBranch.defaultValue(), @@ -658,10 +656,17 @@ export class LightClientServer { const attestedFork = this.config.getForkName(attestedHeader.beacon.slot); const numWitness = syncCommitteeWitness.witness.length; - if (isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS_ELECTRA) { + if ( + isForkPostGloas(attestedFork) && + (syncCommitteeWitness.currentSyncCommitteeBranch === undefined || + syncCommitteeWitness.nextSyncCommitteeBranch === undefined) + ) { + throw Error("Expected post-Gloas sync committee branches"); + } + if (!isForkPostGloas(attestedFork) && isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS_ELECTRA) { throw Error(`Expected ${NUM_WITNESS_ELECTRA} witnesses in post-Electra numWitness=${numWitness}`); } - if (!isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS) { + if (!isForkPostGloas(attestedFork) && !isForkPostElectra(attestedFork) && numWitness !== NUM_WITNESS) { throw Error(`Expected ${NUM_WITNESS} witnesses in pre-Electra numWitness=${numWitness}`); } diff --git a/packages/beacon-node/src/chain/lightClient/proofs.ts b/packages/beacon-node/src/chain/lightClient/proofs.ts index 8636abc5ff7c..da98f33ce13e 100644 --- a/packages/beacon-node/src/chain/lightClient/proofs.ts +++ b/packages/beacon-node/src/chain/lightClient/proofs.ts @@ -1,11 +1,15 @@ import {Tree} from "@chainsafe/persistent-merkle-tree"; import { BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX, + CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS, FINALIZED_ROOT_GINDEX, FINALIZED_ROOT_GINDEX_ELECTRA, + FINALIZED_ROOT_GINDEX_GLOAS, ForkName, ForkPostBellatrix, + NEXT_SYNC_COMMITTEE_GINDEX_GLOAS, isForkPostElectra, + isForkPostGloas, } from "@lodestar/params"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "@lodestar/state-transition"; import {BeaconBlockBody, SSZTypesFor, ssz} from "@lodestar/types"; @@ -17,6 +21,24 @@ export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllFo let currentSyncCommitteeRoot: Uint8Array; let nextSyncCommitteeRoot: Uint8Array; + if (isForkPostGloas(fork)) { + const tree = new Tree(state.node); + const currentSyncCommitteeGindex = BigInt(CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS); + const nextSyncCommitteeGindex = BigInt(NEXT_SYNC_COMMITTEE_GINDEX_GLOAS); + + currentSyncCommitteeRoot = tree.getRoot(currentSyncCommitteeGindex); + nextSyncCommitteeRoot = tree.getRoot(nextSyncCommitteeGindex); + witness = []; + + return { + witness, + currentSyncCommitteeRoot, + nextSyncCommitteeRoot, + currentSyncCommitteeBranch: tree.getSingleProof(currentSyncCommitteeGindex), + nextSyncCommitteeBranch: tree.getSingleProof(nextSyncCommitteeGindex), + }; + } + if (isForkPostElectra(fork)) { const n2 = n1.left; const n5 = n2.right; @@ -60,17 +82,30 @@ export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllFo } export function getNextSyncCommitteeBranch(syncCommitteesWitness: SyncCommitteeWitness): Uint8Array[] { + if (syncCommitteesWitness.nextSyncCommitteeBranch) { + return syncCommitteesWitness.nextSyncCommitteeBranch; + } + // Witness branch is sorted by descending gindex return [syncCommitteesWitness.currentSyncCommitteeRoot, ...syncCommitteesWitness.witness]; } export function getCurrentSyncCommitteeBranch(syncCommitteesWitness: SyncCommitteeWitness): Uint8Array[] { + if (syncCommitteesWitness.currentSyncCommitteeBranch) { + return syncCommitteesWitness.currentSyncCommitteeBranch; + } + // Witness branch is sorted by descending gindex return [syncCommitteesWitness.nextSyncCommitteeRoot, ...syncCommitteesWitness.witness]; } export function getFinalizedRootProof(state: CachedBeaconStateAllForks): Uint8Array[] { - const finalizedRootGindex = state.epochCtx.isPostElectra() ? FINALIZED_ROOT_GINDEX_ELECTRA : FINALIZED_ROOT_GINDEX; + const fork = state.config.getForkName(state.slot); + const finalizedRootGindex = isForkPostGloas(fork) + ? FINALIZED_ROOT_GINDEX_GLOAS + : state.epochCtx.isPostElectra() + ? FINALIZED_ROOT_GINDEX_ELECTRA + : FINALIZED_ROOT_GINDEX; return new Tree(state.node).getSingleProof(BigInt(finalizedRootGindex)); } diff --git a/packages/beacon-node/src/chain/lightClient/types.ts b/packages/beacon-node/src/chain/lightClient/types.ts index b9723df501b3..96af0b165c3e 100644 --- a/packages/beacon-node/src/chain/lightClient/types.ts +++ b/packages/beacon-node/src/chain/lightClient/types.ts @@ -26,8 +26,10 @@ * ``` */ export type SyncCommitteeWitness = { - /** Vector[Bytes32, 4] or Vector[Bytes32, 5] depending on the fork */ + /** Shared witness for pre-Gloas forks where current and next sync committees are siblings. */ witness: Uint8Array[]; currentSyncCommitteeRoot: Uint8Array; nextSyncCommitteeRoot: Uint8Array; + currentSyncCommitteeBranch?: Uint8Array[]; + nextSyncCommitteeBranch?: Uint8Array[]; }; diff --git a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts index 55339fd566bc..d291af350f8c 100644 --- a/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts +++ b/packages/beacon-node/src/chain/opPools/aggregatedAttestationPool.ts @@ -212,7 +212,7 @@ export class AggregatedAttestationPool { forkChoice: IForkChoice, shufflingCache: ShufflingCache, state: IBeaconStateView - ): Attestation[] { + ): electra.Attestation[] { const forkSeq = ForkSeq[fork]; if (forkSeq < ForkSeq.electra) { throw new Error("Does not support producing blocks for pre-electra forks anymore"); diff --git a/packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts b/packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts index 443faff6d0e3..51b8c7046adb 100644 --- a/packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts +++ b/packages/beacon-node/src/chain/opPools/proposerPreferencesPool.ts @@ -17,7 +17,7 @@ import {toRootHex} from "@lodestar/utils"; export class ProposerPreferencesPool { private readonly bySlot = new Map>(); - /** Lookup for bid validation: matches `(bid.slot, get_proposer_dependent_root(parent_state, ...))`. */ + /** Lookup for bid validation: matches `(bid.slot, get_shuffling_dependent_root(store, bid.parent_block_root, epoch))`. */ get(slot: Slot, dependentRootHex: RootHex): gloas.SignedProposerPreferences | null { return this.bySlot.get(slot)?.get(dependentRootHex) ?? null; } diff --git a/packages/beacon-node/src/chain/options.ts b/packages/beacon-node/src/chain/options.ts index 767456d7e69a..844aa8e8baa6 100644 --- a/packages/beacon-node/src/chain/options.ts +++ b/packages/beacon-node/src/chain/options.ts @@ -49,6 +49,10 @@ export type IChainOptions = BlockProcessOpts & archiveDateEpochs?: number; nHistoricalStatesFileDataStore?: boolean; nativeStateView?: boolean; + /** Builder circuit breaker fault inspection window in slots */ + faultInspectionWindow?: number; + /** Allowed unrevealed payloads within the fault inspection window */ + allowedFaults?: number; }; export type BlockProcessOpts = { diff --git a/packages/beacon-node/src/chain/prepareNextSlot.ts b/packages/beacon-node/src/chain/prepareNextSlot.ts index ced08b44ad97..a991db1231ff 100644 --- a/packages/beacon-node/src/chain/prepareNextSlot.ts +++ b/packages/beacon-node/src/chain/prepareNextSlot.ts @@ -1,7 +1,7 @@ import {routes} from "@lodestar/api"; import {ChainForkConfig} from "@lodestar/config"; import {getSafeExecutionBlockHash} from "@lodestar/fork-choice"; -import {ForkPostBellatrix, ForkSeq, SLOTS_PER_EPOCH, isForkPostBellatrix} from "@lodestar/params"; +import {ForkPostBellatrix, ForkSeq, SLOTS_PER_EPOCH, isForkPostBellatrix, isForkPostGloas} from "@lodestar/params"; import { IBeaconStateView, IBeaconStateViewBellatrix, @@ -152,12 +152,16 @@ export class PrepareNextSlotScheduler { updatedHead = proposerHead; } - // Update the builder status, if enabled shoot an api call to check status - this.chain.updateBuilderStatus(clockSlot); - if (this.chain.executionBuilder?.status === BuilderStatus.enabled) { - this.chain.executionBuilder.checkStatus().catch((e) => { - this.logger.error("Builder disabled as the check status api failed", {prepareSlot}, e as Error); - }); + if (isForkPostGloas(fork)) { + this.chain.builderCircuitBreaker.update(clockSlot); + } else { + // Update the builder status, if enabled shoot an api call to check status + this.chain.updateBuilderStatus(clockSlot); + if (this.chain.executionBuilder?.status === BuilderStatus.enabled) { + this.chain.executionBuilder.checkStatus().catch((e) => { + this.logger.error("Builder disabled as the check status api failed", {prepareSlot}, e as Error); + }); + } } } diff --git a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index 25cea68f1d3b..25fb0b3738bf 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -24,6 +24,8 @@ import { isStatePostBellatrix, isStatePostCapella, isStatePostGloas, + upgradeAttestationToGloas, + upgradeAttesterSlashingToGloas, } from "@lodestar/state-transition"; import { BLSPubkey, @@ -1006,11 +1008,11 @@ export async function produceCommonBlockBody( // logger.error("Attestation did not transfer to op pool", {}, e); // } // } - const [attesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = + const [opPoolAttesterSlashings, proposerSlashings, voluntaryExits, blsToExecutionChanges] = this.opPool.getSlashingsAndExits(currentState, blockType, this.metrics); const endAttestations = stepsMetrics?.startTimer(); - const attestations = this.aggregatedAttestationPool.getAttestationsForBlock( + const opPoolAttestations = this.aggregatedAttestationPool.getAttestationsForBlock( fork, this.forkChoice, this.shufflingCache, @@ -1020,6 +1022,11 @@ export async function produceCommonBlockBody( step: BlockProductionStep.attestations, }); + const attestations = isForkPostGloas(fork) ? opPoolAttestations.map(upgradeAttestationToGloas) : opPoolAttestations; + const attesterSlashings = isForkPostGloas(fork) + ? opPoolAttesterSlashings.map(upgradeAttesterSlashingToGloas) + : opPoolAttesterSlashings; + const blockBody: Omit = { randaoReveal, graffiti, diff --git a/packages/beacon-node/src/chain/validation/attestation.ts b/packages/beacon-node/src/chain/validation/attestation.ts index f3b7b8863f57..bb0dae257b80 100644 --- a/packages/beacon-node/src/chain/validation/attestation.ts +++ b/packages/beacon-node/src/chain/validation/attestation.ts @@ -319,10 +319,17 @@ async function validateAttestationNoSignatureCheck( // [REJECT] If `attestation.data.index == 1` (payload present for a past // block), the execution payload for `block` passes validation. // [IGNORE] When `attestation.data.index == 1` (payload present for a past block), - // the corresponding execution payload for `block` has been seen (a client MAY queue - // attestations for processing once the payload is retrieved and SHOULD request the - // payload envelope via `ExecutionPayloadEnvelopesByRoot`). - if (block !== null && attData.index === 1 && !chain.seenPayloadEnvelope(toRootHex(attData.beaconBlockRoot))) { + // the corresponding execution payload for `block` has been fully imported, including its + // data -- i.e. `is_payload_verified(store, beacon_block_root)` returns True (consensus-specs + // #5355). `forkChoice.hasPayloadHexUnsafe` is the equivalent of `root in store.payloads`: + // it is true only once the SignedExecutionPayloadEnvelope has been imported and verified, + // not merely seen on gossip. A client MAY queue attestations until the payload is imported + // and SHOULD request the payload envelope via `ExecutionPayloadEnvelopesByRoot`. + if ( + block !== null && + attData.index === 1 && + !chain.forkChoice.hasPayloadHexUnsafe(toRootHex(attData.beaconBlockRoot)) + ) { throw new AttestationError(GossipAction.IGNORE, { code: AttestationErrorCode.EXECUTION_PAYLOAD_NOT_SEEN, beaconBlockRoot: toRootHex(attData.beaconBlockRoot), diff --git a/packages/beacon-node/src/chain/validation/block.ts b/packages/beacon-node/src/chain/validation/block.ts index 4f4ca45ac5d4..6c1ebb9595b1 100644 --- a/packages/beacon-node/src/chain/validation/block.ts +++ b/packages/beacon-node/src/chain/validation/block.ts @@ -1,6 +1,21 @@ import {ChainForkConfig} from "@lodestar/config"; import {ExecutionStatus} from "@lodestar/fork-choice"; -import {ForkName, isForkPostBellatrix, isForkPostDeneb, isForkPostGloas} from "@lodestar/params"; +import { + ForkName, + MAX_ATTESTATIONS_ELECTRA, + MAX_ATTESTER_SLASHINGS_ELECTRA, + MAX_BLS_TO_EXECUTION_CHANGES, + MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, + MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, + MAX_PAYLOAD_ATTESTATIONS, + MAX_PROPOSER_SLASHINGS, + MAX_VOLUNTARY_EXITS, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, + isForkPostBellatrix, + isForkPostDeneb, + isForkPostGloas, +} from "@lodestar/params"; import { computeEpochAtSlot, computeStartSlotAtEpoch, @@ -164,6 +179,56 @@ export async function validateGossipBlock( }); } + // [REJECT] The counts of `block.body.parent_execution_requests` are within + // their respective limits -- i.e. validate that + // `len(block.body.parent_execution_requests.withdrawals) <= MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD`, + // `len(block.body.parent_execution_requests.consolidations) <= MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD`, + // `len(block.body.parent_execution_requests.builder_deposits) <= MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD`, + // and + // `len(block.body.parent_execution_requests.builder_exits) <= MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD`. + // [REJECT] The counts of the block body operations are within their respective + // limits -- i.e. validate that + // `len(block.body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS`, + // `len(block.body.attester_slashings) <= MAX_ATTESTER_SLASHINGS_ELECTRA`, + // `len(block.body.attestations) <= MAX_ATTESTATIONS_ELECTRA`, + // `len(block.body.deposits) == 0`, + // `len(block.body.voluntary_exits) <= MAX_VOLUNTARY_EXITS`, + // `len(block.body.bls_to_execution_changes) <= MAX_BLS_TO_EXECUTION_CHANGES`, + // and `len(block.body.payload_attestations) <= MAX_PAYLOAD_ATTESTATIONS`. + const body = (block as gloas.BeaconBlock).body; + const requests = body.parentExecutionRequests; + const countLimits: [string, number, number][] = [ + ["parentExecutionRequests.withdrawals", requests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], + [ + "parentExecutionRequests.consolidations", + requests.consolidations.length, + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, + ], + [ + "parentExecutionRequests.builderDeposits", + requests.builderDeposits.length, + MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, + ], + ["parentExecutionRequests.builderExits", requests.builderExits.length, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD], + ["proposerSlashings", body.proposerSlashings.length, MAX_PROPOSER_SLASHINGS], + ["attesterSlashings", body.attesterSlashings.length, MAX_ATTESTER_SLASHINGS_ELECTRA], + ["attestations", body.attestations.length, MAX_ATTESTATIONS_ELECTRA], + ["deposits", body.deposits.length, 0], + ["voluntaryExits", body.voluntaryExits.length, MAX_VOLUNTARY_EXITS], + ["blsToExecutionChanges", body.blsToExecutionChanges.length, MAX_BLS_TO_EXECUTION_CHANGES], + ["payloadAttestations", body.payloadAttestations.length, MAX_PAYLOAD_ATTESTATIONS], + ]; + for (const [name, count, limit] of countLimits) { + if (count > limit) { + throw new BlockGossipError(GossipAction.REJECT, { + code: BlockErrorCode.TOO_MANY_BLOCK_OPERATIONS, + name, + count, + limit, + }); + } + } + // TODO GLOAS: [REJECT] The block's execution payload parent (defined by bid.parent_block_hash) passes all validation // This requires execution engine integration to verify the parent block hash } diff --git a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts index 2e29eac1217f..85736795f171 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadBid.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadBid.ts @@ -165,9 +165,9 @@ async function validateExecutionPayloadBid( }); } - // [REJECT] `bid.fee_recipient == proposer_preferences.fee_recipient`. + // [IGNORE] `bid.fee_recipient == proposer_preferences.fee_recipient`. if (!byteArrayEquals(bid.feeRecipient, proposerPreferences.message.feeRecipient)) { - throw new ExecutionPayloadBidError(GossipAction.REJECT, { + throw new ExecutionPayloadBidError(GossipAction.IGNORE, { code: ExecutionPayloadBidErrorCode.PROPOSER_PREFERENCES_FEE_RECIPIENT_MISMATCH, builderIndex: bid.builderIndex, bidFeeRecipient: toHex(bid.feeRecipient), diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index 9356a916b15d..56d74f0c6aa3 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -1,4 +1,11 @@ import {PayloadStatus} from "@lodestar/fork-choice"; +import { + MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, + MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, + MAX_WITHDRAWALS_PER_PAYLOAD, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, +} from "@lodestar/params"; import { computeStartSlotAtEpoch, getExecutionPayloadEnvelopeSignatureSet, @@ -115,6 +122,36 @@ async function validateExecutionPayloadEnvelope( }); } + // [REJECT] The non-deposit counts of `execution_requests` are within their respective limits. + // New in Gloas:EIP7688 — progressive lists are unbounded at the type level, so bounds + // are enforced here in gossip validation. + const {executionRequests} = envelope; + const requestCountLimits: [string, number, number][] = [ + ["withdrawals", executionRequests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], + ["consolidations", executionRequests.consolidations.length, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD], + ["builderDeposits", executionRequests.builderDeposits.length, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD], + ["builderExits", executionRequests.builderExits.length, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD], + ]; + for (const [name, count, limit] of requestCountLimits) { + if (count > limit) { + throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_COUNT_EXCEEDED, + name, + count, + limit, + }); + } + } + + // [REJECT] The number of withdrawals is within the limit. + if (payload.withdrawals.length > MAX_WITHDRAWALS_PER_PAYLOAD) { + throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { + code: ExecutionPayloadEnvelopeErrorCode.WITHDRAWALS_COUNT_EXCEEDED, + count: payload.withdrawals.length, + limit: MAX_WITHDRAWALS_PER_PAYLOAD, + }); + } + // Get the block state to verify the builder's signature. const blockState = await chain.regen .getState(block.stateRoot, RegenCaller.validateGossipPayloadEnvelope) diff --git a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts index fb42834ab424..55b8d906c128 100644 --- a/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts +++ b/packages/beacon-node/src/chain/validation/signatureSets/aggregateAndProof.ts @@ -1,7 +1,7 @@ import {BeaconConfig} from "@lodestar/config"; -import {DOMAIN_AGGREGATE_AND_PROOF, ForkSeq} from "@lodestar/params"; +import {DOMAIN_AGGREGATE_AND_PROOF} from "@lodestar/params"; import {ISignatureSet, SignatureSetType, computeSigningRoot, computeStartSlotAtEpoch} from "@lodestar/state-transition"; -import {Epoch, SignedAggregateAndProof, ValidatorIndex, ssz} from "@lodestar/types"; +import {Epoch, SignedAggregateAndProof, ValidatorIndex} from "@lodestar/types"; export function getAggregateAndProofSigningRoot( config: BeaconConfig, @@ -14,7 +14,7 @@ export function getAggregateAndProofSigningRoot( const slot = computeStartSlotAtEpoch(epoch); const fork = config.getForkName(slot); const aggregatorDomain = config.getDomainAtFork(fork, DOMAIN_AGGREGATE_AND_PROOF); - const sszType = ForkSeq[fork] >= ForkSeq.electra ? ssz.electra.AggregateAndProof : ssz.phase0.AggregateAndProof; + const sszType = config.getForkTypes(slot).AggregateAndProof; return computeSigningRoot(sszType, aggregateAndProof.message, aggregatorDomain); } diff --git a/packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts b/packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts index e323c3e55f61..21d4e756b7e2 100644 --- a/packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts +++ b/packages/beacon-node/src/db/repositories/lightclientSyncCommitteeWitness.ts @@ -1,14 +1,16 @@ import {ContainerType, VectorCompositeType} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import {DatabaseController, Repository} from "@lodestar/db"; +import {CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS, NEXT_SYNC_COMMITTEE_DEPTH_GLOAS} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {SyncCommitteeWitness} from "../../chain/lightClient/types.js"; import {Bucket, getBucketNameByValue} from "../buckets.js"; -// We add a 1-byte prefix where 0 means pre-electra and 1 means post-electra +// We add a 1-byte prefix where 0 means pre-Electra, 1 means post-Electra, and 2 means post-Gloas. enum PrefixByte { PRE_ELECTRA = 0, POST_ELECTRA = 1, + POST_GLOAS = 2, } export const NUM_WITNESS = 4; @@ -35,6 +37,35 @@ export class SyncCommitteeWitnessRepository extends Repository= ForkSeq.gloas + ? ssz.gloas.DepositRequests.serialize(depositRequests) + : ssz.electra.DepositRequests.serialize(depositRequests); return bytesToData(prefixRequests(requestsBytes, DEPOSIT_REQUEST_TYPE)); } -function deserializeDepositRequests(serialized: DepositRequestsRpc): electra.DepositRequests { - return ssz.electra.DepositRequests.deserialize(dataToBytes(serialized, null)); +function deserializeDepositRequests( + fork: ForkName, + serialized: DepositRequestsRpc +): electra.DepositRequests | gloas.DepositRequests { + return ForkSeq[fork] >= ForkSeq.gloas + ? ssz.gloas.DepositRequests.deserialize(dataToBytes(serialized, null)) + : ssz.electra.DepositRequests.deserialize(dataToBytes(serialized, null)); } function serializeWithdrawalRequests(withdrawalRequests: electra.WithdrawalRequests): WithdrawalRequestsRpc { @@ -571,7 +582,7 @@ export function serializeExecutionRequests(fork: ForkName, executionRequests: Ex const result: ExecutionRequestsRpc = []; if (deposits.length !== 0) { - result.push(serializeDepositRequests(deposits)); + result.push(serializeDepositRequests(fork, deposits)); } if (withdrawals.length !== 0) { @@ -637,7 +648,7 @@ export function deserializeExecutionRequests(fork: ForkName, serialized: Executi switch (currentRequestType) { case DEPOSIT_REQUEST_TYPE: { - result.deposits = deserializeDepositRequests(requests); + result.deposits = deserializeDepositRequests(fork, requests); break; } case WITHDRAWAL_REQUEST_TYPE: { diff --git a/packages/beacon-node/src/metrics/metrics/beacon.ts b/packages/beacon-node/src/metrics/metrics/beacon.ts index 4698600dd65c..06ea2d7d22a8 100644 --- a/packages/beacon-node/src/metrics/metrics/beacon.ts +++ b/packages/beacon-node/src/metrics/metrics/beacon.ts @@ -138,6 +138,17 @@ export function createBeaconMetrics(register: RegistryMetricCreator) { help: "Count of cached produced results", }), + builderCircuitBreaker: { + active: register.gauge({ + name: "beacon_builder_circuit_breaker_active", + help: "Whether the builder circuit breaker is active (1) causing builder bids to be ignored", + }), + faults: register.gauge({ + name: "beacon_builder_circuit_breaker_faults", + help: "Count of blocks with unrevealed payloads in the fault inspection window", + }), + }, + blockPayload: { payloadAdvancePrepTime: register.histogram({ name: "beacon_block_payload_prepare_time", diff --git a/packages/beacon-node/src/network/gossip/encoding.ts b/packages/beacon-node/src/network/gossip/encoding.ts index 6fb3d204e778..ddc2adc33436 100644 --- a/packages/beacon-node/src/network/gossip/encoding.ts +++ b/packages/beacon-node/src/network/gossip/encoding.ts @@ -9,7 +9,7 @@ import {ForkName} from "@lodestar/params"; import {intToBytes} from "@lodestar/utils"; import {MESSAGE_DOMAIN_VALID_SNAPPY} from "./constants.js"; import {Eth2GossipsubMetrics} from "./metrics.js"; -import {GossipTopicCache, getGossipSSZType} from "./topic.js"; +import {GossipTopicCache, getGossipSSZMaxSize, getGossipSSZType} from "./topic.js"; // Load WASM const xxhash = await xxhashFactory(); @@ -98,7 +98,7 @@ export function msgIdFn(gossipTopicCache: GossipTopicCache, msg: Message): Uint8 export class DataTransformSnappy implements DataTransform { constructor( private readonly gossipTopicCache: GossipTopicCache, - private readonly maxSizePerMessage: number, + private readonly maxPayloadSize: number, private readonly metrics: Eth2GossipsubMetrics | null ) {} @@ -111,17 +111,18 @@ export class DataTransformSnappy implements DataTransform { inboundTransform(topicStr: string, data: Uint8Array): Uint8Array { // check uncompressed data length before we actually decompress const uncompressedDataLength = snappyWasm.decompress_len(data); - if (uncompressedDataLength > this.maxSizePerMessage) { - throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${this.maxSizePerMessage}`); - } const topic = this.gossipTopicCache.getTopic(topicStr); const sszType = getGossipSSZType(topic); + const maxSize = getGossipSSZMaxSize(topic, this.maxPayloadSize, sszType); this.metrics?.dataTransform.inbound.inc({type: topic.type}); if (uncompressedDataLength < sszType.minSize) { throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} < ${sszType.minSize}`); } + if (uncompressedDataLength > maxSize) { + throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${maxSize}`); + } if (uncompressedDataLength > sszType.maxSize) { throw Error(`ssz_snappy decoded data length ${uncompressedDataLength} > ${sszType.maxSize}`); } @@ -139,9 +140,14 @@ export class DataTransformSnappy implements DataTransform { */ outboundTransform(topicStr: string, data: Uint8Array): Uint8Array { const topic = this.gossipTopicCache.getTopic(topicStr); + const sszType = getGossipSSZType(topic); + const maxSize = getGossipSSZMaxSize(topic, this.maxPayloadSize, sszType); this.metrics?.dataTransform.outbound.inc({type: topic.type}); - if (data.length > this.maxSizePerMessage) { - throw Error(`ssz_snappy encoded data length ${data.length} > ${this.maxSizePerMessage}`); + if (data.length > maxSize) { + throw Error(`ssz_snappy encoded data length ${data.length} > ${maxSize}`); + } + if (data.length > sszType.maxSize) { + throw Error(`ssz_snappy encoded data length ${data.length} > ${sszType.maxSize}`); } // Using Buffer.alloc() instead of Buffer.allocUnsafe() to mitigate high GC pressure observed in some environments diff --git a/packages/beacon-node/src/network/gossip/gossipsub.ts b/packages/beacon-node/src/network/gossip/gossipsub.ts index 28af60a174f6..547a69c60e91 100644 --- a/packages/beacon-node/src/network/gossip/gossipsub.ts +++ b/packages/beacon-node/src/network/gossip/gossipsub.ts @@ -15,7 +15,12 @@ import {type Multiaddr, multiaddr} from "@multiformats/multiaddr"; import {ENR} from "@chainsafe/enr"; import {routes} from "@lodestar/api"; import {BeaconConfig, ForkBoundary} from "@lodestar/config"; -import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, SYNC_COMMITTEE_SUBNET_COUNT} from "@lodestar/params"; +import { + ATTESTATION_SUBNET_COUNT, + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE, + SLOTS_PER_EPOCH, + SYNC_COMMITTEE_SUBNET_COUNT, +} from "@lodestar/params"; import {SubnetID} from "@lodestar/types"; import {Logger, Map2d, Map2dArr} from "@lodestar/utils"; import {RegistryMetricCreator} from "../../metrics/index.js"; @@ -179,7 +184,7 @@ export class Eth2Gossipsub { // Only send IDONTWANT messages if the message size is larger than this // This should be large enough to not send IDONTWANT for "small" messages // See https://github.com/ChainSafe/lodestar/pull/7077#issuecomment-2383679472 - idontwantMinDataSize: 16829, + idontwantMinDataSize: MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE, })(modules.libp2p.services.components) as GossipSubInternal; if (metrics) { diff --git a/packages/beacon-node/src/network/gossip/topic.ts b/packages/beacon-node/src/network/gossip/topic.ts index 60ed9ad4d0c2..7296e04e22d9 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -1,12 +1,18 @@ +import {type CompositeTypeAny} from "@chainsafe/ssz"; import {ForkDigestContext} from "@lodestar/config"; import { ATTESTATION_SUBNET_COUNT, ForkName, ForkSeq, + MAX_ATTESTER_SLASHING_SIZE, + MAX_DATA_COLUMN_SIDECAR_SIZE, + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE, + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE, SYNC_COMMITTEE_SUBNET_COUNT, isForkPostAltair, isForkPostElectra, isForkPostFulu, + isForkPostGloas, } from "@lodestar/params"; import {Attestation, SingleAttestation, ssz, sszTypesFor} from "@lodestar/types"; import {GossipAction, GossipActionError, GossipErrorCode} from "../../chain/errors/gossipValidation.js"; @@ -85,7 +91,7 @@ function stringifyGossipTopicType(topic: GossipTopic): string { } } -export function getGossipSSZType(topic: GossipTopic) { +export function getGossipSSZType(topic: GossipTopic): CompositeTypeAny { const {fork} = topic.boundary; switch (topic.type) { case GossipType.beacon_block: @@ -130,6 +136,30 @@ export function getGossipSSZType(topic: GossipTopic) { } } +/** + * Return the maximum uncompressed SSZ byte length accepted for a gossip object. + */ +export function getGossipSSZMaxSize(topic: GossipTopic, maxPayloadSize: number, sszType?: CompositeTypeAny): number { + const {fork} = topic.boundary; + // Gloas progressive containers have broad theoretical SSZ max sizes; use preset p2p bounds where specified. + switch (topic.type) { + case GossipType.beacon_block: + return maxPayloadSize; + case GossipType.beacon_aggregate_and_proof: + return isForkPostGloas(fork) ? MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize; + case GossipType.attester_slashing: + return isForkPostGloas(fork) ? MAX_ATTESTER_SLASHING_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize; + case GossipType.data_column_sidecar: + return isForkPostGloas(fork) ? MAX_DATA_COLUMN_SIDECAR_SIZE : (sszType ?? getGossipSSZType(topic)).maxSize; + case GossipType.execution_payload: + return maxPayloadSize; + case GossipType.execution_payload_bid: + return MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE; + default: + return (sszType ?? getGossipSSZType(topic)).maxSize; + } +} + /** * Deserialize a gossip serialized data into an ssz object. */ diff --git a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts index 1797552512d8..78a1e6719bc9 100644 --- a/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts +++ b/packages/beacon-node/src/network/reqresp/handlers/executionPayloadEnvelopesByRange.ts @@ -107,8 +107,10 @@ export function validateExecutionPayloadEnvelopesByRangeRequest( // The gloas req/resp spec uses MIN_EPOCHS_FOR_BLOCK_REQUESTS to define the minimum range peers MUST serve. // Archival nodes may still serve older retained payloads to allow genesis sync. - if (count > config.MAX_REQUEST_BLOCKS_DENEB) { - count = config.MAX_REQUEST_BLOCKS_DENEB; + // Spec: EnvelopesByRange response is bounded by MAX_REQUEST_PAYLOADS (consensus-specs #5383), + // distinct from the MAX_REQUEST_BLOCKS_DENEB cap used for block-by-range. + if (count > config.MAX_REQUEST_PAYLOADS) { + count = config.MAX_REQUEST_PAYLOADS; } return {startSlot, count}; diff --git a/packages/beacon-node/src/network/reqresp/rateLimit.ts b/packages/beacon-node/src/network/reqresp/rateLimit.ts index fc5d4e626625..ad0c3c76d4df 100644 --- a/packages/beacon-node/src/network/reqresp/rateLimit.ts +++ b/packages/beacon-node/src/network/reqresp/rateLimit.ts @@ -87,7 +87,7 @@ export const rateLimitQuotas: (fork: ForkName, config: BeaconConfig) => Record n + MAX_LOOK_AHEAD_EPOCHS // we should have enough batches to process in the buffer: n + 1, ..., n + MAX_LOOK_AHEAD_EPOCHS // let's focus on redownloading these batches first because it may have to reach different peers to get enough sampled columns + // + // Only pending (not-yet-validated) batches count toward the look-ahead window. AwaitingValidation + // batches (e.g. already-processed empty/skip-slot batches) cannot advance lastEpochWithProcessBlocks + // on their own, so counting them would let a run of empty epochs longer than MAX_LOOK_AHEAD_EPOCHS + // fill the window and stall sync. Mirrors the BATCH_BUFFER_SIZE carve-out above. + const pendingBatches = batches.filter((batch) => batch.state.status !== BatchStatus.AwaitingValidation); if ( - batches.length > 0 && - Math.max(...batches.map((b) => b.startEpoch)) >= this.lastEpochWithProcessBlocks + MAX_LOOK_AHEAD_EPOCHS + pendingBatches.length > 0 && + Math.max(...pendingBatches.map((b) => b.startEpoch)) >= this.lastEpochWithProcessBlocks + MAX_LOOK_AHEAD_EPOCHS ) { return null; } - // This line decides the starting epoch of the next batch. MUST ensure no duplicate batch for the same startEpoch + // This line decides the starting epoch of the next batch. MUST ensure no duplicate batch for the same startEpoch. + // Note: intentionally considers ALL batches (including AwaitingValidation), unlike the pending-only + // look-ahead check above — the next epoch must follow the last batch we hold to avoid gaps/duplicates, + // regardless of validation state. const startEpoch = toBeDownloadedStartEpoch(batches, this.lastEpochWithProcessBlocks); // Don't request batches beyond the target head slot. The to-be-downloaded batch must be strictly after target.slot diff --git a/packages/beacon-node/src/sync/utils/downloadByRange.ts b/packages/beacon-node/src/sync/utils/downloadByRange.ts index 5fa704e3417f..4d98646f1fe8 100644 --- a/packages/beacon-node/src/sync/utils/downloadByRange.ts +++ b/packages/beacon-node/src/sync/utils/downloadByRange.ts @@ -515,44 +515,21 @@ export async function validateResponses({ return {result: {responses: validatedResponses, payloadEnvelopes: null}, warnings}; } - if (!dataRequest) { - // Only envelope and/or parent-by-root validation needed - if (parentPayloadCommitments !== undefined) { - const parentValidated = await validateParentPayloadColumns( - parentPayloadCommitments, - columnSidecars ?? [], - peerDasMetrics - ); - if (parentValidated) { - validatedResponses.validatedColumnSidecars = [parentValidated]; - } - } - const validatedPayloadEnvelopes = validateEnvelopesByRangeResponse( - validatedResponses.validatedBlocks ?? [], - batchBlocks, - payloadEnvelopes ?? [], - parentPayloadCommitments - ); - return {result: {responses: validatedResponses, payloadEnvelopes: validatedPayloadEnvelopes}, warnings}; - } - - const blocksForDataValidation = getBlocksForDataValidation( - dataRequest, - batchBlocks, - validatedResponses.validatedBlocks?.length ? validatedResponses.validatedBlocks : undefined - ); - - if (!blocksForDataValidation.length) { - throw new DownloadByRangeError( - { - code: DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE, - ...requestsLogMeta({blobsRequest, columnsRequest}), - }, - "No blocks in data request slot range to validate data response against" - ); - } - - if (blobsRequest) { + // Resolve the in-range blocks the data sidecars are validated against. Empty when there is no data + // request (envelopes-only / parent-payload-only) or when the data request's slot range contains no + // blocks (an empty epoch — valid during poor chain liveness; the empty batch is confirmed at the + // chain level once a later batch imports a block, so a peer cannot stall sync by falsely claiming an + // epoch is empty). getBlocksForDataValidation treats `undefined` and `[]` the same, so pass directly. + const blocksForDataValidation = dataRequest + ? getBlocksForDataValidation(dataRequest, batchBlocks, validatedResponses.validatedBlocks) + : []; + + // When the range has no blocks we neither validate nor reject the sidecars a peer returned. We can't + // confirm the range is genuinely empty at this layer (the empty blocks response is itself unverified + // until a later batch extends the chain), so there is no peer we could safely penalize — orphan + // sidecars are dropped. The per-request validators below only run when there are blocks to validate + // against. + if (blobsRequest && blocksForDataValidation.length > 0) { if (!blobSidecars) { throw new DownloadByRangeError( { @@ -569,7 +546,7 @@ export async function validateResponses({ ); } - if (columnsRequest) { + if (columnsRequest && blocksForDataValidation.length > 0) { if (!columnSidecars) { throw new DownloadByRangeError( { @@ -665,26 +642,23 @@ export function validateBlockByRangeResponse( ): WarnResult { const {startSlot, count} = blocksRequest; - // An error was thrown here by @twoeths in #8150 but it breaks for epochs with 0 blocks during chain - // liveness issues. See comment https://github.com/ChainSafe/lodestar/issues/8147#issuecomment-3246434697 - // There are instances where clients return no blocks though. Need to monitor this via the warns to see - // if what the correct behavior should be + // A peer returning zero blocks for an epoch is the correct response when every slot in the epoch + // was skipped (an "empty epoch"), which happens during periods of poor chain liveness. Return the + // empty result with a warning — rather than throwing — so the SyncChain can process through the + // empty epoch. Throwing here previously killed the chain after MAX_BATCH_DOWNLOAD_ATTEMPTS + // (https://github.com/ChainSafe/lodestar/issues/8147). The empty batch is held in + // AwaitingValidation and only confirmed once a later batch imports a block, so a peer cannot stall + // sync by falsely claiming an epoch is empty. if (!blocks.length) { - throw new DownloadByRangeError({ - code: DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE, - ...requestsLogMeta({blocksRequest}), - }); - // TODO: this was causing deadlock again. need to come back and fix this so that its possible to process through - // an empty epoch for periods with poor liveness - // return { - // result: [], - // warnings: [ - // new DownloadByRangeError({ - // code: DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE, - // ...requestsLogMeta({blocksRequest}), - // }), - // ], - // }; + return { + result: [], + warnings: [ + new DownloadByRangeError({ + code: DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE, + ...requestsLogMeta({blocksRequest}), + }), + ], + }; } if (blocks.length > count) { diff --git a/packages/beacon-node/test/e2e/sync/emptyEpochSync.test.ts b/packages/beacon-node/test/e2e/sync/emptyEpochSync.test.ts new file mode 100644 index 000000000000..2a2254f8bd5a --- /dev/null +++ b/packages/beacon-node/test/e2e/sync/emptyEpochSync.test.ts @@ -0,0 +1,223 @@ +import {afterEach, describe, expect, it, vi} from "vitest"; +import {routes} from "@lodestar/api"; +import {ChainConfig} from "@lodestar/config"; +import {IForkChoice, ProtoBlock} from "@lodestar/fork-choice"; +import {TimestampFormatCode} from "@lodestar/logger"; +import {LogLevel, TestLoggerOpts, testLogger} from "@lodestar/logger/test-utils"; +import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {waitForEvent} from "../../utils/events/resolver.js"; +import {connect, onPeerConnect} from "../../utils/network.js"; +import {getDevBeaconNode} from "../../utils/node/beacon.js"; +import {getAndInitDevValidators} from "../../utils/node/validator.js"; + +/** + * Return the length of the longest run of consecutive empty epochs (epochs containing zero blocks) + * on a node's canonical chain. + */ +function longestEmptyEpochRun(forkChoice: IForkChoice, head: ProtoBlock): number { + let maxRun = 0; + let prevEpoch = Math.floor(head.slot / SLOTS_PER_EPOCH); + for (const block of forkChoice.getAllAncestorBlocks(head.blockRoot, head.payloadStatus)) { + const epoch = Math.floor(block.slot / SLOTS_PER_EPOCH); + maxRun = Math.max(maxRun, prevEpoch - epoch - 1); + prevEpoch = epoch; + } + return maxRun; +} + +// Regression test for https://github.com/ChainSafe/lodestar/pull/9417 (issue #8147). +// +// During periods of poor chain liveness an entire epoch can be empty: every slot is skipped, so +// a BeaconBlocksByRange request for that epoch correctly returns zero blocks. Range sync used to +// throw MISSING_BLOCKS_RESPONSE on a zero-block response, which — after MAX_BATCH_DOWNLOAD_ATTEMPTS +// — killed the SyncChain and deadlocked a node trying to sync past the empty epoch. A run of empty +// epochs longer than MAX_LOOK_AHEAD_EPOCHS (= 2) additionally stalled the look-ahead window. +// +// This test drives the real end-to-end path: Node A builds a chain with a run of >= 3 empty epochs +// (created by taking all validators offline for several epochs, i.e. simulating offline proposers), +// then Node B range-syncs from genesis and must catch up to Node A's head — crossing the gap. With +// the fix Node B reaches the head; without it Node B deadlocks at the first empty epoch. +// +// ~60 slots @ 2s/slot (minimal preset, SLOTS_PER_EPOCH = 8) ⇒ Node A is ready in ~125s, leaving +// ample room under the timeout for Node B's sync (60s budget) before the outer timeout fires. +describe("sync / empty epoch range sync", () => { + vi.setConfig({testTimeout: 300_000}); + + const validatorCount = 8; + const SLOT_DURATION_MS = 2000; + const ELECTRA_FORK_EPOCH = 0; + const FULU_FORK_EPOCH = 1; + const GLOAS_FORK_EPOCH = 2; + const testParams: Partial = { + SLOT_DURATION_MS, + ALTAIR_FORK_EPOCH: ELECTRA_FORK_EPOCH, + BELLATRIX_FORK_EPOCH: ELECTRA_FORK_EPOCH, + CAPELLA_FORK_EPOCH: ELECTRA_FORK_EPOCH, + DENEB_FORK_EPOCH: ELECTRA_FORK_EPOCH, + ELECTRA_FORK_EPOCH, + FULU_FORK_EPOCH, + GLOAS_FORK_EPOCH, + BLOB_SCHEDULE: [{EPOCH: 1, MAX_BLOBS_PER_BLOCK: 3}], + }; + + // Empty-epoch window. Produce blocks through epoch 2 (so the electra→fulu→gloas transitions all + // happen with blocks present), take the validators offline for the next four epochs, then resume. + // Even if a single in-flight proposal lands right after we go offline, epochs 4–6 are guaranteed + // fully empty (>= 3, exceeding MAX_LOOK_AHEAD_EPOCHS), exercising the whole fix. + const STOP_SLOT = 3 * SLOTS_PER_EPOCH; // 24 — first slot with no proposer + const RESUME_SLOT = 7 * SLOTS_PER_EPOCH; // 56 — proposers come back online + const TARGET_HEAD_MIN_SLOT = RESUME_SLOT + Math.floor(SLOTS_PER_EPOCH / 2); // 60 — at least a full batch past the gap + + const afterEachCallbacks: (() => Promise | void)[] = []; + afterEach(async () => { + while (afterEachCallbacks.length > 0) { + const callback = afterEachCallbacks.pop(); + if (callback) await callback(); + } + }); + + it("syncs through a run of empty epochs during poor liveness", async () => { + const genesisSlotsDelay = 4; + const genesisTime = Math.floor(Date.now() / 1000) + genesisSlotsDelay * (SLOT_DURATION_MS / 1000); + + const testLoggerOpts: TestLoggerOpts = { + level: LogLevel.info, + timestampFormat: { + format: TimestampFormatCode.EpochSlot, + genesisTime, + slotsPerEpoch: SLOTS_PER_EPOCH, + secondsPerSlot: SLOT_DURATION_MS / 1000, + }, + }; + + const loggerNodeA = testLogger("EmptyEpochSync-Node-A", testLoggerOpts); + const loggerNodeB = testLogger("EmptyEpochSync-Node-B", testLoggerOpts); + + // Node A: the lone block producer. `isSingleNode` + `allowPublishToZeroPeers` let it build a + // chain without any peers connected. + const bn = await getDevBeaconNode({ + params: testParams, + options: { + // Large slotImportTolerance so Node A keeps considering itself "synced" — and keeps + // producing — while its head sits several epochs behind the wall clock during the offline + // window. A lone node has no peers to range-sync from, so with the default tolerance it + // would go Stalled once the head fell > 1 epoch behind and never resume block production. + sync: {isSingleNode: true, slotImportTolerance: 12 * SLOTS_PER_EPOCH}, + network: {allowPublishToZeroPeers: true, useWorker: false}, + chain: {blsVerifyAllMainThread: true}, + }, + validatorCount, + genesisTime, + logger: loggerNodeA, + }); + afterEachCallbacks.push(() => bn.close()); + + // Produce blocks through epoch 2 (covering the electra→fulu→gloas transitions). + let validators: Awaited>["validators"] = ( + await getAndInitDevValidators({ + node: bn, + logPrefix: "EmptyEpochSyncVc-1", + validatorsPerClient: validatorCount, + validatorClientCount: 1, + startIndex: 0, + useRestApi: false, + testLoggerOpts, + }) + ).validators; + afterEachCallbacks.push(() => Promise.all(validators.map((v) => v.close().catch(() => {})))); + + await bn.chain.clock.waitForSlot(STOP_SLOT); + + // Sanity-check that Node A actually built a chain before we create the gap. + expect( + bn.chain.forkChoice.getHead().slot, + "Node A should have produced blocks before going offline" + ).toBeGreaterThanOrEqual(STOP_SLOT - SLOTS_PER_EPOCH); + + // Take every validator offline. With no proposer online, every slot from here until + // the validators come back is skipped, producing a run of empty epochs on Node A's chain. + await Promise.all(validators.map((v) => v.close())); + loggerNodeA.info("Validators offline — empty epochs begin", {stopSlot: STOP_SLOT}); + + await bn.chain.clock.waitForSlot(RESUME_SLOT); + + // Bring validators back online (fresh clients, same keys). The chain resumes building + // on the pre-gap head, leaving the skipped slots permanently empty. + validators = ( + await getAndInitDevValidators({ + node: bn, + logPrefix: "EmptyEpochSyncVc-2", + validatorsPerClient: validatorCount, + validatorClientCount: 1, + startIndex: 0, + useRestApi: false, + testLoggerOpts, + }) + ).validators; + afterEachCallbacks.push(() => Promise.all(validators.map((v) => v.close().catch(() => {})))); + loggerNodeA.info("Validators back online — empty epochs end", {resumeSlot: RESUME_SLOT}); + + // Wait until Node A has built at least a full epoch of blocks past the gap. Range sync confirms + // an empty (AwaitingValidation) batch only once a *later* batch imports a block, so the synced + // chain must extend past the empty run. + await waitForEvent( + bn.chain.emitter, + routes.events.EventType.head, + 120_000, + ({slot}) => slot >= TARGET_HEAD_MIN_SLOT + ); + + // Capture Node A's head past the gap as Node B's sync target. Node A keeps producing, but this + // block stays on its canonical chain, so Node B reaching it proves it synced across the gap. + const headSummary = bn.chain.forkChoice.getHead(); + const headRootHex = headSummary.blockRoot; + const emptyRunNodeA = longestEmptyEpochRun(bn.chain.forkChoice, headSummary); + loggerNodeA.info("Node A built chain past the empty epochs", { + headSlot: headSummary.slot, + headRoot: headRootHex, + longestEmptyEpochRun: emptyRunNodeA, + }); + + // Precondition: the scenario we mean to exercise must actually exist on Node A's chain. + expect( + emptyRunNodeA, + `Node A chain must contain a run of >= 3 empty epochs to exercise the regression (got ${emptyRunNodeA})` + ).toBeGreaterThanOrEqual(3); + + // Node B: starts from genesis and must reach Node A's head purely via range sync, which has to + // traverse the empty epochs. + const bn2 = await getDevBeaconNode({ + params: testParams, + options: { + api: {rest: {enabled: false}}, + network: {useWorker: false}, + chain: {blsVerifyAllMainThread: true}, + }, + validatorCount, + genesisTime, + logger: loggerNodeB, + }); + afterEachCallbacks.push(() => bn2.close()); + + // Attach the listener before connecting so the head event can't be missed. + const waitForSynced = waitForEvent( + bn2.chain.emitter, + routes.events.EventType.head, + 60_000, + ({block}) => block === headRootHex + ); + + await Promise.all([connect(bn2.network, bn.network), onPeerConnect(bn2.network), onPeerConnect(bn.network)]); + loggerNodeB.info("Node B connected to Node A"); + + try { + await waitForSynced; + } catch (_e) { + expect.fail( + `Node B failed to range-sync through the empty epochs to Node A's head (slot ${headSummary.slot}). ` + + "Before the fix this deadlocks: a zero-block response for an empty epoch threw MISSING_BLOCKS_RESPONSE, " + + "killing the SyncChain after MAX_BATCH_DOWNLOAD_ATTEMPTS." + ); + } + }); +}); diff --git a/packages/beacon-node/test/mocks/mockedBeaconChain.ts b/packages/beacon-node/test/mocks/mockedBeaconChain.ts index ab74100273ca..44faa56d3316 100644 --- a/packages/beacon-node/test/mocks/mockedBeaconChain.ts +++ b/packages/beacon-node/test/mocks/mockedBeaconChain.ts @@ -5,9 +5,11 @@ import {EpochDifference, ForkChoice, ProtoBlock} from "@lodestar/fork-choice"; import {createPubkeyCache} from "@lodestar/state-transition"; import {Logger} from "@lodestar/utils"; import {BeaconProposerCache} from "../../src/chain/beaconProposerCache.js"; +import {BuilderCircuitBreaker} from "../../src/chain/builderCircuitBreaker.js"; import {BeaconChain} from "../../src/chain/chain.js"; import {ChainEventEmitter} from "../../src/chain/emitter.js"; import {LightClientServer} from "../../src/chain/lightClient/index.js"; +import {ExecutionPayloadBidPool} from "../../src/chain/opPools/executionPayloadBidPool.js"; import {AggregatedAttestationPool, OpPool, SyncContributionAndProofPool} from "../../src/chain/opPools/index.js"; import {QueuedStateRegenerator} from "../../src/chain/regen/index.js"; import {SeenBlockInput} from "../../src/chain/seenCache/seenGossipBlockInput.js"; @@ -23,6 +25,8 @@ export type MockedBeaconChain = Mocked & { forkChoice: MockedForkChoice; executionEngine: Mocked; executionBuilder: Mocked; + builderCircuitBreaker: Mocked; + executionPayloadBidPool: Mocked; opPool: Mocked; aggregatedAttestationPool: Mocked; syncContributionAndProofPool: Mocked; @@ -71,6 +75,8 @@ vi.mock("@lodestar/fork-choice", async (importActual) => { hasBlockHex: vi.fn(), getBlockSummariesAtSlot: vi.fn(), notifyPtcMessages: vi.fn(), + shouldBuildOnFull: vi.fn(), + getPayloadRevealCounts: vi.fn(), }; }); @@ -143,6 +149,14 @@ vi.mock("../../src/chain/chain.js", async (importActual) => { getClientVersion: vi.fn(), }, executionBuilder: {}, + builderCircuitBreaker: { + isActive: vi.fn(), + update: vi.fn(), + }, + executionPayloadBidPool: { + add: vi.fn(), + getBestBid: vi.fn(), + }, opPool: new OpPool(config as BeaconConfig), aggregatedAttestationPool: new AggregatedAttestationPool(config as BeaconConfig), syncContributionAndProofPool: new SyncContributionAndProofPool(config, clock), diff --git a/packages/beacon-node/test/spec/general/index.test.ts b/packages/beacon-node/test/spec/general/index.test.ts index 2c763eab5a54..5cd418cc7e8d 100644 --- a/packages/beacon-node/test/spec/general/index.test.ts +++ b/packages/beacon-node/test/spec/general/index.test.ts @@ -34,17 +34,6 @@ specTestIterator( // where deserialized .d value is D: '0x00'. However the tests guide mark that field as D: Bytes[256]. // Those test won't be fixed since most implementations staticly compile types. "ComplexTestStruct", - "ProgressiveTestStruct", - "ProgressiveBitsStruct", - "proglist", - "progbitlist", - "ProgressiveVarTestStruct", - "ProgressiveSingleFieldContainerTestStruct", - "ProgressiveSingleListContainerTestStruct", - "ProgressiveComplexTestStruct", - "CompatibleUnionA", - "CompatibleUnionBC", - "CompatibleUnionABCA", ]), }, }, diff --git a/packages/beacon-node/test/spec/general/ssz_generic_types.ts b/packages/beacon-node/test/spec/general/ssz_generic_types.ts index 2386f770fe60..886c18c3173d 100644 --- a/packages/beacon-node/test/spec/general/ssz_generic_types.ts +++ b/packages/beacon-node/test/spec/general/ssz_generic_types.ts @@ -2,8 +2,16 @@ import { BitListType, BitVectorType, BooleanType, + ByteListType, + CompatibleUnionType, ContainerType, ListBasicType, + ListCompositeType, + ProgressiveBitListType, + ProgressiveByteListType, + ProgressiveContainerType, + ProgressiveListBasicType, + ProgressiveListCompositeType, Type, UintBigintType, UintNumberType, @@ -66,12 +74,24 @@ const ComplexTestStruct = new ContainerType({ A: uint16, B: new ListBasicType(uint16, 128), C: uint8, - D: new BitListType(256), + D: new ByteListType(256), E: VarTestStruct, F: new VectorCompositeType(FixedTestStruct, 4), G: new VectorCompositeType(VarTestStruct, 2), }); +// class ProgressiveTestStruct(Container): +// A: ProgressiveList[byte] +// B: ProgressiveList[uint64] +// C: ProgressiveList[SmallTestStruct] +// D: ProgressiveList[ProgressiveList[VarTestStruct]] +const ProgressiveTestStruct = new ContainerType({ + A: new ProgressiveByteListType(), + B: new ProgressiveListBasicType(uint64), + C: new ProgressiveListCompositeType(SmallTestStruct), + D: new ProgressiveListCompositeType(new ProgressiveListCompositeType(VarTestStruct)), +}); + // class BitsStruct(Container): // A: Bitlist[5] // B: Bitvector[2] @@ -86,13 +106,135 @@ const BitsStruct = new ContainerType({ E: new BitVectorType(8), }); +// class ProgressiveBitsStruct(Container): +// A: Bitvector[256] +// B: Bitlist[256] +// C: ProgressiveBitlist +// D: Bitvector[257] +// E: Bitlist[257] +// F: ProgressiveBitlist +// G: Bitvector[1280] +// H: Bitlist[1280] +// I: ProgressiveBitlist +// J: Bitvector[1281] +// K: Bitlist[1281] +// L: ProgressiveBitlist +const ProgressiveBitsStruct = new ContainerType({ + A: new BitVectorType(256), + B: new BitListType(256), + C: new ProgressiveBitListType(), + D: new BitVectorType(257), + E: new BitListType(257), + F: new ProgressiveBitListType(), + G: new BitVectorType(1280), + H: new BitListType(1280), + I: new ProgressiveBitListType(), + J: new BitVectorType(1281), + K: new BitListType(1281), + L: new ProgressiveBitListType(), +}); + +// class ProgressiveSingleFieldContainerTestStruct(ProgressiveContainer(active_fields=[1])): +// A: byte +const ProgressiveSingleFieldContainerTestStruct = new ProgressiveContainerType({A: byte}, [true]); + +// class ProgressiveSingleListContainerTestStruct(ProgressiveContainer(active_fields=[0, 0, 0, 0, 1])): +// C: ProgressiveBitlist +const ProgressiveSingleListContainerTestStruct = new ProgressiveContainerType({C: new ProgressiveBitListType()}, [ + false, + false, + false, + false, + true, +]); + +// class ProgressiveVarTestStruct(ProgressiveContainer(active_fields=[1, 0, 1, 0, 1])): +// A: byte +// B: List[uint16, 123] +// C: ProgressiveBitlist +const ProgressiveVarTestStruct = new ProgressiveContainerType( + {A: byte, B: new ListBasicType(uint16, 123), C: new ProgressiveBitListType()}, + [true, false, true, false, true] +); + +const progressiveComplexActiveFields = [ + true, + false, + true, + false, + true, + false, + false, + false, + true, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + true, + true, +]; + +// class ProgressiveComplexTestStruct(ProgressiveContainer(active_fields=progressiveComplexActiveFields)): +// A: byte +// B: List[uint16, 123] +// C: ProgressiveBitlist +// D: ProgressiveList[uint64] +// E: ProgressiveList[SmallTestStruct] +// F: ProgressiveList[ProgressiveList[VarTestStruct]] +// G: List[ProgressiveSingleFieldContainerTestStruct, 10] +// H: ProgressiveList[ProgressiveVarTestStruct] +const ProgressiveComplexTestStruct = new ProgressiveContainerType( + { + A: byte, + B: new ListBasicType(uint16, 123), + C: new ProgressiveBitListType(), + D: new ProgressiveListBasicType(uint64), + E: new ProgressiveListCompositeType(SmallTestStruct), + F: new ProgressiveListCompositeType(new ProgressiveListCompositeType(VarTestStruct)), + G: new ListCompositeType(ProgressiveSingleFieldContainerTestStruct, 10), + H: new ProgressiveListCompositeType(ProgressiveVarTestStruct), + }, + progressiveComplexActiveFields +); + const containerTypes = { SingleFieldTestStruct, SmallTestStruct, FixedTestStruct, VarTestStruct, ComplexTestStruct, + ProgressiveTestStruct, BitsStruct, + ProgressiveBitsStruct, +}; + +const progressiveContainerTypes = { + ProgressiveSingleFieldContainerTestStruct, + ProgressiveSingleListContainerTestStruct, + ProgressiveVarTestStruct, + ProgressiveComplexTestStruct, +}; + +const compatibleUnionTypes = { + CompatibleUnionA: new CompatibleUnionType({1: ProgressiveSingleFieldContainerTestStruct}), + CompatibleUnionBC: new CompatibleUnionType({ + 2: ProgressiveSingleListContainerTestStruct, + 3: ProgressiveVarTestStruct, + }), + CompatibleUnionABCA: new CompatibleUnionType({ + 1: ProgressiveSingleFieldContainerTestStruct, + 2: ProgressiveSingleListContainerTestStruct, + 3: ProgressiveVarTestStruct, + 4: ProgressiveSingleFieldContainerTestStruct, + }), }; const vecElementTypes = { @@ -124,6 +266,16 @@ export function getTestType(testType: string, testCase: string): Type { return new VectorBasicType(elementType, length); } + // `proglist_{element type}_{...}` + // {element type}: bool, uint8, uint16, uint32, uint64, uint128, uint256 + case "basic_progressive_list": { + const match = testCase.match(/proglist_([^\W_]+)/); + const [, elementTypeStr] = match || []; + const elementType = vecElementTypes[elementTypeStr as keyof typeof vecElementTypes]; + if (elementType === undefined) throw Error(`No progListElementType for ${elementTypeStr}: '${testCase}'`); + return new ProgressiveListBasicType(elementType); + } + // `bitlist_{limit}` // {limit}: the list limit, in bits, of the bitlist. case "bitlist": { @@ -141,6 +293,9 @@ export function getTestType(testType: string, testCase: string): Type { return new BitVectorType(parseSecondNum(testCase, "length")); } + case "progressive_bitlist": + return new ProgressiveBitListType(); + // A boolean has no type variations. Instead, file names just plainly describe the contents for debugging. case "boolean": return bool; @@ -155,6 +310,26 @@ export function getTestType(testType: string, testCase: string): Type { return containerType; } + // {container name} + // {container name}: Any of the progressive container names listed in the SSZ generic test format + case "progressive_containers": { + const match = testCase.match(/([^\W_]+)/); + const containerName = (match || [])[1]; + const containerType = progressiveContainerTypes[containerName as keyof typeof progressiveContainerTypes]; + if (containerType === undefined) throw Error(`No progressiveContainerType for ${containerName}`); + return containerType; + } + + // {union name} + // {union name}: Any of the compatible union names listed in the SSZ generic test format + case "compatible_unions": { + const match = testCase.match(/([^\W_]+)/); + const unionName = (match || [])[1]; + const unionType = compatibleUnionTypes[unionName as keyof typeof compatibleUnionTypes]; + if (unionType === undefined) throw Error(`No compatibleUnionType for ${unionName}`); + return unionType; + } + // `uint_{size}` // {size}: the uint size: 8, 16, 32, 64, 128 or 256. case "uints": { diff --git a/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts b/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts index 8537f604e7b6..f6323eb9619f 100644 --- a/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts +++ b/packages/beacon-node/test/spec/presets/fast_confirmation.test.ts @@ -4,7 +4,7 @@ import {expect} from "vitest"; import {toHexString} from "@chainsafe/ssz"; import {createBeaconConfig} from "@lodestar/config"; import {getConfig} from "@lodestar/config/test-utils"; -import {CheckpointWithHex, ForkChoice} from "@lodestar/fork-choice"; +import {CheckpointWithHex, ExecutionStatus, ForkChoice} from "@lodestar/fork-choice"; import {testLogger} from "@lodestar/logger/test-utils"; import { ACTIVE_PRESET, @@ -20,10 +20,13 @@ import {InputType} from "@lodestar/spec-test-util"; import { BeaconStateAllForks, BeaconStateView, + DataAvailabilityStatus, + IBeaconStateViewGloas, computeEpochAtSlot, createCachedBeaconState, createPubkeyCache, isExecutionStateType, + isGloasStateType, signedBlockToSignedHeader, syncPubkeys, } from "@lodestar/state-transition"; @@ -35,6 +38,7 @@ import { SignedBeaconBlock, deneb, fulu, + gloas, ssz, sszTypesFor, } from "@lodestar/types"; @@ -47,8 +51,14 @@ import { BlockInputSource, } from "../../../src/chain/blocks/blockInput/index.ts"; import {AttestationImportOpt, BlobSidecarValidation} from "../../../src/chain/blocks/types.ts"; +import { + verifyExecutionPayloadEnvelope, + verifyExecutionPayloadEnvelopeSignature, +} from "../../../src/chain/blocks/verifyExecutionPayloadEnvelope.js"; import {BeaconChain, ChainEvent} from "../../../src/chain/index.ts"; import {defaultChainOptions} from "../../../src/chain/options.ts"; +import {RegenCaller} from "../../../src/chain/regen/index.ts"; +import {getShufflingForAttestationVerification} from "../../../src/chain/validation/attestation.ts"; import {validateFuluBlockDataColumnSidecars} from "../../../src/chain/validation/dataColumnSidecar.ts"; import {ZERO_HASH_HEX} from "../../../src/constants/constants.ts"; import {ExecutionPayloadStatus} from "../../../src/execution/engine/interface.ts"; @@ -56,6 +66,7 @@ import {ExecutionEngineMockBackend} from "../../../src/execution/engine/mock.ts" import {getExecutionEngineFromBackend} from "../../../src/execution/index.ts"; import {computePreFuluKzgCommitmentsInclusionProof} from "../../../src/util/blobs.ts"; import {ClockEvent} from "../../../src/util/clock.ts"; +import {getShufflingDependentRoot} from "../../../src/util/dependentRoot.ts"; import {ClockStopped} from "../../mocks/clock.ts"; import {getMockedBeaconDb} from "../../mocks/mockedBeaconDb.ts"; import {assertCorrectProgressiveBalances} from "../config.ts"; @@ -68,6 +79,7 @@ const ANCHOR_BLOCK_FILE_NAME = "anchor_block"; const BLOCK_FILE_NAME = "^(block)_([0-9a-zA-Z]+)$"; const BLOBS_FILE_NAME = "^(blobs)_([0-9a-zA-Z]+)$"; const COLUMN_FILE_NAME = "^(column)_([0-9a-zA-Z]+)$"; +const EXECUTION_PAYLOAD_ENVELOPE_FILE_NAME = "^(execution_payload_envelope)_([0-9a-zA-Z]+)$"; const ATTESTATION_FILE_NAME = "^(attestation)_([0-9a-zA-Z])+$"; const ATTESTER_SLASHING_FILE_NAME = "^(attester_slashing)_([0-9a-zA-Z])+$"; @@ -88,9 +100,11 @@ const fastConfirmationTest = const clock = new ClockStopped(currentSlot); const executionEngineBackend = new ExecutionEngineMockBackend({ onlyPredefinedResponses: opts.onlyPredefinedResponses, - genesisBlockHash: isExecutionStateType(anchorState) - ? toHexString(anchorState.latestExecutionPayloadHeader.blockHash) - : ZERO_HASH_HEX, + genesisBlockHash: isGloasStateType(anchorState) + ? toHexString(anchorState.latestBlockHash) + : isExecutionStateType(anchorState) + ? toHexString(anchorState.latestExecutionPayloadHeader.blockHash) + : ZERO_HASH_HEX, }); const controller = new AbortController(); @@ -172,10 +186,25 @@ const fastConfirmationTest = logger.debug(`Step ${i}/${stepsLen} attestation`, {root: step.attestation, valid: Boolean(step.valid)}); const attestation = testcase.attestations.get(step.attestation); if (!attestation) throw Error(`No attestation ${step.attestation}`); - const headState = chain.getHeadState(); const attDataRootHex = toHexString(sszTypesFor(fork).AttestationData.hashTreeRoot(attestation.data)); const attEpoch = computeEpochAtSlot(attestation.data.slot); - const decisionRoot = headState.getShufflingDecisionRoot(attEpoch); + const attHeadBlock = chain.forkChoice.getBlockHexDefaultStatus(toHex(attestation.data.beaconBlockRoot)); + if (attHeadBlock === null) { + throw Error(`No attested block ${toHexString(attestation.data.beaconBlockRoot)}`); + } + const attHeadBlockEpoch = computeEpochAtSlot(attHeadBlock.slot); + const decisionRoot = getShufflingDependentRoot( + chain.forkChoice, + attEpoch, + attHeadBlockEpoch, + attHeadBlock + ); + await getShufflingForAttestationVerification( + chain, + attEpoch, + attHeadBlock, + RegenCaller.validateGossipAttestation + ); chain.forkChoice.onAttestation( chain.shufflingCache.getIndexedAttestation(attEpoch, decisionRoot, ForkSeq[fork], attestation), attDataRootHex @@ -379,6 +408,64 @@ const fastConfirmationTest = } } + // execution_payload step for Gloas tests + else if (isExecutionPayload(step)) { + const isValid = Boolean(step.valid ?? true); + logger.debug(`Step ${i}/${stepsLen} execution_payload`, { + envelope: step.execution_payload, + valid: isValid, + }); + const envelope = testcase.executionPayloadEnvelopes.get(step.execution_payload); + if (!envelope) throw Error(`No execution payload envelope ${step.execution_payload}`); + + try { + const beaconBlockRoot = toHex(envelope.message.beaconBlockRoot); + const blockHash = toHex(envelope.message.payload.blockHash); + const blockNumber = envelope.message.payload.blockNumber; + const gasLimit = envelope.message.payload.gasLimit; + + const protoBlock = chain.forkChoice.getBlockHexDefaultStatus(beaconBlockRoot); + if (!protoBlock) throw Error(`Block not found for root ${beaconBlockRoot}`); + const blockState = await chain.regen.getBlockSlotState( + protoBlock, + protoBlock.slot, + {dontTransferCache: true}, + RegenCaller.processBlock + ); + verifyExecutionPayloadEnvelope(beaconConfig, blockState as IBeaconStateViewGloas, envelope.message); + + if (testcase.meta?.bls_setting === BigInt(1)) { + const sigValid = await verifyExecutionPayloadEnvelopeSignature( + beaconConfig, + blockState as IBeaconStateViewGloas, + pubkeyCache, + envelope, + blockState.latestBlockHeader.proposerIndex, + chain.bls + ); + if (!sigValid) throw Error("Invalid execution payload envelope signature"); + } + + executionEngineBackend.addPredefinedPayloadStatus(blockHash, { + status: ExecutionPayloadStatus.VALID, + latestValidHash: null, + validationError: null, + }); + + (chain.forkChoice as ForkChoice).onExecutionPayload( + beaconBlockRoot, + blockHash, + blockNumber, + gasLimit, + ExecutionStatus.Valid, + DataAvailabilityStatus.Available + ); + if (!isValid) throw Error("Expect error since this is a negative test"); + } catch (e) { + if (isValid || (e as Error).message === "Expect error since this is a negative test") throw e; + } + } + // Optional step for optimistic sync tests. else if (isOnPayloadInfoStep(step)) { logger.debug(`Step ${i}/${stepsLen} payload_status`, {blockHash: step.block_hash}); @@ -516,6 +603,7 @@ const fastConfirmationTest = [BLOCK_FILE_NAME]: ssz[fork].SignedBeaconBlock, [BLOBS_FILE_NAME]: ssz.deneb.Blobs, [COLUMN_FILE_NAME]: ssz.fulu.DataColumnSidecar, + [EXECUTION_PAYLOAD_ENVELOPE_FILE_NAME]: ssz.gloas.SignedExecutionPayloadEnvelope, [ATTESTATION_FILE_NAME]: sszTypesFor(fork).Attestation, [ATTESTER_SLASHING_FILE_NAME]: sszTypesFor(fork).AttesterSlashing, }, @@ -524,6 +612,7 @@ const fastConfirmationTest = const blocks = new Map(); const blobs = new Map(); const columns = new Map(); + const executionPayloadEnvelopes = new Map(); const attestations = new Map(); const attesterSlashings = new Map(); for (const key in t) { @@ -541,6 +630,10 @@ const fastConfirmationTest = if (columnMatch) { columns.set(key, t[key]); } + const envelopeMatch = key.match(EXECUTION_PAYLOAD_ENVELOPE_FILE_NAME); + if (envelopeMatch) { + executionPayloadEnvelopes.set(key, t[key]); + } const attMatch = key.match(ATTESTATION_FILE_NAME); if (attMatch) { attestations.set(key, t[key]); @@ -558,6 +651,7 @@ const fastConfirmationTest = blocks, blobs, columns, + executionPayloadEnvelopes, attestations, attesterSlashings, }; @@ -578,7 +672,14 @@ const fastConfirmationTest = // and these tests are failing until we update our implementation. name.includes("voting_source_beyond_two_epoch") || name.includes("justified_update_always_if_better") || - name.includes("justified_update_not_realized_finality"), + name.includes("justified_update_not_realized_finality") || + // TODO: lodestar's fast-confirmation rule (FCR) needs a broader overhaul; the whole + // fast_confirmation area is expected to be reworked. These two electra is_one_confirmed + // cases (new in v1.7.0-alpha.12) currently fail: getConfirmedRoot diverges for a slashed + // large validator, and the recently-activated-validator-in-empty-slot case hits + // INVALID_STATE_ROOT. Unskip once the FCR is reworked. + name.includes("is_one_confirmed_fails_large_validator_slashed") || + name.includes("is_one_confirmed_fails_recently_activated_validator_voting_in_empty_slot"), }, }; }; @@ -590,7 +691,7 @@ function toSpecTestCheckpoint(checkpoint: CheckpointWithHex): SpecTestCheckpoint }; } -type Step = OnTick | OnAttestation | OnAttesterSlashing | OnBlock | OnPayloadInfo | Checks; +type Step = OnTick | OnAttestation | OnAttesterSlashing | OnBlock | OnExecutionPayloadEnvelope | OnPayloadInfo | Checks; type SpecTestCheckpoint = {epoch: bigint; root: string}; @@ -631,6 +732,13 @@ type OnBlock = { valid?: number; }; +type OnExecutionPayloadEnvelope = { + /** the name of the execution_payload_envelope file */ + execution_payload: string; + /** optional, default to `true`. */ + valid?: number; +}; + type OnPayloadInfo = { /** Encoded 32-byte value of payload's block hash. */ block_hash: string; @@ -681,6 +789,7 @@ type FastConfirmationTestCase = { blocks: Map; blobs: Map; columns: Map; + executionPayloadEnvelopes: Map; attestations: Map; attesterSlashings: Map; }; @@ -701,6 +810,10 @@ function isBlock(step: Step): step is OnBlock { return typeof (step as OnBlock).block === "string"; } +function isExecutionPayload(step: Step): step is OnExecutionPayloadEnvelope { + return typeof (step as OnExecutionPayloadEnvelope).execution_payload === "string"; +} + function isOnPayloadInfoStep(step: Step): step is OnPayloadInfo { return typeof (step as OnPayloadInfo).block_hash === "string"; } diff --git a/packages/beacon-node/test/spec/presets/light_client/single_merkle_proof.ts b/packages/beacon-node/test/spec/presets/light_client/single_merkle_proof.ts index 68c472639ca8..9f32b78a6235 100644 --- a/packages/beacon-node/test/spec/presets/light_client/single_merkle_proof.ts +++ b/packages/beacon-node/test/spec/presets/light_client/single_merkle_proof.ts @@ -1,6 +1,6 @@ import {expect} from "vitest"; -import {Tree} from "@chainsafe/persistent-merkle-tree"; -import {TreeViewDU, Type} from "@chainsafe/ssz"; +import {type Node, Tree} from "@chainsafe/persistent-merkle-tree"; +import {CompositeType, Type} from "@chainsafe/ssz"; import {ForkName} from "@lodestar/params"; import {InputType} from "@lodestar/spec-test-util"; import {RootHex, ssz} from "@lodestar/types"; @@ -9,8 +9,8 @@ import {TestRunnerFn} from "../../utils/types.js"; // https://github.com/ethereum/consensus-specs/blob/da3f5af919be4abb5a6db5a80b235deb8b4b5cba/tests/formats/light_client/single_merkle_proof.md type SingleMerkleProofTestCase = { - meta?: any; - object: TreeViewDU; + meta?: unknown; + object: TreeBackedObject | unknown; // leaf: Bytes32 # string, hex encoded, with 0x prefix // leaf_index: int # integer, decimal // branch: list of Bytes32 # list, each element is a string, hex encoded, with 0x prefix @@ -29,7 +29,11 @@ export const singleMerkleProof: TestRunnerFn { // Assert correct proof generation - const branch = new Tree(testcase.object.node).getSingleProof(testcase.proof.leaf_index); + const objectType = getObjectType(fork, testSuite); + const node = isTreeBackedObject(testcase.object) + ? testcase.object.node + : toCompositeType(objectType).toViewDU(testcase.object).node; + const branch = new Tree(node).getSingleProof(testcase.proof.leaf_index); return branch.map(toHex); }, options: { @@ -59,3 +63,15 @@ function getObjectType(fork: ForkName, objectName: string): Type { throw Error(`Unknown objectName ${objectName}`); } } + +type TreeBackedObject = { + node: Node; +}; + +function isTreeBackedObject(object: unknown): object is TreeBackedObject { + return typeof object === "object" && object !== null && "node" in object; +} + +function toCompositeType(type: Type): CompositeType { + return type as CompositeType; +} diff --git a/packages/beacon-node/test/spec/presets/sanity.test.ts b/packages/beacon-node/test/spec/presets/sanity.test.ts index 1d35ab48ecf8..a714ca830032 100644 --- a/packages/beacon-node/test/spec/presets/sanity.test.ts +++ b/packages/beacon-node/test/spec/presets/sanity.test.ts @@ -1,6 +1,6 @@ import path from "node:path"; import {getConfig} from "@lodestar/config/test-utils"; -import {ACTIVE_PRESET, ForkName} from "@lodestar/params"; +import {ACTIVE_PRESET, ForkName, PresetName} from "@lodestar/params"; import {InputType} from "@lodestar/spec-test-util"; import { BeaconStateAllForks, @@ -30,6 +30,8 @@ const sanity: TestRunnerFn = (fork, testName, testSuit }; const sanitySlots: TestRunnerFn = (fork) => { + const timeout = fork === ForkName.gloas && ACTIVE_PRESET === PresetName.mainnet ? 60000 : 30000; + return { testFunction: (testcase) => { const stateTB = testcase.pre.clone(); @@ -46,7 +48,7 @@ const sanitySlots: TestRunnerFn = (for post: ssz[fork].BeaconState, }, shouldError: (testCase) => !testCase.post, - timeout: 30000, + timeout, getExpected: (testCase) => testCase.post, expectFunc: (_testCase, expected, actual) => { expectEqualBeaconState(fork, expected, actual); diff --git a/packages/beacon-node/test/spec/utils/expectEqualBeaconState.ts b/packages/beacon-node/test/spec/utils/expectEqualBeaconState.ts index 481f7891a6cb..128db7801317 100644 --- a/packages/beacon-node/test/spec/utils/expectEqualBeaconState.ts +++ b/packages/beacon-node/test/spec/utils/expectEqualBeaconState.ts @@ -3,6 +3,7 @@ import {ForkAll, ForkName} from "@lodestar/params"; import {InputType} from "@lodestar/spec-test-util"; import {BeaconStateAllForks} from "@lodestar/state-transition"; import {SSZTypesFor, ssz} from "@lodestar/types"; +import {byteArrayEquals, toRootHex} from "@lodestar/utils"; /** Compare each field in BeaconState to help debug failed test easier. */ export function expectEqualBeaconState( @@ -10,13 +11,34 @@ export function expectEqualBeaconState( expectedView: BeaconStateAllForks, actualView: BeaconStateAllForks ): void { - // TODO: Is it cheaper to compare roots? Or maybe the serialized bytes? - const expected = expectedView.toValue(); - const actual = actualView.toValue(); + const expectedRoot = expectedView.hashTreeRoot(); + const actualRoot = actualView.hashTreeRoot(); + if (byteArrayEquals(actualRoot, expectedRoot)) { + return; + } const stateType = ssz[fork].BeaconState as SSZTypesFor; + let expected: ReturnType; + let actual: ReturnType; + try { + expected = expectedView.toValue(); + actual = actualView.toValue(); + } catch { + expect(toRootHex(actualRoot)).toEqualWithMessage(toRootHex(expectedRoot), "Wrong state root"); + return; + } + if (!stateType.equals(actual, expected)) { - expect(stateType.toJson(actual)).toEqualWithMessage(stateType.toJson(expected), "Wrong state"); + let expectedJson: unknown; + let actualJson: unknown; + try { + expectedJson = stateType.toJson(expected); + actualJson = stateType.toJson(actual); + } catch { + expect(toRootHex(actualRoot)).toEqualWithMessage(toRootHex(expectedRoot), "Wrong state root"); + return; + } + expect(actualJson).toEqualWithMessage(expectedJson, "Wrong state"); } } diff --git a/packages/beacon-node/test/spec/utils/replaceUintTypeWithUintBigintType.ts b/packages/beacon-node/test/spec/utils/replaceUintTypeWithUintBigintType.ts index b26c87010557..c49bd56fbd1b 100644 --- a/packages/beacon-node/test/spec/utils/replaceUintTypeWithUintBigintType.ts +++ b/packages/beacon-node/test/spec/utils/replaceUintTypeWithUintBigintType.ts @@ -2,6 +2,9 @@ import { ContainerType, ListBasicType, ListCompositeType, + ProgressiveContainerType, + ProgressiveListBasicType, + ProgressiveListCompositeType, Type, UintBigintType, UintNumberType, @@ -14,7 +17,7 @@ import { * * This mainly entails making sure all numbers are bignumbers */ -export function replaceUintTypeWithUintBigintType>(type: T): T { +export function replaceUintTypeWithUintBigintType>(type: T): T { if (type instanceof UintNumberType && type.byteLength === 8) { return new UintBigintType(type.byteLength) as unknown as T; } @@ -28,6 +31,14 @@ export function replaceUintTypeWithUintBigintType>(type: T): return new ContainerType(fields, type.opts) as unknown as T; } + if (type instanceof ProgressiveContainerType) { + const fields = {...type.fields}; + for (const key of Object.keys(fields) as (keyof typeof fields)[]) { + fields[key] = replaceUintTypeWithUintBigintType(fields[key]); + } + return new ProgressiveContainerType(fields, type.activeFields, type.opts) as unknown as T; + } + // For List or vectors replace the subType if (type instanceof ListBasicType) { return new ListBasicType(replaceUintTypeWithUintBigintType(type.elementType), type.limit) as unknown as T; @@ -38,6 +49,16 @@ export function replaceUintTypeWithUintBigintType>(type: T): if (type instanceof ListCompositeType) { return new ListCompositeType(replaceUintTypeWithUintBigintType(type.elementType), type.limit) as unknown as T; } + if (type instanceof ProgressiveListBasicType) { + return new ProgressiveListBasicType(replaceUintTypeWithUintBigintType(type.elementType), { + typeName: type.typeName, + }) as unknown as T; + } + if (type instanceof ProgressiveListCompositeType) { + return new ProgressiveListCompositeType(replaceUintTypeWithUintBigintType(type.elementType), { + typeName: type.typeName, + }) as unknown as T; + } if (type instanceof VectorCompositeType) { return new VectorCompositeType(replaceUintTypeWithUintBigintType(type.elementType), type.length) as unknown as T; } diff --git a/packages/beacon-node/test/spec/utils/specTestIterator.ts b/packages/beacon-node/test/spec/utils/specTestIterator.ts index 66592383c4d5..5797079c94de 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -75,11 +75,14 @@ export const defaultSkipOpts: SkipOpts = { /^.+\/light_client\/data_collection\/.*/, // Ignore the partial data column container additions for now. Unskip them when // cell level DAS is ready - /^fulu\/ssz_static\/PartialDataColumn(Header|PartsMetadata|Sidecar)\/.*$/, + /^fulu\/ssz_static\/PartialDataColumn(GroupID|Header|PartsMetadata|Sidecar)\/.*$/, /^gloas\/ssz_static\/PartialDataColumn(GroupID|PartsMetadata|Sidecar)\/.*$/, - // TODO-GLOAS: re-enable after Gloas light client is implemented - /^gloas\/light_client\/.*/, - /^gloas\/ssz_static\/LightClient(Bootstrap|FinalityUpdate|Header|OptimisticUpdate|Update)\/.*/, + // TODO-GLOAS: re-enable after Gloas light-client sync deserializes updates by fork digest. + /^gloas\/light_client\/sync\/.*/, + // TODO-GLOAS: re-enable after on_payload_attestation_message (PTC) fork choice is implemented. + // New test suite added in v1.7.0-alpha.8 (consensus-specs #5206); gloas PTC fork choice + // handling is not yet implemented in Lodestar. + /^gloas\/fork_choice\/on_payload_attestation_message\/.*$/, ], skippedTests: [ // TODO-GLOAS: re-enable after gloas light client is implemented diff --git a/packages/beacon-node/test/unit/api/impl/validator/produceBlockV4.test.ts b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV4.test.ts new file mode 100644 index 000000000000..10b46d3f189a --- /dev/null +++ b/packages/beacon-node/test/unit/api/impl/validator/produceBlockV4.test.ts @@ -0,0 +1,110 @@ +import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"; +import {createBeaconConfig, createChainForkConfig, defaultChainConfig} from "@lodestar/config"; +import {ProtoBlock} from "@lodestar/fork-choice"; +import {ForkName} from "@lodestar/params"; +import {ssz} from "@lodestar/types"; +import {getValidatorApi} from "../../../../../src/api/impl/validator/index.js"; +import {defaultApiOptions} from "../../../../../src/api/options.js"; +import {SyncState} from "../../../../../src/sync/interface.js"; +import {ApiTestModules, getApiTestModules} from "../../../../utils/api.js"; +import {zeroProtoBlock} from "../../../../utils/state.js"; + +describe("api/validator - produceBlockV4", () => { + let modules: ApiTestModules; + let api: ReturnType; + + const chainConfig = createChainForkConfig({ + ...defaultChainConfig, + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + FULU_FORK_EPOCH: 0, + GLOAS_FORK_EPOCH: 0, + }); + const genesisValidatorsRoot = Buffer.alloc(32, 0xaa); + const config = createBeaconConfig(chainConfig, genesisValidatorsRoot); + + const slot = 1; + const feeRecipient = "0xccccccccccccccccccccccccccccccccccccccaa"; + const graffiti = "a".repeat(32); + + const engineBlock = ssz.gloas.BeaconBlock.defaultValue(); + engineBlock.slot = slot; + engineBlock.proposerIndex = 1; + const bidBlock = ssz.gloas.BeaconBlock.defaultValue(); + bidBlock.slot = slot; + bidBlock.proposerIndex = 2; + + const builderBid = ssz.gloas.SignedExecutionPayloadBid.defaultValue(); + builderBid.message.value = 1; + builderBid.message.builderIndex = 42; + + const parentBlock = { + blockRoot: "0x1111111111111111111111111111111111111111111111111111111111111111", + slot: slot - 1, + executionPayloadBlockHash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + parentBlockHash: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + } as ProtoBlock; + + const randaoReveal = engineBlock.body.randaoReveal; + + beforeEach(() => { + modules = getApiTestModules({config}); + api = getValidatorApi(defaultApiOptions, {...modules, config}); + + vi.spyOn(modules.chain.clock, "currentSlot", "get").mockReturnValue(slot); + vi.spyOn(modules.sync, "state", "get").mockReturnValue(SyncState.Synced); + modules.chain.getProposerHead.mockReturnValue(parentBlock); + modules.chain.forkChoice.getBlockDefaultStatus.mockReturnValue(zeroProtoBlock); + modules.chain.forkChoice.shouldBuildOnFull.mockReturnValue(true); + modules.chain.produceBlock.mockImplementation(async (attrs: {builderBid?: unknown}) => ({ + block: attrs.builderBid !== undefined ? bidBlock : engineBlock, + executionPayloadValue: BigInt(0), + consensusBlockValue: BigInt(0), + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("prefers builder bid block when a bid is available", async () => { + modules.chain.builderCircuitBreaker.isActive.mockReturnValue(false); + modules.chain.executionPayloadBidPool.getBestBid.mockReturnValue(builderBid); + + const {data: block, meta} = await api.produceBlockV4({slot, randaoReveal, graffiti, feeRecipient}); + + expect(modules.chain.executionPayloadBidPool.getBestBid).toHaveBeenCalledWith( + slot, + parentBlock.executionPayloadBlockHash, + parentBlock.blockRoot + ); + expect(modules.chain.produceBlock).toHaveBeenCalledTimes(2); + expect(block).toEqual(bidBlock); + expect(meta.version).toBe(ForkName.gloas); + }); + + it("produces local block when no bid is available", async () => { + modules.chain.builderCircuitBreaker.isActive.mockReturnValue(false); + modules.chain.executionPayloadBidPool.getBestBid.mockReturnValue(null); + + const {data: block} = await api.produceBlockV4({slot, randaoReveal, graffiti, feeRecipient}); + + expect(modules.chain.produceBlock).toHaveBeenCalledTimes(1); + expect(block).toEqual(engineBlock); + }); + + it("ignores builder bids when the builder circuit breaker is active", async () => { + modules.chain.builderCircuitBreaker.isActive.mockReturnValue(true); + modules.chain.executionPayloadBidPool.getBestBid.mockReturnValue(builderBid); + + const {data: block} = await api.produceBlockV4({slot, randaoReveal, graffiti, feeRecipient}); + + expect(modules.chain.builderCircuitBreaker.isActive).toHaveBeenCalledWith(slot); + expect(modules.chain.executionPayloadBidPool.getBestBid).not.toHaveBeenCalled(); + expect(modules.chain.produceBlock).toHaveBeenCalledTimes(1); + expect(block).toEqual(engineBlock); + }); +}); diff --git a/packages/beacon-node/test/unit/chain/builderCircuitBreaker.test.ts b/packages/beacon-node/test/unit/chain/builderCircuitBreaker.test.ts new file mode 100644 index 000000000000..a6d1ccbd928d --- /dev/null +++ b/packages/beacon-node/test/unit/chain/builderCircuitBreaker.test.ts @@ -0,0 +1,55 @@ +import {describe, expect, it, vi} from "vitest"; +import {IForkChoice} from "@lodestar/fork-choice"; +import {testLogger} from "@lodestar/logger/test-utils"; +import {BuilderCircuitBreaker} from "../../../src/chain/builderCircuitBreaker.js"; + +describe("BuilderCircuitBreaker", () => { + const faultInspectionWindow = 32; + const allowedFaults = 8; + const logger = testLogger("builderCircuitBreaker"); + + function setup(stats: {blocksPresent: number; payloadsRevealed: number}) { + const getPayloadRevealCounts = vi.fn().mockReturnValue(stats); + const forkChoice = {getPayloadRevealCounts} as unknown as IForkChoice; + const breaker = new BuilderCircuitBreaker( + {faultInspectionWindow, allowedFaults}, + {forkChoice, logger, metrics: null} + ); + return {breaker, getPayloadRevealCounts}; + } + + const testCases: [string, {blocksPresent: number; payloadsRevealed: number}, boolean][] = [ + ["empty window", {blocksPresent: 0, payloadsRevealed: 0}, false], + ["full window, no faults", {blocksPresent: 32, payloadsRevealed: 32}, false], + ["full window, faults at budget", {blocksPresent: 32, payloadsRevealed: 24}, false], + ["full window, faults above budget", {blocksPresent: 32, payloadsRevealed: 23}, true], + ["sparse window, faults within scaled budget", {blocksPresent: 8, payloadsRevealed: 6}, false], + ["sparse window, faults above scaled budget", {blocksPresent: 8, payloadsRevealed: 5}, true], + ["sparse window, all payloads unrevealed", {blocksPresent: 4, payloadsRevealed: 0}, true], + ]; + + for (const [name, stats, expected] of testCases) { + it(`${name} - active=${expected}`, () => { + const {breaker} = setup(stats); + expect(breaker.isActive(100)).toBe(expected); + }); + } + + it("inspects the window excluding the current slot", () => { + const {breaker, getPayloadRevealCounts} = setup({blocksPresent: 32, payloadsRevealed: 32}); + breaker.isActive(100); + expect(getPayloadRevealCounts).toHaveBeenCalledWith(100 - faultInspectionWindow, 99); + }); + + it("only updates once per slot", () => { + const {breaker, getPayloadRevealCounts} = setup({blocksPresent: 32, payloadsRevealed: 32}); + expect(breaker.isActive(100)).toBe(false); + + getPayloadRevealCounts.mockReturnValue({blocksPresent: 32, payloadsRevealed: 0}); + expect(breaker.isActive(100)).toBe(false); + expect(getPayloadRevealCounts).toHaveBeenCalledTimes(1); + + expect(breaker.isActive(101)).toBe(true); + expect(getPayloadRevealCounts).toHaveBeenCalledTimes(2); + }); +}); diff --git a/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts b/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts index 5358137b0af6..cd245091889f 100644 --- a/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts +++ b/packages/beacon-node/test/unit/chain/lightclient/proof.test.ts @@ -1,23 +1,34 @@ import {beforeAll, describe, expect, it} from "vitest"; -import {ForkName, SYNC_COMMITTEE_SIZE} from "@lodestar/params"; -import {BeaconStateAltair, BeaconStateElectra} from "@lodestar/state-transition"; +import { + CURRENT_SYNC_COMMITTEE_GINDEX, + CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA, + CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS, + ForkName, + NEXT_SYNC_COMMITTEE_GINDEX, + NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA, + NEXT_SYNC_COMMITTEE_GINDEX_GLOAS, + SYNC_COMMITTEE_SIZE, +} from "@lodestar/params"; +import {BeaconStateAltair, BeaconStateElectra, BeaconStateGloas} from "@lodestar/state-transition"; import {altair, ssz} from "@lodestar/types"; import {hash, verifyMerkleBranch} from "@lodestar/utils"; -import {getNextSyncCommitteeBranch, getSyncCommitteesWitness} from "../../../../src/chain/lightClient/proofs.js"; +import { + getCurrentSyncCommitteeBranch, + getNextSyncCommitteeBranch, + getSyncCommitteesWitness, +} from "../../../../src/chain/lightClient/proofs.js"; import {NUM_WITNESS, NUM_WITNESS_ELECTRA} from "../../../../src/db/repositories/lightclientSyncCommitteeWitness.js"; -const currentSyncCommitteeGindex = 54; -const nextSyncCommitteeGindex = 55; const syncCommitteesGindex = 27; -const currentSyncCommitteeGindexElectra = 86; -const nextSyncCommitteeGindexElectra = 87; const syncCommitteesGindexElectra = 43; describe("chain / lightclient / proof", () => { let stateAltair: BeaconStateAltair; let stateElectra: BeaconStateElectra; + let stateGloas: BeaconStateGloas; let stateRootAltair: Uint8Array; let stateRootElectra: Uint8Array; + let stateRootGloas: Uint8Array; const currentSyncCommittee = fillSyncCommittee(Buffer.alloc(48, 0xbb)); const nextSyncCommittee = fillSyncCommittee(Buffer.alloc(48, 0xcc)); @@ -33,6 +44,11 @@ describe("chain / lightclient / proof", () => { stateElectra.currentSyncCommittee = ssz.altair.SyncCommittee.toViewDU(currentSyncCommittee); stateElectra.nextSyncCommittee = ssz.altair.SyncCommittee.toViewDU(nextSyncCommittee); stateRootElectra = stateElectra.hashTreeRoot(); + + stateGloas = ssz.gloas.BeaconState.defaultViewDU(); + stateGloas.currentSyncCommittee = ssz.altair.SyncCommittee.toViewDU(currentSyncCommittee); + stateGloas.nextSyncCommittee = ssz.altair.SyncCommittee.toViewDU(nextSyncCommittee); + stateRootGloas = stateGloas.hashTreeRoot(); }); it("SyncCommittees proof altair", () => { @@ -62,7 +78,7 @@ describe("chain / lightclient / proof", () => { verifyMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(currentSyncCommittee), currentSyncCommitteeBranch, - ...fromGindex(currentSyncCommitteeGindex), + ...fromGindex(CURRENT_SYNC_COMMITTEE_GINDEX), stateRootAltair ) ).toBe(true); @@ -77,7 +93,7 @@ describe("chain / lightclient / proof", () => { verifyMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(nextSyncCommittee), nextSyncCommitteeBranch, - ...fromGindex(nextSyncCommitteeGindex), + ...fromGindex(NEXT_SYNC_COMMITTEE_GINDEX), stateRootAltair ) ).toBe(true); @@ -110,7 +126,7 @@ describe("chain / lightclient / proof", () => { verifyMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(currentSyncCommittee), currentSyncCommitteeBranch, - ...fromGindex(currentSyncCommitteeGindexElectra), + ...fromGindex(CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA), stateRootElectra ) ).toBe(true); @@ -124,7 +140,7 @@ describe("chain / lightclient / proof", () => { verifyMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(nextSyncCommittee), nextSyncCommitteeBranch, - ...fromGindex(nextSyncCommitteeGindexElectra), + ...fromGindex(NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA), stateRootElectra ) ).toBe(true); @@ -141,6 +157,36 @@ describe("chain / lightclient / proof", () => { expect(syncCommitteesWitness.witness.length).toBe(NUM_WITNESS_ELECTRA); }); + + it("currentSyncCommittee proof gloas", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.gloas, stateGloas); + const currentSyncCommitteeBranch = getCurrentSyncCommitteeBranch(syncCommitteesWitness); + + expect(syncCommitteesWitness.witness.length).toBe(0); + expect( + verifyMerkleBranch( + ssz.altair.SyncCommittee.hashTreeRoot(currentSyncCommittee), + currentSyncCommitteeBranch, + ...fromGindex(CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS), + stateRootGloas + ) + ).toBe(true); + }); + + it("nextSyncCommittee proof gloas", () => { + const syncCommitteesWitness = getSyncCommitteesWitness(ForkName.gloas, stateGloas); + const nextSyncCommitteeBranch = getNextSyncCommitteeBranch(syncCommitteesWitness); + + expect(syncCommitteesWitness.witness.length).toBe(0); + expect( + verifyMerkleBranch( + ssz.altair.SyncCommittee.hashTreeRoot(nextSyncCommittee), + nextSyncCommitteeBranch, + ...fromGindex(NEXT_SYNC_COMMITTEE_GINDEX_GLOAS), + stateRootGloas + ) + ).toBe(true); + }); }); function fillSyncCommittee(pubkey: Uint8Array): altair.SyncCommittee { diff --git a/packages/beacon-node/test/unit/chain/lightclient/upgradeLightClientHeader.test.ts b/packages/beacon-node/test/unit/chain/lightclient/upgradeLightClientHeader.test.ts index a455f97c50ef..6e6653ba912e 100644 --- a/packages/beacon-node/test/unit/chain/lightclient/upgradeLightClientHeader.test.ts +++ b/packages/beacon-node/test/unit/chain/lightclient/upgradeLightClientHeader.test.ts @@ -1,7 +1,8 @@ import {beforeEach, describe, expect, it} from "vitest"; +import {Tree} from "@chainsafe/persistent-merkle-tree"; import {createBeaconConfig, createChainForkConfig, defaultChainConfig} from "@lodestar/config"; -import {ForkName, ForkSeq} from "@lodestar/params"; -import {upgradeLightClientHeader} from "@lodestar/state-transition/light-client"; +import {EXECUTION_BLOCK_HASH_GINDEX_GLOAS, ForkName, ForkSeq} from "@lodestar/params"; +import {normalizeMerkleBranch, upgradeLightClientHeader} from "@lodestar/state-transition/light-client"; import {LightClientHeader, ssz} from "@lodestar/types"; describe("UpgradeLightClientHeader", () => { @@ -31,7 +32,7 @@ describe("UpgradeLightClientHeader", () => { deneb: ssz.deneb.LightClientHeader.defaultValue(), electra: ssz.deneb.LightClientHeader.defaultValue(), fulu: ssz.deneb.LightClientHeader.defaultValue(), - gloas: ssz.deneb.LightClientHeader.defaultValue(), + gloas: ssz.gloas.LightClientHeader.defaultValue(), }; testSlots = { @@ -55,8 +56,12 @@ describe("UpgradeLightClientHeader", () => { lcHeaderByFork[fromFork].beacon.slot = testSlots[fromFork]; lcHeaderByFork[toFork].beacon.slot = testSlots[fromFork]; + const expectedHeader = + toFork === ForkName.gloas + ? getExpectedGloasHeader(fromFork, lcHeaderByFork[fromFork]) + : lcHeaderByFork[toFork]; const updatedHeader = upgradeLightClientHeader(config, toFork, lcHeaderByFork[fromFork]); - expect(updatedHeader).toEqual(lcHeaderByFork[toFork]); + expect(updatedHeader).toEqual(expectedHeader); }); } } @@ -77,3 +82,44 @@ describe("UpgradeLightClientHeader", () => { } } }); + +function getExpectedGloasHeader(fromFork: ForkName, header: LightClientHeader): LightClientHeader { + if (ForkSeq[fromFork] < ForkSeq.capella) { + return { + ...ssz.gloas.LightClientHeader.defaultValue(), + beacon: header.beacon, + }; + } + + if (ForkSeq[fromFork] >= ForkSeq.deneb) { + const pre = header as LightClientHeader; + const blockHashGindex = ssz.deneb.ExecutionPayloadHeader.getPathInfo(["blockHash"]).gindex; + const executionBranch = new Tree(ssz.deneb.ExecutionPayloadHeader.toView(pre.execution).node).getSingleProof( + blockHashGindex + ); + + return { + beacon: pre.beacon, + executionBlockHash: pre.execution.blockHash, + executionBranch: normalizeMerkleBranch( + [...executionBranch, ...pre.executionBranch], + EXECUTION_BLOCK_HASH_GINDEX_GLOAS + ), + }; + } + + const pre = header as LightClientHeader; + const blockHashGindex = ssz.capella.ExecutionPayloadHeader.getPathInfo(["blockHash"]).gindex; + const executionBranch = new Tree(ssz.capella.ExecutionPayloadHeader.toView(pre.execution).node).getSingleProof( + blockHashGindex + ); + + return { + beacon: pre.beacon, + executionBlockHash: pre.execution.blockHash, + executionBranch: normalizeMerkleBranch( + [...executionBranch, ...pre.executionBranch], + EXECUTION_BLOCK_HASH_GINDEX_GLOAS + ), + }; +} diff --git a/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts b/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts index 6cf39e084adf..a69d948872b9 100644 --- a/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts +++ b/packages/beacon-node/test/unit/chain/validation/aggregateAndProof.test.ts @@ -1,11 +1,15 @@ -import {describe, it} from "vitest"; +import {describe, expect, it} from "vitest"; import {BitArray, toHexString} from "@chainsafe/ssz"; -import {SLOTS_PER_EPOCH} from "@lodestar/params"; +import {createBeaconConfig} from "@lodestar/config"; +import {config as defaultConfig} from "@lodestar/config/default"; +import {DOMAIN_AGGREGATE_AND_PROOF, ForkName, SLOTS_PER_EPOCH, ZERO_HASH} from "@lodestar/params"; +import {computeSigningRoot} from "@lodestar/state-transition"; import {generateTestCachedBeaconStateOnlyValidators} from "@lodestar/state-transition/test-utils"; import {phase0, ssz} from "@lodestar/types"; import {AttestationErrorCode} from "../../../../src/chain/errors/index.js"; import {IBeaconChain} from "../../../../src/chain/index.js"; import {validateApiAggregateAndProof, validateGossipAggregateAndProof} from "../../../../src/chain/validation/index.js"; +import {getAggregateAndProofSigningRoot} from "../../../../src/chain/validation/signatureSets/index.js"; import {memoOnce} from "../../../utils/cache.js"; import {expectRejectedWithLodestarError} from "../../../utils/errors.js"; import { @@ -177,6 +181,25 @@ describe("chain / validation / aggregateAndProof", () => { await expectError(chain, signedAggregateAndProof, AttestationErrorCode.INVALID_SIGNATURE); }); + it("uses the fork-specific AggregateAndProof signing root", () => { + const config = createBeaconConfig({...defaultConfig, FULU_FORK_EPOCH: 0, GLOAS_FORK_EPOCH: 1}, ZERO_HASH); + const slot = SLOTS_PER_EPOCH; + const signedAggregateAndProof = ssz.gloas.SignedAggregateAndProof.defaultValue(); + signedAggregateAndProof.message.aggregatorIndex = 1; + signedAggregateAndProof.message.selectionProof[0] = 1; + signedAggregateAndProof.message.aggregate.data.slot = slot; + signedAggregateAndProof.message.aggregate.data.target.epoch = 1; + signedAggregateAndProof.message.aggregate.signature[0] = 2; + signedAggregateAndProof.message.aggregate.aggregationBits = BitArray.fromSingleBit(4, 0); + signedAggregateAndProof.message.aggregate.committeeBits.set(0, true); + + const domain = config.getDomainAtFork(ForkName.gloas, DOMAIN_AGGREGATE_AND_PROOF); + const expectedRoot = computeSigningRoot(ssz.gloas.AggregateAndProof, signedAggregateAndProof.message, domain); + + expect(config.getForkTypes(slot).AggregateAndProof).toBe(ssz.gloas.AggregateAndProof); + expect(getAggregateAndProofSigningRoot(config, 1, signedAggregateAndProof)).toEqual(expectedRoot); + }); + /** Alias to reduce code duplication */ async function expectError( chain: IBeaconChain, diff --git a/packages/beacon-node/test/unit/execution/engine/types.test.ts b/packages/beacon-node/test/unit/execution/engine/types.test.ts index f8a8a91b15f8..99e638feb538 100644 --- a/packages/beacon-node/test/unit/execution/engine/types.test.ts +++ b/packages/beacon-node/test/unit/execution/engine/types.test.ts @@ -5,6 +5,7 @@ import { CONSOLIDATION_REQUEST_TYPE, DEPOSIT_REQUEST_TYPE, ForkName, + MAX_DEPOSIT_REQUESTS_PER_PAYLOAD, WITHDRAWAL_REQUEST_TYPE, } from "@lodestar/params"; import {ExecutionRequests, gloas, ssz} from "@lodestar/types"; @@ -75,6 +76,24 @@ describe("execution / engine / types", () => { expect(serialized.length).toBe(0); }); + it("should serialize Gloas deposit requests beyond the Electra limit", () => { + const expectedLength = MAX_DEPOSIT_REQUESTS_PER_PAYLOAD + 1; + const executionRequests: gloas.ExecutionRequests = { + deposits: Array.from({length: expectedLength}, () => ssz.gloas.DepositRequest.defaultValue()), + withdrawals: [], + consolidations: [], + builderDeposits: [], + builderExits: [], + }; + + const serialized = serializeExecutionRequests(ForkName.gloas, executionRequests); + const deserialized = deserializeExecutionRequests(ForkName.gloas, serialized) as gloas.ExecutionRequests; + + expect(serialized.length).toBe(1); + expect(deserialized.deposits.length).toBe(expectedLength); + expect(serializeExecutionRequests(ForkName.gloas, deserialized)).toEqual(serialized); + }); + it("should serialize builder requests post-gloas", () => { const executionRequests: gloas.ExecutionRequests = { deposits: [], diff --git a/packages/beacon-node/test/unit/network/gossip/topic.test.ts b/packages/beacon-node/test/unit/network/gossip/topic.test.ts index 75cda22e29f7..88537cadd88c 100644 --- a/packages/beacon-node/test/unit/network/gossip/topic.test.ts +++ b/packages/beacon-node/test/unit/network/gossip/topic.test.ts @@ -1,11 +1,24 @@ import {describe, expect, it} from "vitest"; import {createBeaconConfig} from "@lodestar/config"; import {config as chainConfig} from "@lodestar/config/default"; -import {ATTESTATION_SUBNET_COUNT, ForkName, GENESIS_EPOCH, ZERO_HASH} from "@lodestar/params"; +import { + ATTESTATION_SUBNET_COUNT, + ForkName, + GENESIS_EPOCH, + MAX_ATTESTER_SLASHING_SIZE, + MAX_DATA_COLUMN_SIDECAR_SIZE, + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE, + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE, + ZERO_HASH, +} from "@lodestar/params"; +import {DataTransformSnappy} from "../../../../src/network/gossip/encoding.js"; import {GossipEncoding, GossipTopicMap, GossipType} from "../../../../src/network/gossip/index.js"; import { + GossipTopicCache, getAllowedTopics, getCoreTopicsAtFork, + getGossipSSZMaxSize, + getGossipSSZType, parseGossipTopic, stringifyGossipTopic, } from "../../../../src/network/gossip/topic.js"; @@ -226,6 +239,80 @@ describe("network / gossip / topic", () => { }); } + it("should provide finite gossip size limits for every gossip type", () => { + for (const {topic} of Object.values(testCases).flat()) { + const maxSize = getGossipSSZMaxSize(topic, config.MAX_PAYLOAD_SIZE); + + expect(Number.isFinite(maxSize)).toBe(true); + expect(maxSize).toBeGreaterThanOrEqual(getGossipSSZType(topic).minSize); + } + }); + + it("should use preset-defined gossip size limits for Gloas progressive objects", () => { + const boundary = {fork: ForkName.gloas, epoch: config.GLOAS_FORK_EPOCH}; + + expect({ + [GossipType.beacon_block]: getGossipSSZMaxSize( + {type: GossipType.beacon_block, boundary, encoding}, + config.MAX_PAYLOAD_SIZE + ), + [GossipType.data_column_sidecar]: getGossipSSZMaxSize( + {type: GossipType.data_column_sidecar, boundary, subnet: 1, encoding}, + config.MAX_PAYLOAD_SIZE + ), + [GossipType.beacon_aggregate_and_proof]: getGossipSSZMaxSize( + {type: GossipType.beacon_aggregate_and_proof, boundary, encoding}, + config.MAX_PAYLOAD_SIZE + ), + [GossipType.attester_slashing]: getGossipSSZMaxSize( + {type: GossipType.attester_slashing, boundary, encoding}, + config.MAX_PAYLOAD_SIZE + ), + [GossipType.execution_payload_bid]: getGossipSSZMaxSize( + {type: GossipType.execution_payload_bid, boundary, encoding}, + config.MAX_PAYLOAD_SIZE + ), + }).toEqual({ + [GossipType.beacon_block]: config.MAX_PAYLOAD_SIZE, + [GossipType.data_column_sidecar]: MAX_DATA_COLUMN_SIDECAR_SIZE, + [GossipType.beacon_aggregate_and_proof]: MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE, + [GossipType.attester_slashing]: MAX_ATTESTER_SLASHING_SIZE, + [GossipType.execution_payload_bid]: MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE, + }); + }); + + it("should cap Gloas progressive gossip objects below their theoretical SSZ max", () => { + const boundary = {fork: ForkName.gloas, epoch: config.GLOAS_FORK_EPOCH}; + + for (const topic of [ + {type: GossipType.beacon_block, boundary, encoding}, + {type: GossipType.beacon_aggregate_and_proof, boundary, encoding}, + {type: GossipType.attester_slashing, boundary, encoding}, + {type: GossipType.execution_payload, boundary, encoding}, + {type: GossipType.execution_payload_bid, boundary, encoding}, + {type: GossipType.data_column_sidecar, boundary, subnet: 1, encoding}, + ] as const) { + expect(getGossipSSZMaxSize(topic, config.MAX_PAYLOAD_SIZE)).toBeLessThan(getGossipSSZType(topic).maxSize); + } + }); + + it("should reject gossip bytes above the per-topic limit before outbound compression", () => { + const topic = { + type: GossipType.beacon_aggregate_and_proof, + boundary: {fork: ForkName.gloas, epoch: config.GLOAS_FORK_EPOCH}, + encoding, + } as const; + const topicStr = stringifyGossipTopic(config, topic); + const gossipTopicCache = new GossipTopicCache(config); + const transform = new DataTransformSnappy(gossipTopicCache, config.MAX_PAYLOAD_SIZE, null); + + gossipTopicCache.setTopic(topicStr, topic); + + expect(() => + transform.outboundTransform(topicStr, new Uint8Array(MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE + 1)) + ).toThrow(`ssz_snappy encoded data length ${MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE + 1}`); + }); + describe("getAllowedTopics", () => { // A config with every fork scheduled so all fork boundaries (and their topics) are present const allForksConfig = createBeaconConfig( diff --git a/packages/beacon-node/test/unit/network/reqresp/rateLimit.test.ts b/packages/beacon-node/test/unit/network/reqresp/rateLimit.test.ts new file mode 100644 index 000000000000..ff799c1d1ca9 --- /dev/null +++ b/packages/beacon-node/test/unit/network/reqresp/rateLimit.test.ts @@ -0,0 +1,16 @@ +import {describe, expect, it} from "vitest"; +import {createBeaconConfig} from "@lodestar/config"; +import {getConfig} from "@lodestar/config/test-utils"; +import {ForkName} from "@lodestar/params"; +import {ZERO_HASH} from "../../../../src/constants/index.js"; +import {rateLimitQuotas} from "../../../../src/network/reqresp/rateLimit.js"; +import {ReqRespMethod} from "../../../../src/network/reqresp/types.js"; + +describe("network / reqresp / rateLimitQuotas", () => { + it("uses MAX_REQUEST_PAYLOADS for ExecutionPayloadEnvelopesByRange", () => { + const config = createBeaconConfig(getConfig(ForkName.gloas), ZERO_HASH); + const quotas = rateLimitQuotas(ForkName.gloas, config); + + expect(quotas[ReqRespMethod.ExecutionPayloadEnvelopesByRange].byPeer?.quota).toBe(config.MAX_REQUEST_PAYLOADS); + }); +}); diff --git a/packages/beacon-node/test/unit/sync/range/chain.test.ts b/packages/beacon-node/test/unit/sync/range/chain.test.ts index 9bbbad2a887f..f38453fe38c8 100644 --- a/packages/beacon-node/test/unit/sync/range/chain.test.ts +++ b/packages/beacon-node/test/unit/sync/range/chain.test.ts @@ -31,14 +31,23 @@ describe("sync / range / chain", () => { targetEpoch: 16, }, { - // due to BATCH_BUFFER_SIZE and MAX_LOOK_AHEAD_EPOCHS, lodestar cannot deal with unlimited skipped slots - // having a test with 2 epochs of skipped slots is enough to test the logic - // this hasn't happened in any networks as of Aug 2025 + // A run of skipped slots that fits within the MAX_LOOK_AHEAD_EPOCHS window. The longer-run case + // (a run of empty epochs exceeding the look-ahead window, which previously deadlocked) is + // covered by "Simulate sync with 3 epochs of skipped slots" below. id: "Simulate sync with 2 epochs of skipped slots", startEpoch: 0, targetEpoch: 16, skippedSlots: new Set(linspace(3 * SLOTS_PER_EPOCH, 4 * SLOTS_PER_EPOCH)), }, + { + // Regression: a run of empty epochs longer than MAX_LOOK_AHEAD_EPOCHS must not deadlock the + // chain. Empty batches sit in AwaitingValidation and never advance lastEpochWithProcessBlocks, + // so the look-ahead window must not count them or no further batches are ever downloaded. + id: "Simulate sync with 3 epochs of skipped slots", + startEpoch: 0, + targetEpoch: 16, + skippedSlots: new Set(linspace(3 * SLOTS_PER_EPOCH, 6 * SLOTS_PER_EPOCH)), + }, // As of https://github.com/ChainSafe/lodestar/pull/8150, we abort the batch after a single processing error // { // id: "Simulate sync with multiple ranges of bad blocks", diff --git a/packages/beacon-node/test/unit/sync/utils/downloadByRange.test.ts b/packages/beacon-node/test/unit/sync/utils/downloadByRange.test.ts index 29b447d717ce..41e1914960c5 100644 --- a/packages/beacon-node/test/unit/sync/utils/downloadByRange.test.ts +++ b/packages/beacon-node/test/unit/sync/utils/downloadByRange.test.ts @@ -1,16 +1,21 @@ import {beforeEach, describe, expect, it} from "vitest"; -import {ForkName} from "@lodestar/params"; +import {ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; import {BlockInputPreData} from "../../../../src/chain/blocks/blockInput/blockInput.js"; import {BlockInputSource, IBlockInput} from "../../../../src/chain/blocks/blockInput/types.js"; -import {ValidatedBlock, getBlocksForDataValidation} from "../../../../src/sync/utils/downloadByRange.js"; -import {generateChainOfBlockMaybeSidecars} from "../../../utils/blocksAndData.js"; +import { + DownloadByRangeErrorCode, + ValidatedBlock, + getBlocksForDataValidation, + validateBlockByRangeResponse, + validateResponses, +} from "../../../../src/sync/utils/downloadByRange.js"; +import {config, generateChainOfBlockMaybeSidecars, slots} from "../../../utils/blocksAndData.js"; /** * Logic errors and gaps identified during test case creation: * * INSERT_LOGIC_ERROR_BULLET_POINTS_HERE * - * - validateBlockByRangeResponse: Commented out zero blocks check breaks during chain liveness issues (line 445-453) * - validateBlobsByRangeResponse: Missing validation that blob sidecars are in consecutive (slot, index) order as per spec * - validateColumnsByRangeResponse: Missing validation that column sidecars are in consecutive (slot, index) order * - cacheByRangeResponses: Error handling for wrong chain only breaks loop but doesn't throw/propagate error properly @@ -164,6 +169,59 @@ import {generateChainOfBlockMaybeSidecars} from "../../../utils/blocksAndData.js // it("should propagate validation errors from validateBlockDataColumnSidecars"); // }); +describe("validateBlockByRangeResponse", () => { + // Regression: a peer returning zero blocks for an all-skipped epoch is a valid response during + // poor chain liveness. It must be accepted (with a warning), not thrown — otherwise the SyncChain + // dies after MAX_BATCH_DOWNLOAD_ATTEMPTS and the node cannot sync past an empty epoch. + it("should accept empty response during chain liveness issues", () => { + const blocksRequest = {startSlot: slots.deneb, count: SLOTS_PER_EPOCH, step: 1}; + + const {result, warnings} = validateBlockByRangeResponse(config, blocksRequest, []); + + expect(result).toEqual([]); + expect(warnings?.map((w) => w.type.code)).toContain(DownloadByRangeErrorCode.MISSING_BLOCKS_RESPONSE); + }); +}); + +describe("validateResponses", () => { + // Regression: an empty epoch that also carries a data (blobs/columns) request must not throw. + // Both the block validator and the "no blocks in data range" guard previously threw + // MISSING_BLOCKS_RESPONSE for a legitimately empty epoch, killing the SyncChain. + it("should accept an empty epoch when a data request is present", async () => { + const startSlot = slots.deneb; + const blocksRequest = {startSlot, count: SLOTS_PER_EPOCH, step: 1}; + const blobsRequest = {startSlot, count: SLOTS_PER_EPOCH}; + + const {result} = await validateResponses({ + config, + blocksRequest, + blobsRequest, + blocks: [], + blobSidecars: [], + }); + + expect(result.responses.validatedBlocks).toEqual([]); + expect(result.responses.validatedBlobSidecars ?? []).toEqual([]); + }); + + // Characterization test for the no-data-request path (envelopes-only). With no blocks/blobs/columns + // request, data-sidecar validation must be skipped while envelope validation still runs and returns + // a (possibly empty) map — never null. This pins the behavior that the data-validation refactor + // must preserve. + it("validates envelopes and skips data validation when only an envelopes request is present", async () => { + const {result} = await validateResponses({ + config, + envelopesRequest: {startSlot: slots.gloas, count: SLOTS_PER_EPOCH}, + payloadEnvelopes: [], + }); + + expect(result.responses.validatedBlocks).toBeUndefined(); + expect(result.responses.validatedColumnSidecars ?? []).toEqual([]); + expect(result.payloadEnvelopes).toBeInstanceOf(Map); + expect(result.payloadEnvelopes?.size).toBe(0); + }); +}); + describe("getBlocksForDataValidation", () => { const forkName = ForkName.capella; let chainOfBlocks: ReturnType; diff --git a/packages/cli/package.json b/packages/cli/package.json index 137b0a00fdb7..51bc79afeae9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -62,8 +62,8 @@ "@chainsafe/blst": "^2.2.0", "@chainsafe/discv5": "^12.0.1", "@chainsafe/enr": "^6.0.1", - "@chainsafe/persistent-merkle-tree": "^1.2.5", - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/persistent-merkle-tree": "^1.3.0", + "@chainsafe/ssz": "^1.6.2", "@chainsafe/threads": "^1.11.3", "@libp2p/crypto": "^5.1.13", "@libp2p/interface": "^3.1.0", diff --git a/packages/cli/src/options/beaconNodeOptions/builder.ts b/packages/cli/src/options/beaconNodeOptions/builder.ts index 2209250b9c12..85d8e8eab254 100644 --- a/packages/cli/src/options/beaconNodeOptions/builder.ts +++ b/packages/cli/src/options/beaconNodeOptions/builder.ts @@ -51,13 +51,15 @@ export const options: CliCommandOptions = { "builder.faultInspectionWindow": { type: "number", - description: "Window to inspect missed slots for enabling/disabling builder circuit breaker", + description: + "Window to inspect missed slots (pre-gloas) or unrevealed payloads (post-gloas) for enabling/disabling builder circuit breaker", group: "builder", }, "builder.allowedFaults": { type: "number", - description: "Number of missed slots allowed in the `faultInspectionWindow` for builder circuit", + description: + "Number of missed slots (pre-gloas) or unrevealed payloads (post-gloas) allowed in the `faultInspectionWindow` for builder circuit", group: "builder", }, }; diff --git a/packages/config/package.json b/packages/config/package.json index c5e558a15de0..e2357cf469e9 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -66,7 +66,7 @@ ], "dependencies": { "@chainsafe/as-sha256": "^1.2.0", - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@lodestar/params": "workspace:^", "@lodestar/types": "workspace:^", "@lodestar/utils": "workspace:^" diff --git a/packages/config/src/chainConfig/configs/mainnet.ts b/packages/config/src/chainConfig/configs/mainnet.ts index 6025cb88431d..18ee4a91428a 100644 --- a/packages/config/src/chainConfig/configs/mainnet.ts +++ b/packages/config/src/chainConfig/configs/mainnet.ts @@ -70,8 +70,8 @@ export const chainConfig: ChainConfig = { SECONDS_PER_ETH1_BLOCK: 14, // 2**8 (= 256) epochs ~27 hours MIN_VALIDATOR_WITHDRAWABILITY_DELAY: 256, - // 2**13 (= 8,192) epochs ~36 days - MIN_BUILDER_WITHDRAWABILITY_DELAY: 8192, + // 2**6 (= 64) epochs ~6.8 hours + MIN_BUILDER_WITHDRAWABILITY_DELAY: 64, // 2**8 (= 256) epochs ~27 hours SHARD_COMMITTEE_PERIOD: 256, // 2**11 (= 2,048) Eth1 blocks ~8 hours @@ -99,8 +99,8 @@ export const chainConfig: ChainConfig = { CONTRIBUTION_DUE_BPS_GLOAS: 5000, // 75% of SLOT_DURATION_MS PAYLOAD_ATTESTATION_DUE_BPS: 7500, - // 75% of SLOT_DURATION_MS - PAYLOAD_DUE_BPS: 7500, + // 50% of SLOT_DURATION_MS + PAYLOAD_DUE_BPS: 5000, // Validator cycle // --------------------------------------------------------------- diff --git a/packages/config/src/chainConfig/configs/minimal.ts b/packages/config/src/chainConfig/configs/minimal.ts index ed9a3a8e22c1..b28de3a8d630 100644 --- a/packages/config/src/chainConfig/configs/minimal.ts +++ b/packages/config/src/chainConfig/configs/minimal.ts @@ -93,8 +93,8 @@ export const chainConfig: ChainConfig = { CONTRIBUTION_DUE_BPS_GLOAS: 5000, // 75% of SLOT_DURATION_MS PAYLOAD_ATTESTATION_DUE_BPS: 7500, - // 75% of SLOT_DURATION_MS - PAYLOAD_DUE_BPS: 7500, + // 50% of SLOT_DURATION_MS + PAYLOAD_DUE_BPS: 5000, // Validator cycle // --------------------------------------------------------------- diff --git a/packages/db/package.json b/packages/db/package.json index f2105e95c402..cf9112035f0f 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -43,7 +43,7 @@ "check-readme": "pnpm exec ts-node ../../scripts/check_readme.ts" }, "dependencies": { - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@lodestar/config": "workspace:^", "@lodestar/utils": "workspace:^", "classic-level": "^1.4.1" diff --git a/packages/fork-choice/package.json b/packages/fork-choice/package.json index 9c2fc447d7d4..cb68ce2a97cf 100644 --- a/packages/fork-choice/package.json +++ b/packages/fork-choice/package.json @@ -39,7 +39,7 @@ "check-readme": "pnpm exec ts-node ../../scripts/check_readme.ts" }, "dependencies": { - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@lodestar/config": "workspace:^", "@lodestar/params": "workspace:^", "@lodestar/state-transition": "workspace:^", diff --git a/packages/fork-choice/src/forkChoice/forkChoice.ts b/packages/fork-choice/src/forkChoice/forkChoice.ts index 9ea39c53ce29..a5fe279b22c8 100644 --- a/packages/fork-choice/src/forkChoice/forkChoice.ts +++ b/packages/fork-choice/src/forkChoice/forkChoice.ts @@ -589,6 +589,10 @@ export class ForkChoice implements IForkChoice { return this.protoArray.nodes.filter((node) => node.slot > windowStart).length; } + getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} { + return this.protoArray.getPayloadRevealCounts(fromSlot, toSlot); + } + /** Very expensive function, iterates the entire ProtoArray. Called only in debug API */ getHeads(): ProtoBlock[] { return this.protoArray.nodes.filter((node) => node.bestChild === undefined); @@ -1948,13 +1952,6 @@ export class ForkChoice implements IForkChoice { return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.HeadBlockIsTimely}; } - // No reorg if we are at an epoch boundary - // https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.12/specs/phase0/fork-choice.md#is_not_epoch_boundary - const isAtEpochBoundary = slot % SLOTS_PER_EPOCH === 0; - if (isAtEpochBoundary) { - return {prelimProposerHead, prelimNotReorgedReason: NotReorgedReason.AtEpochBoundary}; - } - // No reorg if headBlock and parentBlock are not ffg competitive // https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#is_ffg_competitive const {unrealizedJustifiedEpoch: headBlockCpEpoch, unrealizedJustifiedRoot: headBlockCpRoot} = headBlock; diff --git a/packages/fork-choice/src/forkChoice/interface.ts b/packages/fork-choice/src/forkChoice/interface.ts index 63b5cd983291..c3bc2da07ad8 100644 --- a/packages/fork-choice/src/forkChoice/interface.ts +++ b/packages/fork-choice/src/forkChoice/interface.ts @@ -252,6 +252,11 @@ export interface IForkChoice { hasPayloadUnsafe(blockRoot: Root): boolean; hasPayloadHexUnsafe(blockRoot: RootHex): boolean; getSlotsPresent(windowStart: number): number; + /** + * Count gloas blocks with fromSlot <= slot <= toSlot and how many of them have a revealed + * payload (FULL variant exists). Used by the builder circuit breaker. + */ + getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number}; getPTCVotes(blockRootHex: RootHex): (boolean | null)[] | null; /** Raw PTC vote tallies for the debug fork choice endpoint; `null` for pre-Gloas roots. */ getPTCVoteCounts(blockRootHex: RootHex): { diff --git a/packages/fork-choice/src/protoArray/protoArray.ts b/packages/fork-choice/src/protoArray/protoArray.ts index f2994998e4e8..5ecb122e6970 100644 --- a/packages/fork-choice/src/protoArray/protoArray.ts +++ b/packages/fork-choice/src/protoArray/protoArray.ts @@ -672,6 +672,26 @@ export class ProtoArray { this.maybeUpdateBestChildAndDescendant(pendingIndex, fullIndex, currentSlot, proposerBoostRoot); } + /** + * Count gloas blocks with fromSlot <= slot <= toSlot and how many of them have a revealed + * payload (FULL variant exists). Used by the builder circuit breaker. + */ + getPayloadRevealCounts(fromSlot: Slot, toSlot: Slot): {blocksPresent: number; payloadsRevealed: number} { + let blocksPresent = 0; + let payloadsRevealed = 0; + for (const node of this.nodes) { + // Count each gloas block once via its PENDING variant, pre-gloas nodes are FULL only + if (node.slot < fromSlot || node.slot > toSlot || node.payloadStatus !== PayloadStatus.PENDING) { + continue; + } + blocksPresent++; + if (this.getNodeIndexByRootAndStatus(node.blockRoot, PayloadStatus.FULL) !== undefined) { + payloadsRevealed++; + } + } + return {blocksPresent, payloadsRevealed}; + } + /** * Update PTC votes for multiple validators attesting to a block * Spec: gloas/fork-choice.md#new-notify_ptc_messages diff --git a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts index 5218841ceab4..ecaa1d6beaf2 100644 --- a/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/getProposerHead.test.ts @@ -188,12 +188,24 @@ describe("Forkchoice / GetProposerHead", () => { expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely, }, { - id: "No reorg when currenSlot is at epoch boundary", - parentBlock: {...baseParentHeadBlock}, - headBlock: {...baseHeadBlock}, - expectReorg: false, + id: "Reorg when currentSlot is at epoch boundary", + parentBlock: { + ...baseParentHeadBlock, + slot: SLOTS_PER_EPOCH * 2 - 2, + stateRoot: getStateRoot(SLOTS_PER_EPOCH * 2 - 2), + blockRoot: getBlockRoot(SLOTS_PER_EPOCH * 2 - 2), + targetRoot: getBlockRoot(SLOTS_PER_EPOCH), + }, + headBlock: { + ...baseHeadBlock, + slot: SLOTS_PER_EPOCH * 2 - 1, + stateRoot: getStateRoot(SLOTS_PER_EPOCH * 2 - 1), + parentRoot: getBlockRoot(SLOTS_PER_EPOCH * 2 - 2), + blockRoot: getBlockRoot(SLOTS_PER_EPOCH * 2 - 1), + targetRoot: getBlockRoot(SLOTS_PER_EPOCH), + }, + expectReorg: true, currentSlot: SLOTS_PER_EPOCH * 2, - expectedNotReorgedReason: NotReorgedReason.AtEpochBoundary, }, { id: "No reorg when the blocks are not ffg competitive", diff --git a/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts b/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts index a270bc55e65e..da14775de93e 100644 --- a/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts +++ b/packages/fork-choice/test/unit/forkChoice/shouldOverrideForkChoiceUpdate.test.ts @@ -186,11 +186,23 @@ describe("Forkchoice / shouldOverrideForkChoiceUpdate", () => { expectedNotReorgedReason: NotReorgedReason.HeadBlockIsTimely, }, { - id: "No reorg when proposal slot is at epoch boundary", - parentBlock: {...baseParentHeadBlock}, - headBlock: {...baseHeadBlock, slot: SLOTS_PER_EPOCH * 2 - 1}, // Proposal slot = block slot + 1 - expectReorg: false, - expectedNotReorgedReason: NotReorgedReason.AtEpochBoundary, + id: "Reorg when proposal slot is at epoch boundary", + parentBlock: { + ...baseParentHeadBlock, + slot: SLOTS_PER_EPOCH * 2 - 2, + stateRoot: getStateRoot(SLOTS_PER_EPOCH * 2 - 2), + blockRoot: getBlockRoot(SLOTS_PER_EPOCH * 2 - 2), + targetRoot: getBlockRoot(SLOTS_PER_EPOCH), + }, + headBlock: { + ...baseHeadBlock, + slot: SLOTS_PER_EPOCH * 2 - 1, + stateRoot: getStateRoot(SLOTS_PER_EPOCH * 2 - 1), + parentRoot: getBlockRoot(SLOTS_PER_EPOCH * 2 - 2), + blockRoot: getBlockRoot(SLOTS_PER_EPOCH * 2 - 1), + targetRoot: getBlockRoot(SLOTS_PER_EPOCH), + }, + expectReorg: true, }, { id: "No reorg when the blocks are not ffg competitive", diff --git a/packages/fork-choice/test/unit/protoArray/gloas.test.ts b/packages/fork-choice/test/unit/protoArray/gloas.test.ts index 0724f38ad71f..4ee8cf86c867 100644 --- a/packages/fork-choice/test/unit/protoArray/gloas.test.ts +++ b/packages/fork-choice/test/unit/protoArray/gloas.test.ts @@ -92,6 +92,54 @@ describe("Gloas Fork Choice", () => { }); }); + describe("getPayloadRevealCounts", () => { + it("counts gloas blocks and revealed payloads within slot range", () => { + const currentSlot = gloasForkSlot + 2; + const protoArray = ProtoArray.initialize( + createTestBlock(gloasForkSlot - 1, genesisRoot, "0x00"), + gloasForkSlot - 1 + ); + + const gloasBlocks = [ + createTestBlock(gloasForkSlot, "0x02", genesisRoot, genesisRoot), + createTestBlock(gloasForkSlot + 1, "0x03", genesisRoot, genesisRoot), + createTestBlock(gloasForkSlot + 2, "0x04", genesisRoot, genesisRoot), + ]; + for (const block of gloasBlocks) { + protoArray.onBlock(block, currentSlot, null); + } + // Reveal payloads for the first two blocks only + for (const blockRoot of ["0x02", "0x03"]) { + protoArray.onExecutionPayload( + blockRoot, + currentSlot, + `${blockRoot}ff`, + 1, + 30000000, + null, + ExecutionStatus.Valid, + DataAvailabilityStatus.Available + ); + } + + // Pre-gloas anchor block is not counted + expect(protoArray.getPayloadRevealCounts(0, currentSlot)).toEqual({blocksPresent: 3, payloadsRevealed: 2}); + // Slot range bounds are inclusive + expect(protoArray.getPayloadRevealCounts(gloasForkSlot + 1, gloasForkSlot + 1)).toEqual({ + blocksPresent: 1, + payloadsRevealed: 1, + }); + expect(protoArray.getPayloadRevealCounts(gloasForkSlot + 2, currentSlot + 10)).toEqual({ + blocksPresent: 1, + payloadsRevealed: 0, + }); + expect(protoArray.getPayloadRevealCounts(currentSlot + 1, currentSlot + 10)).toEqual({ + blocksPresent: 0, + payloadsRevealed: 0, + }); + }); + }); + describe("Pre-Gloas (Fulu) behavior", () => { let protoArray: ProtoArray; diff --git a/packages/logger/package.json b/packages/logger/package.json index af75e5773c17..75b60abf1b84 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -74,7 +74,7 @@ "winston-transport": "^4.5.0" }, "devDependencies": { - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@chainsafe/threads": "^1.11.3", "@lodestar/test-utils": "workspace:^", "@types/triple-beam": "^1.3.2" diff --git a/packages/params/src/index.ts b/packages/params/src/index.ts index 2e54b5d7ac6a..d034f82948ba 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -119,9 +119,12 @@ export const { MAX_PAYLOAD_ATTESTATIONS, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, - BUILDER_REGISTRY_LIMIT, - BUILDER_PENDING_WITHDRAWALS_LIMIT, MAX_BUILDERS_PER_WITHDRAWALS_SWEEP, + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE, + MAX_ATTESTER_SLASHING_SIZE, + MAX_DATA_COLUMN_SIDECAR_SIZE, + MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE, + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE, } = activePreset; //////////// @@ -146,7 +149,7 @@ export const ZERO_HASH_HEX = "0x" + "00".repeat(32); export const BLS_WITHDRAWAL_PREFIX = 0x00; export const ETH1_ADDRESS_WITHDRAWAL_PREFIX = 0x01; export const COMPOUNDING_WITHDRAWAL_PREFIX = 0x02; -export const BUILDER_WITHDRAWAL_PREFIX = 0x03; +export const BUILDER_WITHDRAWAL_PREFIX = 0xb0; // Builder version export const PAYLOAD_BUILDER_VERSION = 0; @@ -249,6 +252,33 @@ export const BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX = 25; export const BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH = 4; export const BLOCK_BODY_EXECUTION_PAYLOAD_INDEX = 9; +/** + * ```ts + * types.ssz.capella.BeaconBlockBody.getPathInfo(['executionPayload', 'blockHash']).gindex + * ``` + */ +export const EXECUTION_BLOCK_HASH_GINDEX = 412; +export const EXECUTION_BLOCK_HASH_DEPTH = 8; +export const EXECUTION_BLOCK_HASH_INDEX = 156; + +/** + * ```ts + * types.ssz.deneb.BeaconBlockBody.getPathInfo(['executionPayload', 'blockHash']).gindex + * ``` + */ +export const EXECUTION_BLOCK_HASH_GINDEX_DENEB = 812; +export const EXECUTION_BLOCK_HASH_DEPTH_DENEB = 9; +export const EXECUTION_BLOCK_HASH_INDEX_DENEB = 300; + +/** + * ```ts + * types.ssz.gloas.BeaconBlockBody.getPathInfo(['signedExecutionPayloadBid', 'message', 'parentBlockHash']).gindex + * ``` + */ +export const EXECUTION_BLOCK_HASH_GINDEX_GLOAS = 2856; +export const EXECUTION_BLOCK_HASH_DEPTH_GLOAS = 11; +export const EXECUTION_BLOCK_HASH_INDEX_GLOAS = 808; + /** * ```ts * config.types.altair.BeaconState.getPathGindex(["currentSyncCommittee"]) @@ -322,6 +352,15 @@ export const KZG_COMMITMENTS_GINDEX = 27; export const KZG_COMMITMENTS_SUBTREE_INDEX = KZG_COMMITMENTS_GINDEX - 2 ** KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH; // Gloas Misc +export const FINALIZED_ROOT_GINDEX_GLOAS = 735; +export const FINALIZED_ROOT_DEPTH_GLOAS = 9; +export const FINALIZED_ROOT_INDEX_GLOAS = 223; +export const CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS = 2945; +export const CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS = 11; +export const CURRENT_SYNC_COMMITTEE_INDEX_GLOAS = 897; +export const NEXT_SYNC_COMMITTEE_GINDEX_GLOAS = 2946; +export const NEXT_SYNC_COMMITTEE_DEPTH_GLOAS = 11; +export const NEXT_SYNC_COMMITTEE_INDEX_GLOAS = 898; export const BUILDER_INDEX_FLAG = 2 ** 40; export const BUILDER_INDEX_SELF_BUILD = Infinity; export const BUILDER_PAYMENT_THRESHOLD_NUMERATOR = 6; diff --git a/packages/params/src/presets/mainnet.ts b/packages/params/src/presets/mainnet.ts index 2cdd35d1d270..c1a325207167 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -145,9 +145,15 @@ export const mainnetPreset: BeaconPreset = { // GLOAS PTC_SIZE: 512, MAX_PAYLOAD_ATTESTATIONS: 4, - MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 256, // 2**8 + MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 64, // 2**6 MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16, // 2**4 - BUILDER_REGISTRY_LIMIT: 1099511627776, // 2**40 - BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576, // 2**20 MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16384, // 2**14 + + // Type-specific SSZ bounds + // --------------------------------------------------------------- + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE: 16829, + MAX_ATTESTER_SLASHING_SIZE: 2097616, + MAX_DATA_COLUMN_SIDECAR_SIZE: 8585272, + MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE: 8585741, + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE: 196932, }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index 7e5d50fa4161..0c7be8b39e6c 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -146,9 +146,15 @@ export const minimalPreset: BeaconPreset = { // GLOAS PTC_SIZE: 16, MAX_PAYLOAD_ATTESTATIONS: 4, - MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 256, // 2**8 + MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: 64, // 2**6 MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: 16, // 2**4 - BUILDER_REGISTRY_LIMIT: 1099511627776, // 2**40 - BUILDER_PENDING_WITHDRAWALS_LIMIT: 1048576, // 2**20 MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: 16, // 2**4 + + // Type-specific SSZ bounds + // --------------------------------------------------------------- + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE: 1462, + MAX_ATTESTER_SLASHING_SIZE: 131536, + MAX_DATA_COLUMN_SIDECAR_SIZE: 8585272, + MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE: 8585741, + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE: 196932, }; diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index 9889020b3f27..49ca04d27d2f 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -109,9 +109,12 @@ export type BeaconPreset = { MAX_PAYLOAD_ATTESTATIONS: number; MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: number; MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: number; - BUILDER_REGISTRY_LIMIT: number; - BUILDER_PENDING_WITHDRAWALS_LIMIT: number; MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: number; + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE: number; + MAX_ATTESTER_SLASHING_SIZE: number; + MAX_DATA_COLUMN_SIDECAR_SIZE: number; + MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE: number; + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE: number; }; /** @@ -226,9 +229,12 @@ export const beaconPresetTypes: BeaconPresetTypes = { MAX_PAYLOAD_ATTESTATIONS: "number", MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: "number", MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD: "number", - BUILDER_REGISTRY_LIMIT: "number", - BUILDER_PENDING_WITHDRAWALS_LIMIT: "number", MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: "number", + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE: "number", + MAX_ATTESTER_SLASHING_SIZE: "number", + MAX_DATA_COLUMN_SIDECAR_SIZE: "number", + MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE: "number", + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE: "number", }; type BeaconPresetTypes = { diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index 7ab1bcae0a22..ad9b00cd8e5d 100644 --- a/packages/reqresp/package.json +++ b/packages/reqresp/package.json @@ -56,7 +56,7 @@ "uint8arraylist": "^2.4.7" }, "devDependencies": { - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@libp2p/crypto": "^5.1.13", "@libp2p/peer-id": "^6.0.4", "@lodestar/logger": "workspace:^", diff --git a/packages/spec-test-util/package.json b/packages/spec-test-util/package.json index bab0446f3da4..7efa2c111c5e 100644 --- a/packages/spec-test-util/package.json +++ b/packages/spec-test-util/package.json @@ -65,7 +65,7 @@ "vitest": "catalog:" }, "peerDependencies": { - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@lodestar/types": "workspace:^", "vitest": "catalog:" } diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index 1b3a7cbebd3c..2fe7d27f39d1 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -67,9 +67,9 @@ "dependencies": { "@chainsafe/as-sha256": "^1.2.0", "@chainsafe/blst": "^2.2.0", - "@chainsafe/persistent-merkle-tree": "^1.2.5", + "@chainsafe/persistent-merkle-tree": "^1.3.0", "@chainsafe/pubkey-index-map": "^3.0.0", - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@chainsafe/swap-or-not-shuffle": "^1.2.1", "@lodestar/config": "workspace:^", "@lodestar/params": "workspace:^", diff --git a/packages/state-transition/src/block/processBuilderDepositRequest.ts b/packages/state-transition/src/block/processBuilderDepositRequest.ts index 1f1a89586b62..8896a71af94d 100644 --- a/packages/state-transition/src/block/processBuilderDepositRequest.ts +++ b/packages/state-transition/src/block/processBuilderDepositRequest.ts @@ -1,8 +1,13 @@ -import {FAR_FUTURE_EPOCH} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, PAYLOAD_BUILDER_VERSION} from "@lodestar/params"; import {gloas} from "@lodestar/types"; import {CachedBeaconStateGloas} from "../types.js"; import {computeEpochAtSlot} from "../util/epoch.js"; -import {addBuilderToRegistry, findBuilderIndexByPubkey, isValidBuilderDepositSignature} from "../util/gloas.js"; +import { + addBuilderToRegistry, + findBuilderIndexByPubkey, + isBuilderWithdrawalCredential, + isValidBuilderDepositSignature, +} from "../util/gloas.js"; /** * Process a builder deposit request from the execution layer: register a new builder @@ -15,6 +20,12 @@ export function processBuilderDepositRequest( request: gloas.BuilderDepositRequest ): void { const {pubkey, withdrawalCredentials, amount, signature} = request; + + // Ignore deposits with unexpected withdrawal credential prefixes. + if (!isBuilderWithdrawalCredential(withdrawalCredentials)) { + return; + } + const builderIndex = findBuilderIndexByPubkey(state, pubkey); if (builderIndex === null) { @@ -22,7 +33,7 @@ export function processBuilderDepositRequest( addBuilderToRegistry( state, pubkey, - withdrawalCredentials[0], + PAYLOAD_BUILDER_VERSION, withdrawalCredentials.subarray(12), amount, state.slot @@ -33,11 +44,13 @@ export function processBuilderDepositRequest( const builder = state.builders.get(builderIndex); - // Increase balance by deposit amount - builder.balance += amount; - - // If exited, reset the withdrawable epoch - if (builder.withdrawableEpoch !== FAR_FUTURE_EPOCH) { + // If the builder has exited and been fully swept (balance drained to 0), reset the + // withdrawable epoch so this top-up becomes withdrawable again. Must run before the + // balance increase, since the reset is gated on the current balance being 0. + if (builder.withdrawableEpoch !== FAR_FUTURE_EPOCH && builder.balance === 0) { builder.withdrawableEpoch = computeEpochAtSlot(state.slot) + state.config.MIN_BUILDER_WITHDRAWABILITY_DELAY; } + + // Increase balance by deposit amount + builder.balance += amount; } diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index de737e74c9d1..06f3137e6eef 100644 --- a/packages/state-transition/src/block/processOperations.ts +++ b/packages/state-transition/src/block/processOperations.ts @@ -1,4 +1,12 @@ -import {ForkSeq} from "@lodestar/params"; +import { + ForkSeq, + MAX_ATTESTATIONS_ELECTRA, + MAX_ATTESTER_SLASHINGS_ELECTRA, + MAX_BLS_TO_EXECUTION_CHANGES, + MAX_PAYLOAD_ATTESTATIONS, + MAX_PROPOSER_SLASHINGS, + MAX_VOLUNTARY_EXITS, +} from "@lodestar/params"; import {BeaconBlockBody, capella, electra, gloas} from "@lodestar/types"; import {BeaconStateTransitionMetrics} from "../metrics.js"; import { @@ -39,6 +47,10 @@ export function processOperations( opts: ProcessBlockOpts = {verifySignatures: true}, metrics?: BeaconStateTransitionMetrics | null ): void { + if (fork >= ForkSeq.gloas) { + assertGloasOperationLimits(body as gloas.BeaconBlockBody); + } + // verify that outstanding deposits are processed up to the maximum number of deposits. // From Fulu the eth1 bridge deposit mechanism was removed, so blocks must not contain any deposits. const maxDeposits = fork >= ForkSeq.fulu ? 0 : getEth1DepositCount(state); @@ -94,3 +106,18 @@ export function processOperations( } } } + +function assertGloasOperationLimits(body: gloas.BeaconBlockBody): void { + assertMaxLength("proposerSlashings", body.proposerSlashings.length, MAX_PROPOSER_SLASHINGS); + assertMaxLength("attesterSlashings", body.attesterSlashings.length, MAX_ATTESTER_SLASHINGS_ELECTRA); + assertMaxLength("attestations", body.attestations.length, MAX_ATTESTATIONS_ELECTRA); + assertMaxLength("voluntaryExits", body.voluntaryExits.length, MAX_VOLUNTARY_EXITS); + assertMaxLength("blsToExecutionChanges", body.blsToExecutionChanges.length, MAX_BLS_TO_EXECUTION_CHANGES); + assertMaxLength("payloadAttestations", body.payloadAttestations.length, MAX_PAYLOAD_ATTESTATIONS); +} + +function assertMaxLength(name: string, length: number, limit: number): void { + if (length > limit) { + throw new Error(`Block contains too many ${name}: count=${length} limit=${limit}`); + } +} diff --git a/packages/state-transition/src/block/processParentExecutionPayload.ts b/packages/state-transition/src/block/processParentExecutionPayload.ts index c0a1837a1fc0..dc00b09700fa 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -1,4 +1,12 @@ -import {ForkPostGloas, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; +import { + ForkPostGloas, + MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, + MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, + MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD, + SLOTS_PER_EPOCH, + SLOTS_PER_HISTORICAL_ROOT, +} from "@lodestar/params"; import {BeaconBlock, gloas, ssz} from "@lodestar/types"; import {byteArrayEquals, toRootHex} from "@lodestar/utils"; import {CachedBeaconStateGloas} from "../types.js"; @@ -47,6 +55,8 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.6/specs/gloas/beacon-chain.md#new-apply_parent_execution_payload */ export function applyParentExecutionPayload(state: CachedBeaconStateGloas, requests: gloas.ExecutionRequests): void { + assertExecutionRequestsWithinLimits(requests); + const fork = state.config.getForkSeq(state.slot); const parentBid = state.latestExecutionPayloadBid; const parentSlot = parentBid.slot; @@ -116,6 +126,21 @@ function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: numbe state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); } +function assertExecutionRequestsWithinLimits(requests: gloas.ExecutionRequests): void { + assertMaxLength("withdrawals", requests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD); + assertMaxLength("consolidations", requests.consolidations.length, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD); + // New in GLOAS:EIP8282 + assertMaxLength("builderDeposits", requests.builderDeposits.length, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD); + // New in GLOAS:EIP8282 + assertMaxLength("builderExits", requests.builderExits.length, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD); +} + +function assertMaxLength(name: string, length: number, limit: number): void { + if (length > limit) { + throw new Error(`Too many parent execution request ${name} count=${length} limit=${limit}`); + } +} + function assertEmptyExecutionRequests(requests: gloas.ExecutionRequests): void { if ( requests.deposits.length !== 0 || diff --git a/packages/state-transition/src/block/processWithdrawals.ts b/packages/state-transition/src/block/processWithdrawals.ts index c97a53a9c83c..66e53cf89815 100644 --- a/packages/state-transition/src/block/processWithdrawals.ts +++ b/packages/state-transition/src/block/processWithdrawals.ts @@ -99,7 +99,7 @@ export function processWithdrawals( const stateGloas = state as CachedBeaconStateGloas; // Store expected withdrawals for verification - stateGloas.payloadExpectedWithdrawals = ssz.capella.Withdrawals.toViewDU(expectedWithdrawals); + stateGloas.payloadExpectedWithdrawals = ssz.gloas.Withdrawals.toViewDU(expectedWithdrawals); // Update builder pending withdrawals queue stateGloas.builderPendingWithdrawals = stateGloas.builderPendingWithdrawals.sliceFrom( diff --git a/packages/state-transition/src/cache/stateCache.ts b/packages/state-transition/src/cache/stateCache.ts index 6054001a6027..efa3b266289c 100644 --- a/packages/state-transition/src/cache/stateCache.ts +++ b/packages/state-transition/src/cache/stateCache.ts @@ -254,11 +254,9 @@ export function isCachedBeaconState( // This cache is populated during epoch transition, and should be preserved for performance. // If the cache is missing too often, means that our clone strategy is not working well. export function isStateValidatorsNodesPopulated(state: CachedBeaconStateAllForks): boolean { - // biome-ignore lint/complexity/useLiteralKeys: It is a private attribute - return state.validators["nodesPopulated"] === true; + return (state.validators as unknown as {nodesPopulated?: boolean}).nodesPopulated === true; } export function isStateBalancesNodesPopulated(state: CachedBeaconStateAllForks): boolean { - // biome-ignore lint/complexity/useLiteralKeys: It is a private attribute - return state.balances["nodesPopulated"] === true; + return (state.balances as unknown as {nodesPopulated?: boolean}).nodesPopulated === true; } diff --git a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts index fad5857b3ce6..ae917dd5ecb2 100644 --- a/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts +++ b/packages/state-transition/src/epoch/processParticipationFlagUpdates.ts @@ -1,6 +1,7 @@ import {zeroNode} from "@chainsafe/persistent-merkle-tree"; import {ssz} from "@lodestar/types"; -import {CachedBeaconStateAltair} from "../types.js"; +import type {CachedBeaconStateAltair, CachedBeaconStateGloas} from "../types.js"; +import {isGloasStateType} from "../util/execution.js"; /** * Updates `state.previousEpochParticipation` with precalculated epoch participation. Creates a new empty tree for @@ -10,6 +11,11 @@ import {CachedBeaconStateAltair} from "../types.js"; * trees completely. */ export function processParticipationFlagUpdates(state: CachedBeaconStateAltair): void { + if (isGloasStateType(state)) { + processParticipationFlagUpdatesGloas(state as CachedBeaconStateGloas); + return; + } + // Set view and tree from currentEpochParticipation to previousEpochParticipation state.previousEpochParticipation = state.currentEpochParticipation; @@ -25,3 +31,10 @@ export function processParticipationFlagUpdates(state: CachedBeaconStateAltair): state.currentEpochParticipation = ssz.altair.EpochParticipation.getViewDU(currentEpochParticipationNode); } + +function processParticipationFlagUpdatesGloas(state: CachedBeaconStateGloas): void { + state.previousEpochParticipation = state.currentEpochParticipation; + state.currentEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + new Array(state.currentEpochParticipation.length).fill(0) + ); +} diff --git a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts index ef074dfd6820..1fd7852b5338 100644 --- a/packages/state-transition/src/epoch/processRewardsAndPenalties.ts +++ b/packages/state-transition/src/epoch/processRewardsAndPenalties.ts @@ -1,5 +1,4 @@ import {ForkSeq, GENESIS_EPOCH} from "@lodestar/params"; -import {ssz} from "@lodestar/types"; import { CachedBeaconStateAllForks, CachedBeaconStateAltair, @@ -39,7 +38,7 @@ export function processRewardsAndPenalties( // important: do not change state one balance at a time. Set them all at once, constructing the tree in one go // cache the balances array, too - state.balances = ssz.phase0.Balances.toViewDU(balances); + state.balances = state.type.fields.balances.toViewDU(balances); // For processEffectiveBalanceUpdates() to prevent having to re-compute the balances array. // For validator metrics diff --git a/packages/state-transition/src/index.ts b/packages/state-transition/src/index.ts index 5fd6b6be2316..23680fa7fb41 100644 --- a/packages/state-transition/src/index.ts +++ b/packages/state-transition/src/index.ts @@ -41,6 +41,11 @@ export type {EpochTransitionStep} from "./epoch/index.js"; export {type BeaconStateTransitionMetrics, getMetrics} from "./metrics.js"; export * from "./rewards/index.js"; export * from "./signatureSets/index.js"; +export { + upgradeAttestationToGloas, + upgradeAttesterSlashingToGloas, + upgradeIndexedAttestationToGloas, +} from "./slot/upgradeStateToGloas.js"; export * from "./stateTransition.js"; export {BeaconStateView} from "./stateView/beaconStateView.js"; export { diff --git a/packages/state-transition/src/lightClient/proofs.ts b/packages/state-transition/src/lightClient/proofs.ts index d8b3847d978d..5912d4827daf 100644 --- a/packages/state-transition/src/lightClient/proofs.ts +++ b/packages/state-transition/src/lightClient/proofs.ts @@ -1,11 +1,15 @@ import {Tree} from "@chainsafe/persistent-merkle-tree"; import { BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX, + CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS, FINALIZED_ROOT_GINDEX, FINALIZED_ROOT_GINDEX_ELECTRA, + FINALIZED_ROOT_GINDEX_GLOAS, ForkName, ForkPostBellatrix, + NEXT_SYNC_COMMITTEE_GINDEX_GLOAS, isForkPostElectra, + isForkPostGloas, } from "@lodestar/params"; import {BeaconBlockBody, SSZTypesFor, ssz} from "@lodestar/types"; import {BeaconStateAllForks, CachedBeaconStateAllForks} from "../types.js"; @@ -17,6 +21,24 @@ export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllFo let currentSyncCommitteeRoot: Uint8Array; let nextSyncCommitteeRoot: Uint8Array; + if (isForkPostGloas(fork)) { + const tree = new Tree(state.node); + const currentSyncCommitteeGindex = BigInt(CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS); + const nextSyncCommitteeGindex = BigInt(NEXT_SYNC_COMMITTEE_GINDEX_GLOAS); + + currentSyncCommitteeRoot = tree.getRoot(currentSyncCommitteeGindex); + nextSyncCommitteeRoot = tree.getRoot(nextSyncCommitteeGindex); + witness = []; + + return { + witness, + currentSyncCommitteeRoot, + nextSyncCommitteeRoot, + currentSyncCommitteeBranch: tree.getSingleProof(currentSyncCommitteeGindex), + nextSyncCommitteeBranch: tree.getSingleProof(nextSyncCommitteeGindex), + }; + } + if (isForkPostElectra(fork)) { const n2 = n1.left; const n5 = n2.right; @@ -60,17 +82,30 @@ export function getSyncCommitteesWitness(fork: ForkName, state: BeaconStateAllFo } export function getNextSyncCommitteeBranch(syncCommitteesWitness: SyncCommitteeWitness): Uint8Array[] { + if (syncCommitteesWitness.nextSyncCommitteeBranch) { + return syncCommitteesWitness.nextSyncCommitteeBranch; + } + // Witness branch is sorted by descending gindex return [syncCommitteesWitness.currentSyncCommitteeRoot, ...syncCommitteesWitness.witness]; } export function getCurrentSyncCommitteeBranch(syncCommitteesWitness: SyncCommitteeWitness): Uint8Array[] { + if (syncCommitteesWitness.currentSyncCommitteeBranch) { + return syncCommitteesWitness.currentSyncCommitteeBranch; + } + // Witness branch is sorted by descending gindex return [syncCommitteesWitness.nextSyncCommitteeRoot, ...syncCommitteesWitness.witness]; } export function getFinalizedRootProof(state: CachedBeaconStateAllForks): Uint8Array[] { - const finalizedRootGindex = state.epochCtx.isPostElectra() ? FINALIZED_ROOT_GINDEX_ELECTRA : FINALIZED_ROOT_GINDEX; + const fork = state.config.getForkName(state.slot); + const finalizedRootGindex = isForkPostGloas(fork) + ? FINALIZED_ROOT_GINDEX_GLOAS + : state.epochCtx.isPostElectra() + ? FINALIZED_ROOT_GINDEX_ELECTRA + : FINALIZED_ROOT_GINDEX; return new Tree(state.node).getSingleProof(BigInt(finalizedRootGindex)); } diff --git a/packages/state-transition/src/lightClient/spec/index.ts b/packages/state-transition/src/lightClient/spec/index.ts index 7eed6acd6cb2..0e1f063c4622 100644 --- a/packages/state-transition/src/lightClient/spec/index.ts +++ b/packages/state-transition/src/lightClient/spec/index.ts @@ -14,7 +14,7 @@ import { processLightClientUpdate, } from "./processLightClientUpdate.js"; import {type ILightClientStore, LightClientStore, type LightClientStoreEvents} from "./store.js"; -import {ZERO_HEADER, ZERO_SYNC_COMMITTEE, getZeroFinalityBranch, getZeroSyncCommitteeBranch} from "./utils.js"; +import {ZERO_SYNC_COMMITTEE, getZeroFinalityBranch, getZeroSyncCommitteeBranch} from "./utils.js"; export type {LightClientUpdateSummary} from "./isBetterUpdate.js"; export {isBetterUpdate, toLightClientUpdateSummary} from "./isBetterUpdate.js"; @@ -76,12 +76,16 @@ export class LightclientSpec { } onOptimisticUpdate(currentSlot: Slot, optimisticUpdate: LightClientOptimisticUpdate): void { + const fork = this.config.getForkName(optimisticUpdate.signatureSlot); + this.onUpdate(currentSlot, { attestedHeader: optimisticUpdate.attestedHeader, nextSyncCommittee: ZERO_SYNC_COMMITTEE, - nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(this.config.getForkName(optimisticUpdate.signatureSlot)), - finalizedHeader: {beacon: ZERO_HEADER}, - finalityBranch: getZeroFinalityBranch(this.config.getForkName(optimisticUpdate.signatureSlot)), + nextSyncCommitteeBranch: getZeroSyncCommitteeBranch(fork), + finalizedHeader: this.config + .getPostAltairForkTypes(optimisticUpdate.signatureSlot) + .LightClientHeader.defaultValue(), + finalityBranch: getZeroFinalityBranch(fork), syncAggregate: optimisticUpdate.syncAggregate, signatureSlot: optimisticUpdate.signatureSlot, }); diff --git a/packages/state-transition/src/lightClient/spec/utils.ts b/packages/state-transition/src/lightClient/spec/utils.ts index a571614e2e2e..bcd0cd66aa45 100644 --- a/packages/state-transition/src/lightClient/spec/utils.ts +++ b/packages/state-transition/src/lightClient/spec/utils.ts @@ -1,16 +1,25 @@ import {PublicKey} from "@chainsafe/blst"; +import {Tree} from "@chainsafe/persistent-merkle-tree"; import {BitArray} from "@chainsafe/ssz"; import {ChainForkConfig} from "@lodestar/config"; import { - BLOCK_BODY_EXECUTION_PAYLOAD_DEPTH as EXECUTION_PAYLOAD_DEPTH, - BLOCK_BODY_EXECUTION_PAYLOAD_INDEX as EXECUTION_PAYLOAD_INDEX, - FINALIZED_ROOT_DEPTH, - FINALIZED_ROOT_DEPTH_ELECTRA, + BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX, + CURRENT_SYNC_COMMITTEE_GINDEX, + CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA, + CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS, + EXECUTION_BLOCK_HASH_GINDEX, + EXECUTION_BLOCK_HASH_GINDEX_DENEB, + EXECUTION_BLOCK_HASH_GINDEX_GLOAS, + FINALIZED_ROOT_GINDEX, + FINALIZED_ROOT_GINDEX_ELECTRA, + FINALIZED_ROOT_GINDEX_GLOAS, ForkName, ForkSeq, - NEXT_SYNC_COMMITTEE_DEPTH, - NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA, + NEXT_SYNC_COMMITTEE_GINDEX, + NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA, + NEXT_SYNC_COMMITTEE_GINDEX_GLOAS, isForkPostElectra, + isForkPostGloas, } from "@lodestar/params"; import { BeaconBlockHeader, @@ -74,17 +83,11 @@ export function getSafetyThreshold(maxActiveParticipants: number): number { } export function getZeroSyncCommitteeBranch(fork: ForkName): Uint8Array[] { - const nextSyncCommitteeDepth = isForkPostElectra(fork) - ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA - : NEXT_SYNC_COMMITTEE_DEPTH; - - return Array.from({length: nextSyncCommitteeDepth}, () => ZERO_HASH); + return Array.from({length: getGindexDepth(nextSyncCommitteeGindexAtFork(fork))}, () => ZERO_HASH); } export function getZeroFinalityBranch(fork: ForkName): Uint8Array[] { - const finalizedRootDepth = isForkPostElectra(fork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH; - - return Array.from({length: finalizedRootDepth}, () => ZERO_HASH); + return Array.from({length: getGindexDepth(finalizedRootGindexAtFork(fork))}, () => ZERO_HASH); } export function isSyncCommitteeUpdate(update: LightClientUpdate): boolean { @@ -129,12 +132,73 @@ export function isValidMerkleBranch( return verifyMerkleBranch(leaf, branch, depth, index, root); } -export function normalizeMerkleBranch(branch: Uint8Array[], depth: number): Uint8Array[] { +export function isValidNormalizedMerkleBranch( + leaf: Uint8Array, + branch: Uint8Array[], + gindex: number, + root: Uint8Array +): boolean { + const depth = getGindexDepth(gindex); + const index = getGindexIndex(gindex); + const numExtraDepth = branch.length - depth; + if (numExtraDepth < 0) { + return false; + } + + for (let i = 0; i < numExtraDepth; i++) { + if (!byteArrayEquals(branch[i], ZERO_HASH)) { + return false; + } + } + + return isValidMerkleBranch(leaf, branch.slice(numExtraDepth), depth, index, root); +} + +export function normalizeMerkleBranch(branch: Uint8Array[], gindex: number): Uint8Array[] { + const depth = getGindexDepth(gindex); const numExtraDepth = depth - branch.length; return [...Array.from({length: numExtraDepth}, () => ZERO_HASH), ...branch]; } +export function currentSyncCommitteeGindexAtFork(fork: ForkName): number { + if (isForkPostGloas(fork)) { + return CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS; + } + if (isForkPostElectra(fork)) { + return CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA; + } + return CURRENT_SYNC_COMMITTEE_GINDEX; +} + +export function finalizedRootGindexAtFork(fork: ForkName): number { + if (isForkPostGloas(fork)) { + return FINALIZED_ROOT_GINDEX_GLOAS; + } + if (isForkPostElectra(fork)) { + return FINALIZED_ROOT_GINDEX_ELECTRA; + } + return FINALIZED_ROOT_GINDEX; +} + +export function nextSyncCommitteeGindexAtFork(fork: ForkName): number { + if (isForkPostGloas(fork)) { + return NEXT_SYNC_COMMITTEE_GINDEX_GLOAS; + } + if (isForkPostElectra(fork)) { + return NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA; + } + return NEXT_SYNC_COMMITTEE_GINDEX; +} + +export function getGindexDepth(gindex: number): number { + return Math.floor(Math.log2(gindex)); +} + +export function getGindexIndex(gindex: number): number { + return gindex - 2 ** getGindexDepth(gindex); +} + export function upgradeLightClientHeader( config: ChainForkConfig, targetFork: ForkName, @@ -147,7 +211,7 @@ export function upgradeLightClientHeader( // We are modifying the same header object, may be we could create a copy, but its // not required as of now - const upgradedHeader = header; + let upgradedHeader = header; const startUpgradeFromFork = Object.values(ForkName)[ForkSeq[headerFork] + 1]; switch (startUpgradeFromFork) { @@ -198,7 +262,11 @@ export function upgradeLightClientHeader( if (ForkSeq[targetFork] <= ForkSeq.fulu) break; case ForkName.gloas: - // No changes to LightClientHeader in Gloas + if (isGloasLightClientHeader(upgradedHeader)) { + break; + } + + upgradedHeader = upgradeLightClientHeaderToGloas(config, upgradedHeader as LightClientHeader); // Break if no further upgrades is required else fall through if (ForkSeq[targetFork] <= ForkSeq.gloas) break; @@ -209,6 +277,40 @@ export function upgradeLightClientHeader( export function isValidLightClientHeader(config: ChainForkConfig, header: LightClientHeader): boolean { const epoch = computeEpochAtSlot(header.beacon.slot); + if (isGloasLightClientHeader(header)) { + if (epoch >= config.GLOAS_FORK_EPOCH) { + return isValidNormalizedMerkleBranch( + header.executionBlockHash, + header.executionBranch, + EXECUTION_BLOCK_HASH_GINDEX_GLOAS, + header.beacon.bodyRoot + ); + } + + if (epoch >= config.DENEB_FORK_EPOCH) { + return isValidNormalizedMerkleBranch( + header.executionBlockHash, + header.executionBranch, + EXECUTION_BLOCK_HASH_GINDEX_DENEB, + header.beacon.bodyRoot + ); + } + + if (epoch >= config.CAPELLA_FORK_EPOCH) { + return isValidNormalizedMerkleBranch( + header.executionBlockHash, + header.executionBranch, + EXECUTION_BLOCK_HASH_GINDEX, + header.beacon.bodyRoot + ); + } + + return ( + byteArrayEquals(header.executionBlockHash, ZERO_HASH) && + header.executionBranch.every((node) => byteArrayEquals(node, ZERO_HASH)) + ); + } + if (epoch < config.CAPELLA_FORK_EPOCH) { return ( ((header as LightClientHeader).execution === undefined || @@ -239,8 +341,8 @@ export function isValidLightClientHeader(config: ChainForkConfig, header: LightC .getPostBellatrixForkTypes(header.beacon.slot) .ExecutionPayloadHeader.hashTreeRoot((header as LightClientHeader).execution), (header as LightClientHeader).executionBranch, - EXECUTION_PAYLOAD_DEPTH, - EXECUTION_PAYLOAD_INDEX, + getGindexDepth(BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX), + getGindexIndex(BLOCK_BODY_EXECUTION_PAYLOAD_GINDEX), header.beacon.bodyRoot ); } @@ -254,12 +356,9 @@ export function upgradeLightClientUpdate( update.finalizedHeader = upgradeLightClientHeader(config, targetFork, update.finalizedHeader); update.nextSyncCommitteeBranch = normalizeMerkleBranch( update.nextSyncCommitteeBranch, - isForkPostElectra(targetFork) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH - ); - update.finalityBranch = normalizeMerkleBranch( - update.finalityBranch, - isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH + nextSyncCommitteeGindexAtFork(targetFork) ); + update.finalityBranch = normalizeMerkleBranch(update.finalityBranch, finalizedRootGindexAtFork(targetFork)); return update; } @@ -273,7 +372,7 @@ export function upgradeLightClientFinalityUpdate( finalityUpdate.finalizedHeader = upgradeLightClientHeader(config, targetFork, finalityUpdate.finalizedHeader); finalityUpdate.finalityBranch = normalizeMerkleBranch( finalityUpdate.finalityBranch, - isForkPostElectra(targetFork) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH + finalizedRootGindexAtFork(targetFork) ); return finalityUpdate; @@ -315,3 +414,72 @@ export function upgradeLightClientStore( return store; } + +function isGloasLightClientHeader(header: LightClientHeader): header is LightClientHeader { + return (header as LightClientHeader).executionBlockHash !== undefined; +} + +function upgradeLightClientHeaderToGloas( + config: ChainForkConfig, + pre: LightClientHeader +): LightClientHeader { + if (ssz.electra.LightClientHeader.equals(pre, ssz.electra.LightClientHeader.defaultValue())) { + return ssz.gloas.LightClientHeader.defaultValue(); + } + + const epoch = computeEpochAtSlot(pre.beacon.slot); + + if (epoch >= config.DENEB_FORK_EPOCH) { + const blockHashGindex = ssz.deneb.ExecutionPayloadHeader.getPathInfo(["blockHash"]).gindex; + const executionBranch = new Tree(ssz.deneb.ExecutionPayloadHeader.toView(pre.execution).node).getSingleProof( + blockHashGindex + ); + + return { + beacon: pre.beacon, + executionBlockHash: pre.execution.blockHash, + executionBranch: normalizeMerkleBranch( + [...executionBranch, ...pre.executionBranch], + EXECUTION_BLOCK_HASH_GINDEX_GLOAS + ), + }; + } + + if (epoch >= config.CAPELLA_FORK_EPOCH) { + const executionHeader = { + parentHash: pre.execution.parentHash, + feeRecipient: pre.execution.feeRecipient, + stateRoot: pre.execution.stateRoot, + receiptsRoot: pre.execution.receiptsRoot, + logsBloom: pre.execution.logsBloom, + prevRandao: pre.execution.prevRandao, + blockNumber: pre.execution.blockNumber, + gasLimit: pre.execution.gasLimit, + gasUsed: pre.execution.gasUsed, + timestamp: pre.execution.timestamp, + extraData: pre.execution.extraData, + baseFeePerGas: pre.execution.baseFeePerGas, + blockHash: pre.execution.blockHash, + transactionsRoot: pre.execution.transactionsRoot, + withdrawalsRoot: pre.execution.withdrawalsRoot, + }; + const blockHashGindex = ssz.capella.ExecutionPayloadHeader.getPathInfo(["blockHash"]).gindex; + const executionBranch = new Tree(ssz.capella.ExecutionPayloadHeader.toView(executionHeader).node).getSingleProof( + blockHashGindex + ); + + return { + beacon: pre.beacon, + executionBlockHash: executionHeader.blockHash, + executionBranch: normalizeMerkleBranch( + [...executionBranch, ...pre.executionBranch], + EXECUTION_BLOCK_HASH_GINDEX_GLOAS + ), + }; + } + + return { + ...ssz.gloas.LightClientHeader.defaultValue(), + beacon: pre.beacon, + }; +} diff --git a/packages/state-transition/src/lightClient/spec/validateLightClientBootstrap.ts b/packages/state-transition/src/lightClient/spec/validateLightClientBootstrap.ts index 003218d95096..82cd9de2b021 100644 --- a/packages/state-transition/src/lightClient/spec/validateLightClientBootstrap.ts +++ b/packages/state-transition/src/lightClient/spec/validateLightClientBootstrap.ts @@ -1,13 +1,7 @@ import {ChainForkConfig} from "@lodestar/config"; -import {isForkPostElectra} from "@lodestar/params"; import {LightClientBootstrap, Root, ssz} from "@lodestar/types"; import {byteArrayEquals, toHex} from "@lodestar/utils"; -import {isValidLightClientHeader, isValidMerkleBranch} from "./utils.js"; - -const CURRENT_SYNC_COMMITTEE_INDEX = 22; -const CURRENT_SYNC_COMMITTEE_DEPTH = 5; -const CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA = 22; -const CURRENT_SYNC_COMMITTEE_DEPTH_ELECTRA = 6; +import {currentSyncCommitteeGindexAtFork, isValidLightClientHeader, isValidNormalizedMerkleBranch} from "./utils.js"; export function validateLightClientBootstrap( config: ChainForkConfig, @@ -26,11 +20,10 @@ export function validateLightClientBootstrap( } if ( - !isValidMerkleBranch( + !isValidNormalizedMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(bootstrap.currentSyncCommittee), bootstrap.currentSyncCommitteeBranch, - isForkPostElectra(fork) ? CURRENT_SYNC_COMMITTEE_DEPTH_ELECTRA : CURRENT_SYNC_COMMITTEE_DEPTH, - isForkPostElectra(fork) ? CURRENT_SYNC_COMMITTEE_INDEX_ELECTRA : CURRENT_SYNC_COMMITTEE_INDEX, + currentSyncCommitteeGindexAtFork(fork), bootstrap.header.beacon.stateRoot ) ) { diff --git a/packages/state-transition/src/lightClient/spec/validateLightClientUpdate.ts b/packages/state-transition/src/lightClient/spec/validateLightClientUpdate.ts index d253433423bf..1e3e3aaa7ba1 100644 --- a/packages/state-transition/src/lightClient/spec/validateLightClientUpdate.ts +++ b/packages/state-transition/src/lightClient/spec/validateLightClientUpdate.ts @@ -1,29 +1,19 @@ import {PublicKey, Signature, fastAggregateVerify} from "@chainsafe/blst"; import {ChainForkConfig} from "@lodestar/config"; -import { - DOMAIN_SYNC_COMMITTEE, - FINALIZED_ROOT_DEPTH, - FINALIZED_ROOT_DEPTH_ELECTRA, - FINALIZED_ROOT_INDEX, - FINALIZED_ROOT_INDEX_ELECTRA, - GENESIS_SLOT, - MIN_SYNC_COMMITTEE_PARTICIPANTS, - NEXT_SYNC_COMMITTEE_DEPTH, - NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA, - NEXT_SYNC_COMMITTEE_INDEX, - NEXT_SYNC_COMMITTEE_INDEX_ELECTRA, -} from "@lodestar/params"; -import {LightClientUpdate, Root, isElectraLightClientUpdate, ssz} from "@lodestar/types"; +import {DOMAIN_SYNC_COMMITTEE, GENESIS_SLOT, MIN_SYNC_COMMITTEE_PARTICIPANTS} from "@lodestar/params"; +import {LightClientUpdate, Root, ssz} from "@lodestar/types"; import type {ILightClientStore, SyncCommitteeFast} from "./store.js"; import { ZERO_HASH, + finalizedRootGindexAtFork, getParticipantPubkeys, isFinalityUpdate, isSyncCommitteeUpdate, isValidLightClientHeader, - isValidMerkleBranch, + isValidNormalizedMerkleBranch, isZeroedHeader, isZeroedSyncCommittee, + nextSyncCommitteeGindexAtFork, sumBits, } from "./utils.js"; @@ -33,6 +23,8 @@ export function validateLightClientUpdate( update: LightClientUpdate, syncCommittee: SyncCommitteeFast ): void { + const attestedFork = config.getForkName(update.attestedHeader.beacon.slot); + // Verify sync committee has sufficient participants if (sumBits(update.syncAggregate.syncCommitteeBits) < MIN_SYNC_COMMITTEE_PARTICIPANTS) { throw Error("Sync committee has not sufficient participants"); @@ -78,11 +70,10 @@ export function validateLightClientUpdate( } if ( - !isValidMerkleBranch( + !isValidNormalizedMerkleBranch( finalizedRoot, update.finalityBranch, - isElectraLightClientUpdate(update) ? FINALIZED_ROOT_DEPTH_ELECTRA : FINALIZED_ROOT_DEPTH, - isElectraLightClientUpdate(update) ? FINALIZED_ROOT_INDEX_ELECTRA : FINALIZED_ROOT_INDEX, + finalizedRootGindexAtFork(attestedFork), update.attestedHeader.beacon.stateRoot ) ) { @@ -98,11 +89,10 @@ export function validateLightClientUpdate( } } else { if ( - !isValidMerkleBranch( + !isValidNormalizedMerkleBranch( ssz.altair.SyncCommittee.hashTreeRoot(update.nextSyncCommittee), update.nextSyncCommitteeBranch, - isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_DEPTH_ELECTRA : NEXT_SYNC_COMMITTEE_DEPTH, - isElectraLightClientUpdate(update) ? NEXT_SYNC_COMMITTEE_INDEX_ELECTRA : NEXT_SYNC_COMMITTEE_INDEX, + nextSyncCommitteeGindexAtFork(attestedFork), update.attestedHeader.beacon.stateRoot ) ) { diff --git a/packages/state-transition/src/lightClient/types.ts b/packages/state-transition/src/lightClient/types.ts index b9723df501b3..96af0b165c3e 100644 --- a/packages/state-transition/src/lightClient/types.ts +++ b/packages/state-transition/src/lightClient/types.ts @@ -26,8 +26,10 @@ * ``` */ export type SyncCommitteeWitness = { - /** Vector[Bytes32, 4] or Vector[Bytes32, 5] depending on the fork */ + /** Shared witness for pre-Gloas forks where current and next sync committees are siblings. */ witness: Uint8Array[]; currentSyncCommitteeRoot: Uint8Array; nextSyncCommitteeRoot: Uint8Array; + currentSyncCommitteeBranch?: Uint8Array[]; + nextSyncCommitteeBranch?: Uint8Array[]; }; diff --git a/packages/state-transition/src/metrics.ts b/packages/state-transition/src/metrics.ts index 831199a1213e..7fabc7992e64 100644 --- a/packages/state-transition/src/metrics.ts +++ b/packages/state-transition/src/metrics.ts @@ -159,11 +159,9 @@ export function onPostStateMetrics(postState: CachedBeaconStateAllForks, metrics // This cache is populated during epoch transition, and should be preserved for performance. // If the cache is missing too often, means that our clone strategy is not working well. function isValidatorsNodesPopulated(state: CachedBeaconStateAllForks): boolean { - // biome-ignore lint/complexity/useLiteralKeys: It is a private attribute - return state.validators["nodesPopulated"] === true; + return (state.validators as unknown as {nodesPopulated?: boolean}).nodesPopulated === true; } function isBalancesNodesPopulated(state: CachedBeaconStateAllForks): boolean { - // biome-ignore lint/complexity/useLiteralKeys: It is a private attribute - return state.balances["nodesPopulated"] === true; + return (state.balances as unknown as {nodesPopulated?: boolean}).nodesPopulated === true; } diff --git a/packages/state-transition/src/slot/index.ts b/packages/state-transition/src/slot/index.ts index e373343fda3c..6169aa7828b3 100644 --- a/packages/state-transition/src/slot/index.ts +++ b/packages/state-transition/src/slot/index.ts @@ -9,7 +9,12 @@ export {upgradeStateToCapella} from "./upgradeStateToCapella.js"; export {upgradeStateToDeneb} from "./upgradeStateToDeneb.js"; export {upgradeStateToElectra} from "./upgradeStateToElectra.js"; export {upgradeStateToFulu} from "./upgradeStateToFulu.js"; -export {upgradeStateToGloas} from "./upgradeStateToGloas.js"; +export { + upgradeAttestationToGloas, + upgradeAttesterSlashingToGloas, + upgradeIndexedAttestationToGloas, + upgradeStateToGloas, +} from "./upgradeStateToGloas.js"; /** * Dial state to next slot. Common for all forks diff --git a/packages/state-transition/src/slot/upgradeStateToGloas.ts b/packages/state-transition/src/slot/upgradeStateToGloas.ts index f50341430614..2e21a3843c09 100644 --- a/packages/state-transition/src/slot/upgradeStateToGloas.ts +++ b/packages/state-transition/src/slot/upgradeStateToGloas.ts @@ -1,5 +1,7 @@ +import {BranchNode, LeafNode, Node} from "@chainsafe/persistent-merkle-tree"; +import {BitArray, progressiveSubtreeFillToContents} from "@chainsafe/ssz"; import {PAYLOAD_BUILDER_VERSION, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; -import {ssz} from "@lodestar/types"; +import {electra, gloas, ssz} from "@lodestar/types"; import {toPubkeyHex} from "@lodestar/utils"; import {isValidDepositSignature} from "../block/processDeposit.js"; import {getCachedBeaconState} from "../cache/stateCache.js"; @@ -34,17 +36,21 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea stateGloasView.eth1Data = stateGloasCloned.eth1Data; stateGloasView.eth1DataVotes = stateGloasCloned.eth1DataVotes; stateGloasView.eth1DepositIndex = stateGloasCloned.eth1DepositIndex; - stateGloasView.validators = stateGloasCloned.validators; - stateGloasView.balances = stateGloasCloned.balances; + stateGloasView.validators = migrateCompositeListToGloas(stateGloasCloned.validators, ssz.gloas.Validators); + stateGloasView.balances = ssz.gloas.Balances.toViewDU(stateGloasCloned.balances.getAll()); stateGloasView.randaoMixes = stateGloasCloned.randaoMixes; stateGloasView.slashings = stateGloasCloned.slashings; - stateGloasView.previousEpochParticipation = stateGloasCloned.previousEpochParticipation; - stateGloasView.currentEpochParticipation = stateGloasCloned.currentEpochParticipation; + stateGloasView.previousEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + stateGloasCloned.previousEpochParticipation.getAll() + ); + stateGloasView.currentEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + stateGloasCloned.currentEpochParticipation.getAll() + ); stateGloasView.justificationBits = stateGloasCloned.justificationBits; stateGloasView.previousJustifiedCheckpoint = stateGloasCloned.previousJustifiedCheckpoint; stateGloasView.currentJustifiedCheckpoint = stateGloasCloned.currentJustifiedCheckpoint; stateGloasView.finalizedCheckpoint = stateGloasCloned.finalizedCheckpoint; - stateGloasView.inactivityScores = stateGloasCloned.inactivityScores; + stateGloasView.inactivityScores = ssz.gloas.InactivityScores.toViewDU(stateGloasCloned.inactivityScores.getAll()); stateGloasView.currentSyncCommittee = stateGloasCloned.currentSyncCommittee; stateGloasView.nextSyncCommittee = stateGloasCloned.nextSyncCommittee; stateGloasView.latestExecutionPayloadBid.blockHash = stateFulu.latestExecutionPayloadHeader.blockHash; @@ -61,9 +67,18 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea stateGloasView.earliestExitEpoch = stateGloasCloned.earliestExitEpoch; stateGloasView.consolidationBalanceToConsume = stateGloasCloned.consolidationBalanceToConsume; stateGloasView.earliestConsolidationEpoch = stateGloasCloned.earliestConsolidationEpoch; - stateGloasView.pendingDeposits = stateGloasCloned.pendingDeposits; - stateGloasView.pendingPartialWithdrawals = stateGloasCloned.pendingPartialWithdrawals; - stateGloasView.pendingConsolidations = stateGloasCloned.pendingConsolidations; + stateGloasView.pendingDeposits = migrateCompositeListToGloas( + stateGloasCloned.pendingDeposits, + ssz.gloas.PendingDeposits + ); + stateGloasView.pendingPartialWithdrawals = migrateCompositeListToGloas( + stateGloasCloned.pendingPartialWithdrawals, + ssz.gloas.PendingPartialWithdrawals + ); + stateGloasView.pendingConsolidations = migrateCompositeListToGloas( + stateGloasCloned.pendingConsolidations, + ssz.gloas.PendingConsolidations + ); stateGloasView.proposerLookahead = stateGloasCloned.proposerLookahead; stateGloasView.ptcWindow = ssz.gloas.PtcWindow.toViewDU(initializePtcWindow(stateFulu)); @@ -85,6 +100,73 @@ export function upgradeStateToGloas(stateFulu: CachedBeaconStateFulu): CachedBea return stateGloas; } +export function upgradeAttestationToGloas(pre: electra.Attestation): gloas.Attestation { + return { + aggregationBits: cloneBitArray(pre.aggregationBits), + data: pre.data, + signature: pre.signature, + committeeBits: cloneBitArray(pre.committeeBits), + }; +} + +export function upgradeIndexedAttestationToGloas(pre: electra.IndexedAttestation): gloas.IndexedAttestation; +export function upgradeIndexedAttestationToGloas(pre: electra.IndexedAttestationBigint): gloas.IndexedAttestationBigint; +export function upgradeIndexedAttestationToGloas( + pre: electra.IndexedAttestation | electra.IndexedAttestationBigint +): gloas.IndexedAttestation | gloas.IndexedAttestationBigint { + if (isIndexedAttestationBigint(pre)) { + return { + attestingIndices: [...pre.attestingIndices], + data: pre.data, + signature: pre.signature, + }; + } + + return { + attestingIndices: [...pre.attestingIndices], + data: pre.data, + signature: pre.signature, + }; +} + +export function upgradeAttesterSlashingToGloas(pre: electra.AttesterSlashing): gloas.AttesterSlashing { + return { + attestation1: upgradeIndexedAttestationToGloas(pre.attestation1), + attestation2: upgradeIndexedAttestationToGloas(pre.attestation2), + }; +} + +function cloneBitArray(bitArray: BitArray): BitArray { + return bitArray.clone(); +} + +function isIndexedAttestationBigint( + attestation: electra.IndexedAttestation | electra.IndexedAttestationBigint +): attestation is electra.IndexedAttestationBigint { + return typeof attestation.data.slot === "bigint"; +} + +/** + * Migrate a composite list from fulu to its gloas progressive-list equivalent by reusing the fulu + * list's cached element nodes. + * + * Works whenever the element type is identical across the fork (e.g. validators use ValidatorNodeStruct, + * the pending* queues use the same electra element types). Each element's cached subtree root is then + * valid under gloas, so only the progressive list superstructure is rebuilt and a subsequent + * hashTreeRoot() skips re-hashing every element — the dominant cost for large lists like validators. + * Much cheaper than `gloasType.toViewDU(fuluList.getAllReadonlyValues())`, which decodes every element + * to a value and forces a full re-hash. + */ +function migrateCompositeListToGloas( + fuluList: {getAllReadonly(): {node: Node}[]}, + gloasType: {getViewDU(node: Node): V} +): V { + const elementNodes = fuluList.getAllReadonly().map((v) => v.node); + const chunksNode = progressiveSubtreeFillToContents(elementNodes); + const rootNode = new BranchNode(chunksNode, LeafNode.fromUint32(elementNodes.length)); + return gloasType.getViewDU(rootNode); +} + /** * Applies any pending deposits for builders to onboard builders during the fork transition * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.8/specs/gloas/fork.md#new-onboard_builders_from_pending_deposits @@ -94,7 +176,7 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void // at the fork, so every builder pubkey here is one added in an earlier iteration. const builderPubkeys = new Set(); - const pendingDeposits = ssz.electra.PendingDeposits.defaultViewDU(); + const pendingDeposits = ssz.gloas.PendingDeposits.defaultViewDU(); const pendingDepositsLookup = PendingDepositsLookup.buildEmpty(); for (let i = 0; i < state.pendingDeposits.length; i++) { diff --git a/packages/state-transition/src/util/execution.ts b/packages/state-transition/src/util/execution.ts index 63da17a17d52..cdd39d48fce6 100644 --- a/packages/state-transition/src/util/execution.ts +++ b/packages/state-transition/src/util/execution.ts @@ -9,6 +9,7 @@ import { bellatrix, capella, deneb, + gloas, isBlindedBeaconBlockBody, isExecutionPayload, ssz, @@ -127,7 +128,10 @@ export function isCapellaPayloadHeader( } export function executionPayloadToPayloadHeader(fork: ForkSeq, payload: ExecutionPayload): ExecutionPayloadHeader { - const transactionsRoot = ssz.bellatrix.Transactions.hashTreeRoot(payload.transactions); + const transactionsRoot = + fork >= ForkSeq.gloas + ? ssz.gloas.Transactions.hashTreeRoot((payload as gloas.ExecutionPayload).transactions) + : ssz.bellatrix.Transactions.hashTreeRoot(payload.transactions); const bellatrixPayloadFields: ExecutionPayloadHeader = { parentHash: payload.parentHash, @@ -147,9 +151,10 @@ export function executionPayloadToPayloadHeader(fork: ForkSeq, payload: Executio }; if (fork >= ForkSeq.capella) { - (bellatrixPayloadFields as capella.ExecutionPayloadHeader).withdrawalsRoot = ssz.capella.Withdrawals.hashTreeRoot( - (payload as capella.ExecutionPayload).withdrawals - ); + (bellatrixPayloadFields as capella.ExecutionPayloadHeader).withdrawalsRoot = + fork >= ForkSeq.gloas + ? ssz.gloas.Withdrawals.hashTreeRoot((payload as gloas.ExecutionPayload).withdrawals) + : ssz.capella.Withdrawals.hashTreeRoot((payload as capella.ExecutionPayload).withdrawals); } if (fork >= ForkSeq.deneb) { diff --git a/packages/state-transition/test/unit/block/processBuilderDepositRequest.test.ts b/packages/state-transition/test/unit/block/processBuilderDepositRequest.test.ts index 1d70914fad28..e4cde9d97de2 100644 --- a/packages/state-transition/test/unit/block/processBuilderDepositRequest.test.ts +++ b/packages/state-transition/test/unit/block/processBuilderDepositRequest.test.ts @@ -1,7 +1,13 @@ import {beforeEach, describe, expect, it, vi} from "vitest"; import {createBeaconConfig} from "@lodestar/config"; import {getConfig} from "@lodestar/config/test-utils"; -import {BUILDER_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; +import { + BUILDER_WITHDRAWAL_PREFIX, + FAR_FUTURE_EPOCH, + ForkName, + PAYLOAD_BUILDER_VERSION, + SLOTS_PER_EPOCH, +} from "@lodestar/params"; import {ssz} from "@lodestar/types"; const isValidBuilderDepositSignatureMock = vi.hoisted(() => @@ -49,9 +55,12 @@ function buildGloasState(slot = 0) { ); } -function makeBuilderWithdrawalCredentials(executionAddress: Uint8Array): Uint8Array { +function makeBuilderWithdrawalCredentials( + executionAddress: Uint8Array, + prefix = BUILDER_WITHDRAWAL_PREFIX +): Uint8Array { const creds = new Uint8Array(32); - creds[0] = BUILDER_WITHDRAWAL_PREFIX; + creds[0] = prefix; creds.set(executionAddress, 12); return creds; } @@ -61,17 +70,21 @@ function makeBuilderDepositRequest({ executionAddress = Uint8Array.from({length: 20}, (_, i) => i + 1), amount = 1_000_000_000, signatureFirstByte = 1, // 1 => valid via mock, anything else => invalid + withdrawalCredentials, + prefix = BUILDER_WITHDRAWAL_PREFIX, }: { pubkey?: Uint8Array; executionAddress?: Uint8Array; amount?: number; signatureFirstByte?: number; + withdrawalCredentials?: Uint8Array; + prefix?: number; } = {}) { const signature = new Uint8Array(96); signature[0] = signatureFirstByte; return { pubkey, - withdrawalCredentials: makeBuilderWithdrawalCredentials(executionAddress), + withdrawalCredentials: withdrawalCredentials ?? makeBuilderWithdrawalCredentials(executionAddress, prefix), amount, signature, }; @@ -95,10 +108,20 @@ describe("processBuilderDepositRequest", () => { const builder = state.builders.get(0); expect(builder.balance).toBe(32_000_000_000); expect(builder.executionAddress).toEqual(request.withdrawalCredentials.subarray(12)); - expect(builder.version).toBe(BUILDER_WITHDRAWAL_PREFIX); + expect(builder.version).toBe(PAYLOAD_BUILDER_VERSION); expect(builder.withdrawableEpoch).toBe(FAR_FUTURE_EPOCH); }); + it("drops a new builder request when the withdrawal credentials prefix is not the builder prefix", () => { + const state = buildGloasState(1); + const request = makeBuilderDepositRequest({prefix: 0x01}); + + processBuilderDepositRequest(state, request); + + expect(isValidBuilderDepositSignatureMock).not.toHaveBeenCalled(); + expect(state.builders.length).toBe(0); + }); + it("drops the request when PoP is invalid", () => { const state = buildGloasState(1); const request = makeBuilderDepositRequest({signatureFirstByte: 0}); @@ -113,12 +136,11 @@ describe("processBuilderDepositRequest", () => { const state = buildGloasState(SLOTS_PER_EPOCH); const pubkey = Uint8Array.from({length: 48}, (_, i) => i + 1); const originalAddress = Uint8Array.from({length: 20}, (_, i) => i + 1); - const originalCreds = makeBuilderWithdrawalCredentials(originalAddress); state.builders.push( ssz.gloas.Builder.toViewDU({ pubkey, - version: originalCreds[0], + version: PAYLOAD_BUILDER_VERSION, executionAddress: originalAddress, balance: 32_000_000_000, depositEpoch: 0, @@ -126,8 +148,8 @@ describe("processBuilderDepositRequest", () => { }) ); - // Attacker-shaped top-up: same pubkey but different (would-be) execution address. Top-ups - // must ignore the request's withdrawal credentials and signature entirely. + // Attacker-shaped top-up: same pubkey but different (would-be) execution address. Valid + // top-ups must ignore the request's execution address and signature. const attackerAddress = Uint8Array.from({length: 20}, () => 0xff); const request = makeBuilderDepositRequest({ pubkey, @@ -143,22 +165,54 @@ describe("processBuilderDepositRequest", () => { const builder = state.builders.get(0); expect(builder.balance).toBe(33_000_000_000); expect(builder.executionAddress).toEqual(originalAddress); - expect(builder.version).toBe(BUILDER_WITHDRAWAL_PREFIX); + expect(builder.version).toBe(PAYLOAD_BUILDER_VERSION); + }); + + it("drops a top-up when the withdrawal credentials prefix is not the builder prefix", () => { + const state = buildGloasState(SLOTS_PER_EPOCH); + const pubkey = Uint8Array.from({length: 48}, (_, i) => i + 1); + const executionAddress = Uint8Array.from({length: 20}, (_, i) => i + 1); + + state.builders.push( + ssz.gloas.Builder.toViewDU({ + pubkey, + version: PAYLOAD_BUILDER_VERSION, + executionAddress, + balance: 32_000_000_000, + depositEpoch: 0, + withdrawableEpoch: FAR_FUTURE_EPOCH, + }) + ); + + const request = makeBuilderDepositRequest({ + pubkey, + amount: 1_000_000_000, + prefix: 0x01, + }); + + processBuilderDepositRequest(state, request); + + expect(isValidBuilderDepositSignatureMock).not.toHaveBeenCalled(); + expect(state.builders.length).toBe(1); + const builder = state.builders.get(0); + expect(builder.balance).toBe(32_000_000_000); + expect(builder.executionAddress).toEqual(executionAddress); + expect(builder.version).toBe(PAYLOAD_BUILDER_VERSION); }); - it("resets the withdrawable epoch when topping up an exited builder", () => { + it("resets the withdrawable epoch when topping up an exited, fully-swept builder", () => { const slot = SLOTS_PER_EPOCH * 2; const state = buildGloasState(slot); const pubkey = Uint8Array.from({length: 48}, (_, i) => i + 1); const executionAddress = Uint8Array.from({length: 20}, (_, i) => i + 1); - // Exited builder: finite withdrawableEpoch + // Exited and fully swept builder: finite withdrawableEpoch, zero balance state.builders.push( ssz.gloas.Builder.toViewDU({ pubkey, - version: BUILDER_WITHDRAWAL_PREFIX, + version: PAYLOAD_BUILDER_VERSION, executionAddress, - balance: 1_000_000_000, + balance: 0, depositEpoch: 0, withdrawableEpoch: 1, }) @@ -169,8 +223,36 @@ describe("processBuilderDepositRequest", () => { processBuilderDepositRequest(state, request); const builder = state.builders.get(0); - expect(builder.balance).toBe(2_000_000_000); + expect(builder.balance).toBe(1_000_000_000); const currentEpoch = Math.floor(slot / SLOTS_PER_EPOCH); expect(builder.withdrawableEpoch).toBe(currentEpoch + state.config.MIN_BUILDER_WITHDRAWABILITY_DELAY); }); + + it("does not reset the withdrawable epoch when topping up an exited builder with nonzero balance", () => { + const slot = SLOTS_PER_EPOCH * 2; + const state = buildGloasState(slot); + const pubkey = Uint8Array.from({length: 48}, (_, i) => i + 1); + const executionAddress = Uint8Array.from({length: 20}, (_, i) => i + 1); + + // Exited builder that has NOT been swept: finite withdrawableEpoch, nonzero balance. + // Per spec, the reset is gated on balance == 0, so the withdrawableEpoch must be preserved. + state.builders.push( + ssz.gloas.Builder.toViewDU({ + pubkey, + version: PAYLOAD_BUILDER_VERSION, + executionAddress, + balance: 1_000_000_000, + depositEpoch: 0, + withdrawableEpoch: 1, + }) + ); + + const request = makeBuilderDepositRequest({pubkey, executionAddress, amount: 1_000_000_000}); + + processBuilderDepositRequest(state, request); + + const builder = state.builders.get(0); + expect(builder.balance).toBe(2_000_000_000); + expect(builder.withdrawableEpoch).toBe(1); + }); }); diff --git a/packages/state-transition/test/unit/block/processBuilderExitRequest.test.ts b/packages/state-transition/test/unit/block/processBuilderExitRequest.test.ts index a129057b5318..1e8ca60cf201 100644 --- a/packages/state-transition/test/unit/block/processBuilderExitRequest.test.ts +++ b/packages/state-transition/test/unit/block/processBuilderExitRequest.test.ts @@ -1,7 +1,7 @@ import {describe, expect, it} from "vitest"; import {createBeaconConfig} from "@lodestar/config"; import {getConfig} from "@lodestar/config/test-utils"; -import {BUILDER_WITHDRAWAL_PREFIX, FAR_FUTURE_EPOCH, ForkName, SLOTS_PER_EPOCH} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, ForkName, PAYLOAD_BUILDER_VERSION, SLOTS_PER_EPOCH} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {processBuilderExitRequest} from "../../../src/block/processBuilderExitRequest.js"; import {createCachedBeaconState, createPubkeyCache} from "../../../src/index.js"; @@ -53,7 +53,7 @@ function pushBuilder( state.builders.push( ssz.gloas.Builder.toViewDU({ pubkey, - version: BUILDER_WITHDRAWAL_PREFIX, + version: PAYLOAD_BUILDER_VERSION, executionAddress, balance, depositEpoch, diff --git a/packages/state-transition/test/unit/epoch/processParticipationFlagUpdates.test.ts b/packages/state-transition/test/unit/epoch/processParticipationFlagUpdates.test.ts new file mode 100644 index 000000000000..bb449d44c6ff --- /dev/null +++ b/packages/state-transition/test/unit/epoch/processParticipationFlagUpdates.test.ts @@ -0,0 +1,25 @@ +import {describe, expect, it} from "vitest"; +import {ssz} from "@lodestar/types"; +import {processParticipationFlagUpdates} from "../../../src/epoch/processParticipationFlagUpdates.js"; +import type {CachedBeaconStateAltair} from "../../../src/types.js"; + +describe("processParticipationFlagUpdates", () => { + it("preserves Gloas progressive participation trees", () => { + const validatorCount = 64; + const state = ssz.gloas.BeaconState.defaultViewDU(); + state.previousEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + Array.from({length: validatorCount}, () => 1) + ); + state.currentEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + Array.from({length: validatorCount}, () => 7) + ); + + processParticipationFlagUpdates(state as unknown as CachedBeaconStateAltair); + + expect(state.previousEpochParticipation.getAll()).toEqual(Array.from({length: validatorCount}, () => 7)); + expect(state.currentEpochParticipation.getAll()).toEqual(Array.from({length: validatorCount}, () => 0)); + expect(ssz.gloas.EpochParticipation.toValueFromViewDU(state.currentEpochParticipation)).toEqual( + Array.from({length: validatorCount}, () => 0) + ); + }); +}); diff --git a/packages/state-transition/test/unit/epoch/processRewardsAndPenalties.test.ts b/packages/state-transition/test/unit/epoch/processRewardsAndPenalties.test.ts new file mode 100644 index 000000000000..2749bbb2f1af --- /dev/null +++ b/packages/state-transition/test/unit/epoch/processRewardsAndPenalties.test.ts @@ -0,0 +1,62 @@ +import {describe, expect, it} from "vitest"; +import {createBeaconConfig, createChainForkConfig} from "@lodestar/config"; +import {FAR_FUTURE_EPOCH, MAX_EFFECTIVE_BALANCE, SLOTS_PER_EPOCH} from "@lodestar/params"; +import {ssz} from "@lodestar/types"; +import {beforeProcessEpoch} from "../../../src/cache/epochTransitionCache.js"; +import {createPubkeyCache} from "../../../src/cache/pubkeyCache.js"; +import {createCachedBeaconState} from "../../../src/cache/stateCache.js"; +import {processRewardsAndPenalties} from "../../../src/epoch/processRewardsAndPenalties.js"; + +describe("processRewardsAndPenalties", () => { + it("preserves Gloas progressive balances tree", () => { + const state = ssz.gloas.BeaconState.defaultViewDU(); + state.slot = SLOTS_PER_EPOCH * 2; + + const validatorCount = 8; + state.validators = ssz.gloas.Validators.toViewDU( + Array.from({length: validatorCount}, (_, i) => ({ + ...ssz.phase0.Validator.defaultValue(), + pubkey: Buffer.alloc(48, i + 1), + effectiveBalance: MAX_EFFECTIVE_BALANCE, + activationEligibilityEpoch: 0, + activationEpoch: 0, + exitEpoch: FAR_FUTURE_EPOCH, + withdrawableEpoch: FAR_FUTURE_EPOCH, + })) + ); + state.balances = ssz.gloas.Balances.toViewDU(Array.from({length: validatorCount}, () => MAX_EFFECTIVE_BALANCE)); + state.previousEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + Array.from({length: validatorCount}, () => 0) + ); + state.currentEpochParticipation = ssz.gloas.EpochParticipation.toViewDU( + Array.from({length: validatorCount}, () => 0) + ); + state.inactivityScores = ssz.gloas.InactivityScores.toViewDU(Array.from({length: validatorCount}, () => 0)); + + const config = createBeaconConfig( + createChainForkConfig({ + ALTAIR_FORK_EPOCH: 0, + BELLATRIX_FORK_EPOCH: 0, + CAPELLA_FORK_EPOCH: 0, + DENEB_FORK_EPOCH: 0, + ELECTRA_FORK_EPOCH: 0, + FULU_FORK_EPOCH: 0, + GLOAS_FORK_EPOCH: 0, + }), + state.genesisValidatorsRoot + ); + const cachedState = createCachedBeaconState( + state, + {config, pubkeyCache: createPubkeyCache()}, + {skipSyncPubkeys: true, skipSyncCommitteeCache: true} + ); + const cache = beforeProcessEpoch(cachedState); + + processRewardsAndPenalties(cachedState, cache); + cachedState.commit(); + cachedState["clearCache"](); + + expect(cachedState.balances.get(0)).toBeGreaterThanOrEqual(0); + expect(cachedState.balances.get(validatorCount - 1)).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/packages/state-transition/test/unit/upgradeState.test.ts b/packages/state-transition/test/unit/upgradeState.test.ts index 573d819e4ab6..17c2552c4bf2 100644 --- a/packages/state-transition/test/unit/upgradeState.test.ts +++ b/packages/state-transition/test/unit/upgradeState.test.ts @@ -1,12 +1,19 @@ import {describe, expect, it} from "vitest"; +import {BitArray} from "@chainsafe/ssz"; import {ChainForkConfig, createBeaconConfig, createChainForkConfig} from "@lodestar/config"; import {config as chainConfig} from "@lodestar/config/default"; -import {ForkName} from "@lodestar/params"; +import {FAR_FUTURE_EPOCH, ForkName} from "@lodestar/params"; import {ssz} from "@lodestar/types"; import {createPubkeyCache} from "../../src/cache/pubkeyCache.js"; -import {createCachedBeaconState} from "../../src/cache/stateCache.js"; +import {CachedBeaconStateFulu, createCachedBeaconState} from "../../src/cache/stateCache.js"; import {upgradeStateToDeneb} from "../../src/slot/upgradeStateToDeneb.js"; import {upgradeStateToElectra} from "../../src/slot/upgradeStateToElectra.js"; +import { + upgradeAttestationToGloas, + upgradeAttesterSlashingToGloas, + upgradeIndexedAttestationToGloas, + upgradeStateToGloas, +} from "../../src/slot/upgradeStateToGloas.js"; describe("upgradeState", () => { it("upgradeStateToDeneb", () => { @@ -37,6 +44,127 @@ describe("upgradeState", () => { const newState = upgradeStateToElectra(stateView); expect(() => newState.toValue()).not.toThrow(); }); + + it("upgradeStateToGloas reuses composite-list nodes with identical merkle roots", () => { + // Enough validators to span multiple progressive subtrees (capacities 1, 4, 16, 64, ...) and to + // populate every slot's committee for the gloas PTC window computed during the upgrade. + const numValidators = 128; + const fuluStateView = ssz.fulu.BeaconState.defaultViewDU(); + for (let i = 0; i < numValidators; i++) { + const validator = ssz.phase0.Validator.defaultValue(); + // Distinct pubkey/withdrawalCredentials so each validator has a distinct subtree root + validator.pubkey = Buffer.alloc(48, i + 1); + validator.withdrawalCredentials = Buffer.alloc(32, i + 1); + validator.effectiveBalance = 32e9; + // Active at epoch 0 so shuffling / PTC committee selection has a non-empty validator set + validator.activationEligibilityEpoch = 0; + validator.activationEpoch = 0; + validator.exitEpoch = FAR_FUTURE_EPOCH; + validator.withdrawableEpoch = FAR_FUTURE_EPOCH; + fuluStateView.validators.push(ssz.phase0.Validator.toViewDU(validator)); + fuluStateView.balances.push(32e9); + fuluStateView.previousEpochParticipation.push(0); + fuluStateView.currentEpochParticipation.push(0); + fuluStateView.inactivityScores.push(0); + } + + // Populate the pending* composite queues so the node-reuse path is exercised for them too. + // Non-builder withdrawal credentials (default zeros) keep the deposits pending in-order. + for (let i = 0; i < 5; i++) { + const pendingDeposit = ssz.electra.PendingDeposit.defaultValue(); + pendingDeposit.amount = 1000 + i; + fuluStateView.pendingDeposits.push(ssz.electra.PendingDeposit.toViewDU(pendingDeposit)); + const pendingPartialWithdrawal = ssz.electra.PendingPartialWithdrawal.defaultValue(); + pendingPartialWithdrawal.amount = BigInt(2000 + i); + fuluStateView.pendingPartialWithdrawals.push( + ssz.electra.PendingPartialWithdrawal.toViewDU(pendingPartialWithdrawal) + ); + const pendingConsolidation = ssz.electra.PendingConsolidation.defaultValue(); + pendingConsolidation.sourceIndex = i; + fuluStateView.pendingConsolidations.push(ssz.electra.PendingConsolidation.toViewDU(pendingConsolidation)); + } + fuluStateView.commit(); + + const config = getConfig(ForkName.fulu); + const fuluState = createCachedBeaconState( + fuluStateView, + { + config: createBeaconConfig(config, fuluStateView.genesisValidatorsRoot), + pubkeyCache: createPubkeyCache(), + }, + // dummy pubkeys aren't valid BLS keys; skip syncing (no builder deposits need the cache) + {skipSyncCommitteeCache: true, skipSyncPubkeys: true} + ) as CachedBeaconStateFulu; + + // Reference gloas roots via the value-based path (what the node-reuse replaces) + const expectedValidatorsRoot = ssz.gloas.Validators.hashTreeRoot(fuluState.validators.getAllReadonlyValues()); + const expectedPendingDepositsRoot = ssz.gloas.PendingDeposits.hashTreeRoot( + fuluState.pendingDeposits.getAllReadonlyValues() + ); + const expectedPendingPartialWithdrawalsRoot = ssz.gloas.PendingPartialWithdrawals.hashTreeRoot( + fuluState.pendingPartialWithdrawals.getAllReadonlyValues() + ); + const expectedPendingConsolidationsRoot = ssz.gloas.PendingConsolidations.hashTreeRoot( + fuluState.pendingConsolidations.getAllReadonlyValues() + ); + + const gloasState = upgradeStateToGloas(fuluState); + + // Node-reuse must produce byte-identical merkle roots for every migrated composite list + expect(gloasState.validators.hashTreeRoot()).toEqual(expectedValidatorsRoot); + expect(gloasState.validators.length).toEqual(numValidators); + expect(gloasState.pendingDeposits.hashTreeRoot()).toEqual(expectedPendingDepositsRoot); + expect(gloasState.pendingPartialWithdrawals.hashTreeRoot()).toEqual(expectedPendingPartialWithdrawalsRoot); + expect(gloasState.pendingConsolidations.hashTreeRoot()).toEqual(expectedPendingConsolidationsRoot); + // Full state still merkleizes and round-trips + expect(() => gloasState.hashTreeRoot()).not.toThrow(); + expect(() => gloasState.toValue()).not.toThrow(); + }); + + it("upgradeAttestationToGloas copies Fulu attestation bitlists into Gloas values", () => { + const attestation = ssz.electra.Attestation.defaultValue(); + attestation.aggregationBits = BitArray.fromBitLen(4); + attestation.aggregationBits.set(0, true); + attestation.committeeBits.set(0, true); + + const upgraded = upgradeAttestationToGloas(attestation); + + expect(upgraded).toEqual(attestation); + expect(upgraded.aggregationBits).not.toBe(attestation.aggregationBits); + expect(upgraded.committeeBits).not.toBe(attestation.committeeBits); + expect(ssz.gloas.Attestation.deserialize(ssz.gloas.Attestation.serialize(upgraded))).toEqual(upgraded); + + attestation.aggregationBits.set(0, false); + attestation.committeeBits.set(0, false); + expect(upgraded.aggregationBits.get(0)).toBe(true); + expect(upgraded.committeeBits.get(0)).toBe(true); + }); + + it("upgradeIndexedAttestationToGloas copies Fulu attesting indices into Gloas values", () => { + const indexedAttestation = ssz.electra.IndexedAttestation.defaultValue(); + indexedAttestation.attestingIndices = [1, 3, 5]; + + const upgraded = upgradeIndexedAttestationToGloas(indexedAttestation); + + expect(upgraded).toEqual(indexedAttestation); + expect(upgraded.attestingIndices).not.toBe(indexedAttestation.attestingIndices); + expect(ssz.gloas.IndexedAttestation.deserialize(ssz.gloas.IndexedAttestation.serialize(upgraded))).toEqual( + upgraded + ); + }); + + it("upgradeAttesterSlashingToGloas upgrades both indexed attestations", () => { + const attesterSlashing = ssz.electra.AttesterSlashing.defaultValue(); + attesterSlashing.attestation1.attestingIndices = [2, 4]; + attesterSlashing.attestation2.attestingIndices = [6, 8]; + + const upgraded = upgradeAttesterSlashingToGloas(attesterSlashing); + + expect(upgraded).toEqual(attesterSlashing); + expect(upgraded.attestation1.attestingIndices).not.toBe(attesterSlashing.attestation1.attestingIndices); + expect(upgraded.attestation2.attestingIndices).not.toBe(attesterSlashing.attestation2.attestingIndices); + expect(ssz.gloas.AttesterSlashing.deserialize(ssz.gloas.AttesterSlashing.serialize(upgraded))).toEqual(upgraded); + }); }); const ZERO_HASH = Buffer.alloc(32, 0); diff --git a/packages/types/package.json b/packages/types/package.json index 490484c1ef0c..31889e64b60c 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -81,7 +81,7 @@ }, "types": "lib/index.d.ts", "dependencies": { - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@lodestar/params": "workspace:^", "ethereum-cryptography": "^2.0.0" }, diff --git a/packages/types/src/fulu/types.ts b/packages/types/src/fulu/types.ts index cd2c01c3bb97..9aa769df8674 100644 --- a/packages/types/src/fulu/types.ts +++ b/packages/types/src/fulu/types.ts @@ -2,6 +2,7 @@ import {ValueOf} from "@chainsafe/ssz"; import * as ssz from "./sszTypes.js"; export type KZGProof = ValueOf; +export type KZGProofs = ValueOf; export type Blob = ValueOf; export type Metadata = ValueOf; diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index 7aab53b4e2fc..e53dbe2b49ed 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -1,21 +1,24 @@ import { BitVectorType, - ByteListType, ContainerType, ListBasicType, ListCompositeType, + ProgressiveBitListType, + ProgressiveByteListType, + ProgressiveContainerType, + ProgressiveListBasicType, + ProgressiveListCompositeType, VectorBasicType, VectorCompositeType, } from "@chainsafe/ssz"; import { - BUILDER_PENDING_WITHDRAWALS_LIMIT, - BUILDER_REGISTRY_LIMIT, + CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS, + EPOCHS_PER_SYNC_COMMITTEE_PERIOD, + EXECUTION_BLOCK_HASH_DEPTH_GLOAS, + FINALIZED_ROOT_DEPTH_GLOAS, HISTORICAL_ROOTS_LIMIT, - MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD, - MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD, - MAX_BYTES_PER_TRANSACTION, - MAX_PAYLOAD_ATTESTATIONS, MIN_SEED_LOOKAHEAD, + NEXT_SYNC_COMMITTEE_DEPTH_GLOAS, NUMBER_OF_COLUMNS, PTC_SIZE, SLOTS_PER_EPOCH, @@ -47,38 +50,101 @@ const { Uint8, BuilderIndex, EpochInf, + ParticipationFlags, } = primitiveSsz; -export const Builder = new ContainerType( +function activeFields(count: number): boolean[] { + return Array.from({length: count}, () => true); +} + +export const ExecutionBranch = new VectorCompositeType(Bytes32, EXECUTION_BLOCK_HASH_DEPTH_GLOAS); + +export const CurrentSyncCommitteeBranch = new VectorCompositeType(Bytes32, CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS); + +export const FinalityBranch = new VectorCompositeType(Bytes32, FINALIZED_ROOT_DEPTH_GLOAS); + +export const NextSyncCommitteeBranch = new VectorCompositeType(Bytes32, NEXT_SYNC_COMMITTEE_DEPTH_GLOAS); + +export const AggregationBits = new ProgressiveBitListType({typeName: "AggregationBits"}); +export const AttestingIndices = new ProgressiveListBasicType(ValidatorIndex, {typeName: "AttestingIndices"}); +export const Transaction = new ProgressiveByteListType({typeName: "Transaction"}); +export const Transactions = new ProgressiveListCompositeType(Transaction, {typeName: "Transactions"}); +export const Withdrawals = new ProgressiveListCompositeType(capellaSsz.Withdrawal, {typeName: "Withdrawals"}); +export const BlobKzgCommitments = new ProgressiveListCompositeType(denebSsz.KZGCommitment, { + typeName: "BlobKzgCommitments", +}); +export const KZGProofs = new ProgressiveListCompositeType(denebSsz.KZGProof, {typeName: "KZGProofs"}); +export const DataColumn = new ProgressiveListCompositeType(fuluSsz.Cell, {typeName: "DataColumn"}); + +export const Attestation = new ProgressiveContainerType( { - pubkey: BLSPubkey, - version: Uint8, - executionAddress: ExecutionAddress, - balance: UintNum64, - depositEpoch: EpochInf, - withdrawableEpoch: EpochInf, + aggregationBits: AggregationBits, + data: phase0Ssz.AttestationData, + signature: BLSSignature, + committeeBits: electraSsz.CommitteeBits, }, - {typeName: "Builder", jsonCase: "eth2"} + activeFields(4), + {typeName: "Attestation", jsonCase: "eth2"} ); -export const BuilderPendingWithdrawal = new ContainerType( +export const IndexedAttestation = new ProgressiveContainerType( { - feeRecipient: ExecutionAddress, - amount: UintNum64, - builderIndex: BuilderIndex, + attestingIndices: AttestingIndices, + data: phase0Ssz.AttestationData, + signature: BLSSignature, }, - {typeName: "BuilderPendingWithdrawal", jsonCase: "eth2"} + activeFields(3), + {typeName: "IndexedAttestation", jsonCase: "eth2"} ); -export const BuilderPendingPayment = new ContainerType( +/** Same as `IndexedAttestation` but epoch, slot and index are not bounded and must be a bigint */ +export const IndexedAttestationBigint = new ProgressiveContainerType( { - weight: UintNum64, - withdrawal: BuilderPendingWithdrawal, - proposerIndex: ValidatorIndex, + attestingIndices: AttestingIndices, + data: phase0Ssz.AttestationDataBigint, + signature: BLSSignature, }, - {typeName: "BuilderPendingPayment", jsonCase: "eth2"} + activeFields(3), + {typeName: "IndexedAttestation", jsonCase: "eth2"} ); +export const AttesterSlashing = new ContainerType( + { + attestation1: IndexedAttestationBigint, + attestation2: IndexedAttestationBigint, + }, + {typeName: "AttesterSlashing", jsonCase: "eth2"} +); + +export const AggregateAndProof = new ContainerType( + { + aggregatorIndex: ValidatorIndex, + aggregate: Attestation, + selectionProof: BLSSignature, + }, + {typeName: "AggregateAndProof", jsonCase: "eth2", cachePermanentRootStruct: true} +); + +export const SignedAggregateAndProof = new ContainerType( + { + message: AggregateAndProof, + signature: BLSSignature, + }, + {typeName: "SignedAggregateAndProof", jsonCase: "eth2"} +); + +export const DepositRequest = electraSsz.DepositRequest; +export const DepositRequests = new ProgressiveListCompositeType(DepositRequest, {typeName: "DepositRequests"}); +export const WithdrawalRequest = electraSsz.WithdrawalRequest; +export const WithdrawalRequests = new ProgressiveListCompositeType(WithdrawalRequest, { + typeName: "WithdrawalRequests", +}); +export const ConsolidationRequest = electraSsz.ConsolidationRequest; +export const ConsolidationRequests = new ProgressiveListCompositeType(ConsolidationRequest, { + typeName: "ConsolidationRequests", +}); + +// New in GLOAS:EIP8282 export const BuilderDepositRequest = new ContainerType( { pubkey: BLSPubkey, @@ -88,12 +154,11 @@ export const BuilderDepositRequest = new ContainerType( }, {typeName: "BuilderDepositRequest", jsonCase: "eth2"} ); +export const BuilderDepositRequests = new ProgressiveListCompositeType(BuilderDepositRequest, { + typeName: "BuilderDepositRequests", +}); -export const BuilderDepositRequests = new ListCompositeType( - BuilderDepositRequest, - MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD -); - +// New in GLOAS:EIP8282 export const BuilderExitRequest = new ContainerType( { sourceAddress: ExecutionAddress, @@ -101,21 +166,88 @@ export const BuilderExitRequest = new ContainerType( }, {typeName: "BuilderExitRequest", jsonCase: "eth2"} ); +export const BuilderExitRequests = new ProgressiveListCompositeType(BuilderExitRequest, { + typeName: "BuilderExitRequests", +}); -export const BuilderExitRequests = new ListCompositeType(BuilderExitRequest, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD); - -// New in GLOAS:EIP8282 — extends electra ExecutionRequests with builder deposits and exits -export const ExecutionRequests = new ContainerType( +export const ExecutionRequests = new ProgressiveContainerType( { - deposits: electraSsz.DepositRequests, - withdrawals: electraSsz.WithdrawalRequests, - consolidations: electraSsz.ConsolidationRequests, - builderDeposits: BuilderDepositRequests, - builderExits: BuilderExitRequests, + deposits: DepositRequests, + withdrawals: WithdrawalRequests, + consolidations: ConsolidationRequests, + builderDeposits: BuilderDepositRequests, // New in GLOAS:EIP8282 + builderExits: BuilderExitRequests, // New in GLOAS:EIP8282 }, + activeFields(5), {typeName: "ExecutionRequests", jsonCase: "eth2"} ); +export const ProposerSlashings = new ProgressiveListCompositeType(phase0Ssz.ProposerSlashing, { + typeName: "ProposerSlashings", +}); +export const AttesterSlashings = new ProgressiveListCompositeType(AttesterSlashing, { + typeName: "AttesterSlashings", +}); +export const Attestations = new ProgressiveListCompositeType(Attestation, {typeName: "Attestations"}); +export const Deposits = new ProgressiveListCompositeType(phase0Ssz.Deposit, {typeName: "Deposits"}); +export const VoluntaryExits = new ProgressiveListCompositeType(phase0Ssz.SignedVoluntaryExit, { + typeName: "VoluntaryExits", +}); +export const BlsToExecutionChanges = new ProgressiveListCompositeType(capellaSsz.SignedBLSToExecutionChange, { + typeName: "BLSToExecutionChanges", +}); + +export const Validators = new ProgressiveListCompositeType(phase0Ssz.Validator, {typeName: "Validators"}); +export const Balances = new ProgressiveListBasicType(UintNum64, {typeName: "Balances"}); +export const EpochParticipation = new ProgressiveListBasicType(ParticipationFlags, { + typeName: "EpochParticipation", +}); +export const InactivityScores = new ProgressiveListBasicType(UintNum64, {typeName: "InactivityScores"}); +export const PendingDeposits = new ProgressiveListCompositeType(electraSsz.PendingDeposit, { + typeName: "PendingDeposits", +}); +export const PendingPartialWithdrawals = new ProgressiveListCompositeType(electraSsz.PendingPartialWithdrawal, { + typeName: "PendingPartialWithdrawals", +}); +export const PendingConsolidations = new ProgressiveListCompositeType(electraSsz.PendingConsolidation, { + typeName: "PendingConsolidations", +}); + +export const Builder = new ContainerType( + { + pubkey: BLSPubkey, + version: Uint8, + executionAddress: ExecutionAddress, + balance: UintNum64, + depositEpoch: EpochInf, + withdrawableEpoch: EpochInf, + }, + {typeName: "Builder", jsonCase: "eth2"} +); + +export const BuilderPendingWithdrawal = new ContainerType( + { + feeRecipient: ExecutionAddress, + amount: UintNum64, + builderIndex: BuilderIndex, + }, + {typeName: "BuilderPendingWithdrawal", jsonCase: "eth2"} +); + +export const BuilderPendingPayment = new ContainerType( + { + weight: UintNum64, + withdrawal: BuilderPendingWithdrawal, + proposerIndex: ValidatorIndex, + }, + {typeName: "BuilderPendingPayment", jsonCase: "eth2"} +); + +export const Builders = new ProgressiveListCompositeType(Builder, {typeName: "Builders"}); +export const BuilderPendingWithdrawals = new ProgressiveListCompositeType(BuilderPendingWithdrawal, { + typeName: "BuilderPendingWithdrawals", +}); + export const PayloadTimelinessCommittee = new VectorBasicType(ValidatorIndex, PTC_SIZE); export const PtcWindow = new VectorCompositeType( PayloadTimelinessCommittee, @@ -132,15 +264,20 @@ export const PayloadAttestationData = new ContainerType( {typeName: "PayloadAttestationData", jsonCase: "eth2"} ); -export const PayloadAttestation = new ContainerType( +export const PayloadAttestation = new ProgressiveContainerType( { aggregationBits: new BitVectorType(PTC_SIZE), data: PayloadAttestationData, signature: BLSSignature, }, + activeFields(3), {typeName: "PayloadAttestation", jsonCase: "eth2"} ); +export const PayloadAttestations = new ProgressiveListCompositeType(PayloadAttestation, { + typeName: "PayloadAttestations", +}); + export const PayloadAttestationMessage = new ContainerType( { validatorIndex: ValidatorIndex, @@ -150,12 +287,13 @@ export const PayloadAttestationMessage = new ContainerType( {typeName: "PayloadAttestationMessage", jsonCase: "eth2"} ); -export const IndexedPayloadAttestation = new ContainerType( +export const IndexedPayloadAttestation = new ProgressiveContainerType( { attestingIndices: new ListBasicType(ValidatorIndex, PTC_SIZE), data: PayloadAttestationData, signature: BLSSignature, }, + activeFields(3), {typeName: "IndexedPayloadAttestation", jsonCase: "eth2"} ); @@ -178,7 +316,7 @@ export const SignedProposerPreferences = new ContainerType( {typeName: "SignedProposerPreferences", jsonCase: "eth2"} ); -export const ExecutionPayloadBid = new ContainerType( +export const ExecutionPayloadBid = new ProgressiveContainerType( { parentBlockHash: Bytes32, parentBlockRoot: Root, @@ -190,9 +328,10 @@ export const ExecutionPayloadBid = new ContainerType( slot: Slot, value: UintNum64, executionPayment: UintNum64, - blobKzgCommitments: denebSsz.BlobKzgCommitments, + blobKzgCommitments: BlobKzgCommitments, executionRequestsRoot: Root, }, + activeFields(12), {typeName: "ExecutionPayloadBid", jsonCase: "eth2"} ); @@ -204,18 +343,21 @@ export const SignedExecutionPayloadBid = new ContainerType( {typeName: "SignedExecutionPayloadBid", jsonCase: "eth2"} ); -export const BlockAccessList = new ByteListType(MAX_BYTES_PER_TRANSACTION); +export const BlockAccessList = new ProgressiveByteListType({typeName: "BlockAccessList"}); -export const ExecutionPayload = new ContainerType( +export const ExecutionPayload = new ProgressiveContainerType( { ...electraSsz.ExecutionPayload.fields, + transactions: Transactions, + withdrawals: Withdrawals, blockAccessList: BlockAccessList, // New in GLOAS:EIP-7928 slotNumber: Slot, // New in GLOAS:EIP-7843 }, + activeFields(19), {typeName: "ExecutionPayload", jsonCase: "eth2"} ); -export const ExecutionPayloadEnvelope = new ContainerType( +export const ExecutionPayloadEnvelope = new ProgressiveContainerType( { payload: ExecutionPayload, executionRequests: ExecutionRequests, @@ -223,6 +365,7 @@ export const ExecutionPayloadEnvelope = new ContainerType( beaconBlockRoot: Root, parentBeaconBlockRoot: Root, }, + activeFields(5), {typeName: "ExecutionPayloadEnvelope", jsonCase: "eth2"} ); @@ -234,25 +377,35 @@ export const SignedExecutionPayloadEnvelope = new ContainerType( {typeName: "SignedExecutionPayloadEnvelope", jsonCase: "eth2"} ); -export const BeaconBlockBody = new ContainerType( +export const SignedExecutionPayloadEnvelopeContents = new ContainerType( + { + signedExecutionPayloadEnvelope: SignedExecutionPayloadEnvelope, + kzgProofs: fuluSsz.KZGProofs, + blobs: denebSsz.Blobs, + }, + {typeName: "SignedExecutionPayloadEnvelopeContents", jsonCase: "eth2"} +); + +export const BeaconBlockBody = new ProgressiveContainerType( { randaoReveal: phase0Ssz.BeaconBlockBody.fields.randaoReveal, eth1Data: phase0Ssz.BeaconBlockBody.fields.eth1Data, graffiti: phase0Ssz.BeaconBlockBody.fields.graffiti, - proposerSlashings: phase0Ssz.BeaconBlockBody.fields.proposerSlashings, - attesterSlashings: electraSsz.BeaconBlockBody.fields.attesterSlashings, - attestations: electraSsz.BeaconBlockBody.fields.attestations, - deposits: phase0Ssz.BeaconBlockBody.fields.deposits, - voluntaryExits: phase0Ssz.BeaconBlockBody.fields.voluntaryExits, + proposerSlashings: ProposerSlashings, + attesterSlashings: AttesterSlashings, + attestations: Attestations, + deposits: Deposits, + voluntaryExits: VoluntaryExits, syncAggregate: altairSsz.BeaconBlockBody.fields.syncAggregate, // executionPayload: ExecutionPayload, // Removed in GLOAS:EIP7732 - blsToExecutionChanges: capellaSsz.BeaconBlockBody.fields.blsToExecutionChanges, + blsToExecutionChanges: BlsToExecutionChanges, // blobKzgCommitments: denebSsz.BeaconBlockBody.fields.blobKzgCommitments, // Removed in GLOAS:EIP7732 // executionRequests: ExecutionRequests, // Removed in GLOAS:EIP7732 signedExecutionPayloadBid: SignedExecutionPayloadBid, // New in GLOAS:EIP7732 - payloadAttestations: new ListCompositeType(PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS), // New in GLOAS:EIP7732 + payloadAttestations: PayloadAttestations, // New in GLOAS:EIP7732 parentExecutionRequests: ExecutionRequests, // New in GLOAS:EIP7732 }, + activeFields(13), {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -272,7 +425,66 @@ export const SignedBeaconBlock = new ContainerType( {typeName: "SignedBeaconBlock", jsonCase: "eth2"} ); -export const BeaconState = new ContainerType( +export const LightClientHeader = new ContainerType( + { + beacon: phase0Ssz.BeaconBlockHeader, + executionBlockHash: Bytes32, + executionBranch: ExecutionBranch, + }, + {typeName: "LightClientHeader", jsonCase: "eth2"} +); + +export const LightClientBootstrap = new ContainerType( + { + header: LightClientHeader, + currentSyncCommittee: altairSsz.SyncCommittee, + currentSyncCommitteeBranch: CurrentSyncCommitteeBranch, + }, + {typeName: "LightClientBootstrap", jsonCase: "eth2"} +); + +export const LightClientUpdate = new ContainerType( + { + attestedHeader: LightClientHeader, + nextSyncCommittee: altairSsz.SyncCommittee, + nextSyncCommitteeBranch: NextSyncCommitteeBranch, + finalizedHeader: LightClientHeader, + finalityBranch: FinalityBranch, + syncAggregate: altairSsz.SyncAggregate, + signatureSlot: Slot, + }, + {typeName: "LightClientUpdate", jsonCase: "eth2"} +); + +export const LightClientFinalityUpdate = new ContainerType( + { + attestedHeader: LightClientHeader, + finalizedHeader: LightClientHeader, + finalityBranch: FinalityBranch, + syncAggregate: altairSsz.SyncAggregate, + signatureSlot: Slot, + }, + {typeName: "LightClientFinalityUpdate", jsonCase: "eth2"} +); + +export const LightClientOptimisticUpdate = new ContainerType( + { + attestedHeader: LightClientHeader, + syncAggregate: altairSsz.SyncAggregate, + signatureSlot: Slot, + }, + {typeName: "LightClientOptimisticUpdate", jsonCase: "eth2"} +); + +export const LightClientStore = new ContainerType( + { + snapshot: LightClientBootstrap, + validUpdates: new ListCompositeType(LightClientUpdate, EPOCHS_PER_SYNC_COMMITTEE_PERIOD * SLOTS_PER_EPOCH), + }, + {typeName: "LightClientStore", jsonCase: "eth2"} +); + +export const BeaconState = new ProgressiveContainerType( { genesisTime: UintNum64, genesisValidatorsRoot: Root, @@ -289,21 +501,21 @@ export const BeaconState = new ContainerType( eth1DataVotes: phase0Ssz.Eth1DataVotes, eth1DepositIndex: UintNum64, // Registry - validators: phase0Ssz.Validators, - balances: phase0Ssz.Balances, + validators: Validators, + balances: Balances, randaoMixes: phase0Ssz.RandaoMixes, // Slashings slashings: phase0Ssz.Slashings, // Participation - previousEpochParticipation: altairSsz.EpochParticipation, - currentEpochParticipation: altairSsz.EpochParticipation, + previousEpochParticipation: EpochParticipation, + currentEpochParticipation: EpochParticipation, // Finality justificationBits: phase0Ssz.JustificationBits, previousJustifiedCheckpoint: phase0Ssz.Checkpoint, currentJustifiedCheckpoint: phase0Ssz.Checkpoint, finalizedCheckpoint: phase0Ssz.Checkpoint, // Inactivity - inactivityScores: altairSsz.InactivityScores, + inactivityScores: InactivityScores, // Sync currentSyncCommittee: altairSsz.SyncCommittee, nextSyncCommittee: altairSsz.SyncCommittee, @@ -321,28 +533,29 @@ export const BeaconState = new ContainerType( earliestExitEpoch: Epoch, consolidationBalanceToConsume: Gwei, earliestConsolidationEpoch: Epoch, - pendingDeposits: electraSsz.BeaconState.fields.pendingDeposits, - pendingPartialWithdrawals: electraSsz.BeaconState.fields.pendingPartialWithdrawals, - pendingConsolidations: electraSsz.BeaconState.fields.pendingConsolidations, + pendingDeposits: PendingDeposits, + pendingPartialWithdrawals: PendingPartialWithdrawals, + pendingConsolidations: PendingConsolidations, proposerLookahead: fuluSsz.BeaconState.fields.proposerLookahead, - builders: new ListCompositeType(Builder, BUILDER_REGISTRY_LIMIT), // New in GLOAS:EIP7732 + builders: Builders, // New in GLOAS:EIP7732 nextWithdrawalBuilderIndex: BuilderIndex, // New in GLOAS:EIP7732 executionPayloadAvailability: new BitVectorType(SLOTS_PER_HISTORICAL_ROOT), // New in GLOAS:EIP7732 builderPendingPayments: new VectorCompositeType(BuilderPendingPayment, 2 * SLOTS_PER_EPOCH), // New in GLOAS:EIP7732 - builderPendingWithdrawals: new ListCompositeType(BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT), // New in GLOAS:EIP7732 + builderPendingWithdrawals: BuilderPendingWithdrawals, // New in GLOAS:EIP7732 latestExecutionPayloadBid: ExecutionPayloadBid, // New in GLOAS:EIP7732 - payloadExpectedWithdrawals: capellaSsz.Withdrawals, // New in GLOAS:EIP7732 + payloadExpectedWithdrawals: Withdrawals, // New in GLOAS:EIP7732 ptcWindow: PtcWindow, // New in GLOAS:EIP7732 }, + activeFields(46), {typeName: "BeaconState", jsonCase: "eth2"} ); export const DataColumnSidecar = new ContainerType( { index: fuluSsz.DataColumnSidecar.fields.index, - column: fuluSsz.DataColumnSidecar.fields.column, + column: DataColumn, // kzgCommitments: denebSsz.BlobKzgCommitments, // Removed in GLOAS:EIP7732 - kzgProofs: fuluSsz.DataColumnSidecar.fields.kzgProofs, + kzgProofs: KZGProofs, // signedBlockHeader: phase0Ssz.SignedBeaconBlockHeader, // Removed in GLOAS:EIP7732 // kzgCommitmentsInclusionProof: KzgCommitmentsInclusionProof, // Removed in GLOAS:EIP7732 slot: Slot, // New in GLOAS:EIP7732 diff --git a/packages/types/src/gloas/types.ts b/packages/types/src/gloas/types.ts index c562c59e3e60..03802da39840 100644 --- a/packages/types/src/gloas/types.ts +++ b/packages/types/src/gloas/types.ts @@ -1,6 +1,31 @@ import {ValueOf} from "@chainsafe/ssz"; import * as ssz from "./sszTypes.js"; +export type AggregationBits = ValueOf; +export type AttestingIndices = ValueOf; +export type Transaction = ValueOf; +export type Transactions = ValueOf; +export type Withdrawals = ValueOf; +export type BlobKzgCommitments = ValueOf; +export type KZGProofs = ValueOf; +export type DataColumn = ValueOf; + +export type Attestation = ValueOf; +export type IndexedAttestation = ValueOf; +export type IndexedAttestationBigint = ValueOf; +export type AttesterSlashing = ValueOf; + +export type AggregateAndProof = ValueOf; +export type SignedAggregateAndProof = ValueOf; + +export type DepositRequest = ValueOf; +export type DepositRequests = ValueOf; +export type WithdrawalRequest = ValueOf; +export type WithdrawalRequests = ValueOf; +export type ConsolidationRequest = ValueOf; +export type ConsolidationRequests = ValueOf; +export type ExecutionRequests = ValueOf; + export type Builder = ValueOf; export type BuilderPendingWithdrawal = ValueOf; export type BuilderPendingPayment = ValueOf; @@ -8,7 +33,6 @@ export type BuilderDepositRequest = ValueOf; export type BuilderDepositRequests = ValueOf; export type BuilderExitRequest = ValueOf; export type BuilderExitRequests = ValueOf; -export type ExecutionRequests = ValueOf; export type PayloadTimelinessCommittee = ValueOf; export type PtcWindow = ValueOf; export type PayloadAttestationData = ValueOf; @@ -23,9 +47,16 @@ export type SignedExecutionPayloadBid = ValueOf; export type ExecutionPayloadEnvelope = ValueOf; export type SignedExecutionPayloadEnvelope = ValueOf; +export type SignedExecutionPayloadEnvelopeContents = ValueOf; export type BeaconBlockBody = ValueOf; export type BeaconBlock = ValueOf; export type SignedBeaconBlock = ValueOf; +export type LightClientHeader = ValueOf; +export type LightClientBootstrap = ValueOf; +export type LightClientUpdate = ValueOf; +export type LightClientFinalityUpdate = ValueOf; +export type LightClientOptimisticUpdate = ValueOf; +export type LightClientStore = ValueOf; export type BeaconState = ValueOf; export type DataColumnSidecar = ValueOf; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 781f36dcc473..8485aa7ffd5c 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -294,12 +294,12 @@ type TypesByFork = { SignedBeaconBlock: gloas.SignedBeaconBlock; Metadata: fulu.Metadata; Status: fulu.Status; - LightClientHeader: deneb.LightClientHeader; - LightClientBootstrap: electra.LightClientBootstrap; - LightClientUpdate: electra.LightClientUpdate; - LightClientFinalityUpdate: electra.LightClientFinalityUpdate; - LightClientOptimisticUpdate: electra.LightClientOptimisticUpdate; - LightClientStore: electra.LightClientStore; + LightClientHeader: gloas.LightClientHeader; + LightClientBootstrap: gloas.LightClientBootstrap; + LightClientUpdate: gloas.LightClientUpdate; + LightClientFinalityUpdate: gloas.LightClientFinalityUpdate; + LightClientOptimisticUpdate: gloas.LightClientOptimisticUpdate; + LightClientStore: gloas.LightClientStore; BlindedBeaconBlock: electra.BlindedBeaconBlock; BlindedBeaconBlockBody: electra.BlindedBeaconBlockBody; SignedBlindedBeaconBlock: electra.SignedBlindedBeaconBlock; @@ -315,12 +315,12 @@ type TypesByFork = { SyncCommittee: altair.SyncCommittee; SyncAggregate: altair.SyncAggregate; SingleAttestation: electra.SingleAttestation; - Attestation: electra.Attestation; - IndexedAttestation: electra.IndexedAttestation; - IndexedAttestationBigint: electra.IndexedAttestationBigint; - AttesterSlashing: electra.AttesterSlashing; - AggregateAndProof: electra.AggregateAndProof; - SignedAggregateAndProof: electra.SignedAggregateAndProof; + Attestation: gloas.Attestation; + IndexedAttestation: gloas.IndexedAttestation; + IndexedAttestationBigint: gloas.IndexedAttestationBigint; + AttesterSlashing: gloas.AttesterSlashing; + AggregateAndProof: gloas.AggregateAndProof; + SignedAggregateAndProof: gloas.SignedAggregateAndProof; ExecutionRequests: gloas.ExecutionRequests; ExecutionPayloadBid: gloas.ExecutionPayloadBid; DataColumnSidecar: gloas.DataColumnSidecar; diff --git a/packages/types/src/utils/typeguards.ts b/packages/types/src/utils/typeguards.ts index fa8ffe49d17d..22633b3b2f87 100644 --- a/packages/types/src/utils/typeguards.ts +++ b/packages/types/src/utils/typeguards.ts @@ -1,5 +1,6 @@ import { FINALIZED_ROOT_DEPTH_ELECTRA, + FINALIZED_ROOT_DEPTH_GLOAS, ForkPostBellatrix, ForkPostDeneb, ForkPostElectra, @@ -90,7 +91,8 @@ export function isElectraLightClientUpdate(update: LightClientUpdate): update is const updatePostElectra = update as LightClientUpdate; return ( updatePostElectra.finalityBranch !== undefined && - updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_ELECTRA + (updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_ELECTRA || + updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_GLOAS) ); } @@ -100,7 +102,8 @@ export function isELectraLightClientFinalityUpdate( const updatePostElectra = update as LightClientUpdate; return ( updatePostElectra.finalityBranch !== undefined && - updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_ELECTRA + (updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_ELECTRA || + updatePostElectra.finalityBranch.length === FINALIZED_ROOT_DEPTH_GLOAS) ); } diff --git a/packages/types/test/unit/constants/lightclient.test.ts b/packages/types/test/unit/constants/lightclient.test.ts index dc46a5e76ad2..813f8ecaf287 100644 --- a/packages/types/test/unit/constants/lightclient.test.ts +++ b/packages/types/test/unit/constants/lightclient.test.ts @@ -7,21 +7,30 @@ import {ssz} from "../../../src/index.js"; // guarantee that these constants are correct. describe(`${constants.ACTIVE_PRESET}/ Lightclient pre-computed constants`, () => { - const FINALIZED_ROOT_GINDEX = bnToNum(ssz.altair.BeaconState.getPathInfo(["finalizedCheckpoint", "root"]).gindex); - const FINALIZED_ROOT_DEPTH = floorlog2(FINALIZED_ROOT_GINDEX); - const FINALIZED_ROOT_INDEX = FINALIZED_ROOT_GINDEX % 2 ** FINALIZED_ROOT_DEPTH; - - const NEXT_SYNC_COMMITTEE_GINDEX = bnToNum(ssz.altair.BeaconState.getPathInfo(["nextSyncCommittee"]).gindex); - const NEXT_SYNC_COMMITTEE_DEPTH = floorlog2(NEXT_SYNC_COMMITTEE_GINDEX); - const NEXT_SYNC_COMMITTEE_INDEX = NEXT_SYNC_COMMITTEE_GINDEX % 2 ** NEXT_SYNC_COMMITTEE_DEPTH; - const correctConstants = { - FINALIZED_ROOT_GINDEX, - FINALIZED_ROOT_DEPTH, - FINALIZED_ROOT_INDEX, - NEXT_SYNC_COMMITTEE_GINDEX, - NEXT_SYNC_COMMITTEE_DEPTH, - NEXT_SYNC_COMMITTEE_INDEX, + ...stateConstants("altair", ""), + ...stateConstants("electra", "_ELECTRA"), + ...stateConstants("gloas", "_GLOAS"), + ...gindexConstants( + "BLOCK_BODY_EXECUTION_PAYLOAD", + bnToNum(ssz.capella.BeaconBlockBody.getPathInfo(["executionPayload"]).gindex) + ), + ...gindexConstants( + "EXECUTION_BLOCK_HASH", + bnToNum(ssz.capella.BeaconBlockBody.getPathInfo(["executionPayload", "blockHash"]).gindex) + ), + ...gindexConstants( + "EXECUTION_BLOCK_HASH", + bnToNum(ssz.deneb.BeaconBlockBody.getPathInfo(["executionPayload", "blockHash"]).gindex), + "_DENEB" + ), + ...gindexConstants( + "EXECUTION_BLOCK_HASH", + bnToNum( + ssz.gloas.BeaconBlockBody.getPathInfo(["signedExecutionPayloadBid", "message", "parentBlockHash"]).gindex + ), + "_GLOAS" + ), }; for (const [key, expectedValue] of Object.entries(correctConstants)) { @@ -31,11 +40,44 @@ describe(`${constants.ACTIVE_PRESET}/ Lightclient pre-computed constants`, () => } }); +function stateConstants( + fork: "altair" | "electra" | "gloas", + suffix: "" | "_ELECTRA" | "_GLOAS" +): Record { + return { + ...gindexConstants( + "FINALIZED_ROOT", + bnToNum(ssz[fork].BeaconState.getPathInfo(["finalizedCheckpoint", "root"]).gindex), + suffix + ), + ...gindexConstants( + "CURRENT_SYNC_COMMITTEE", + bnToNum(ssz[fork].BeaconState.getPathInfo(["currentSyncCommittee"]).gindex), + suffix + ), + ...gindexConstants( + "NEXT_SYNC_COMMITTEE", + bnToNum(ssz[fork].BeaconState.getPathInfo(["nextSyncCommittee"]).gindex), + suffix + ), + }; +} + +function gindexConstants(name: string, gindex: number, suffix = ""): Record { + const depth = floorlog2(gindex); + + return { + [`${name}_GINDEX${suffix}`]: gindex, + [`${name}_DEPTH${suffix}`]: depth, + [`${name}_INDEX${suffix}`]: gindex % 2 ** depth, + }; +} + function floorlog2(num: number): number { return Math.floor(Math.log2(num)); } -/** Type safe wrapper for Number constructor that takes 'any' */ +/** Type safe wrapper for Number constructor that takes a bigint */ function bnToNum(bn: bigint): number { return Number(bn); } diff --git a/packages/types/test/unit/gloas/eip7688.test.ts b/packages/types/test/unit/gloas/eip7688.test.ts new file mode 100644 index 000000000000..f7a0c62ab5c3 --- /dev/null +++ b/packages/types/test/unit/gloas/eip7688.test.ts @@ -0,0 +1,83 @@ +import {describe, expect, it} from "vitest"; +import { + ProgressiveByteListType, + ProgressiveContainerType, + ProgressiveListBasicType, + ProgressiveListCompositeType, +} from "@chainsafe/ssz"; +import {ssz} from "../../../src/index.js"; + +describe("Gloas EIP-7688 SSZ types", () => { + it("uses progressive containers and lists for modified Gloas containers", () => { + expect(ssz.gloas.Attestation).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.IndexedAttestation).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.BeaconBlockBody).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.ExecutionPayload).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.ExecutionRequests).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.BeaconState).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.ExecutionPayloadBid).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.ExecutionPayloadEnvelope).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.PayloadAttestation).toBeInstanceOf(ProgressiveContainerType); + expect(ssz.gloas.IndexedPayloadAttestation).toBeInstanceOf(ProgressiveContainerType); + + expect(ssz.gloas.AttestingIndices).toBeInstanceOf(ProgressiveListBasicType); + expect(ssz.gloas.Transactions).toBeInstanceOf(ProgressiveListCompositeType); + expect(ssz.gloas.Withdrawals).toBeInstanceOf(ProgressiveListCompositeType); + expect(ssz.gloas.BlobKzgCommitments).toBeInstanceOf(ProgressiveListCompositeType); + expect(ssz.gloas.DataColumn).toBeInstanceOf(ProgressiveListCompositeType); + expect(ssz.gloas.BuilderDepositRequests).toBeInstanceOf(ProgressiveListCompositeType); + expect(ssz.gloas.BuilderExitRequests).toBeInstanceOf(ProgressiveListCompositeType); + }); + + it("round-trips default Gloas top-level containers through progressive serialization", () => { + // Guards against progressive-container offset mismatches on deserialization + // (e.g. the "First offset must equal to fixedEnd" genesis-load failure). + // Typed per-container via a generic helper so serialize/deserialize bind to the same + // value type; a heterogeneous array would collapse to a union and fail type-checking. + function assertRoundTrips(type: { + defaultValue(): V; + serialize(value: V): Uint8Array; + deserialize(data: Uint8Array): V; + }): void { + const value = type.defaultValue(); + expect(type.deserialize(type.serialize(value))).toEqual(value); + } + + assertRoundTrips(ssz.gloas.BeaconState); + assertRoundTrips(ssz.gloas.SignedBeaconBlock); + assertRoundTrips(ssz.gloas.SignedExecutionPayloadEnvelope); + assertRoundTrips(ssz.gloas.SignedExecutionPayloadBid); + }); + + it("keeps byte-list values while using progressive merkleization", () => { + expect(ssz.gloas.Transaction).toBeInstanceOf(ProgressiveByteListType); + expect(ssz.gloas.BlockAccessList).toBeInstanceOf(ProgressiveByteListType); + + const transaction = Uint8Array.from([1, 2, 3, 4]); + const serialized = ssz.gloas.Transaction.serialize(transaction); + expect(ssz.gloas.Transaction.deserialize(serialized)).toEqual(transaction); + + const blockAccessList = Uint8Array.from([5, 6, 7, 8]); + const blockAccessListSerialized = ssz.gloas.BlockAccessList.serialize(blockAccessList); + expect(ssz.gloas.BlockAccessList.deserialize(blockAccessListSerialized)).toEqual(blockAccessList); + }); + + it("keeps Lodestar DU list helpers on upstream progressive lists", () => { + const validator = ssz.phase0.Validator.defaultValue(); + const validators = ssz.gloas.Validators.toViewDU([validator]); + expect(validators.getReadonly(0).toValue()).toEqual(validator); + expect(validators.getAllReadonlyValues()).toEqual([validator]); + expect(validators.sliceTo(0).length).toBe(1); + expect(validators.sliceFrom(1).length).toBe(0); + + const balances = ssz.gloas.Balances.toViewDU([1, 2, 3]); + expect(balances.sliceTo(1).getAll()).toEqual([1, 2]); + expect(balances.sliceFrom(1).getAll()).toEqual([2, 3]); + }); + + it("matches Gloas light-client state gindices from EIP-7688 progressive containers", () => { + expect(Number(ssz.gloas.BeaconState.getPathInfo(["finalizedCheckpoint", "root"]).gindex)).toBe(735); + expect(Number(ssz.gloas.BeaconState.getPathInfo(["currentSyncCommittee"]).gindex)).toBe(2945); + expect(Number(ssz.gloas.BeaconState.getPathInfo(["nextSyncCommittee"]).gindex)).toBe(2946); + }); +}); diff --git a/packages/validator/package.json b/packages/validator/package.json index 62a2e111805c..7c73990702f5 100644 --- a/packages/validator/package.json +++ b/packages/validator/package.json @@ -49,7 +49,7 @@ ], "dependencies": { "@chainsafe/blst": "^2.2.0", - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.2", "@lodestar/api": "workspace:^", "@lodestar/config": "workspace:^", "@lodestar/db": "workspace:^", diff --git a/packages/validator/src/services/validatorStore.ts b/packages/validator/src/services/validatorStore.ts index e37c2e5047e5..286730a6efda 100644 --- a/packages/validator/src/services/validatorStore.ts +++ b/packages/validator/src/services/validatorStore.ts @@ -607,9 +607,11 @@ export class ValidatorStore { const signingSlot = aggregate.data.slot; const domain = this.config.getDomain(signingSlot, DOMAIN_AGGREGATE_AND_PROOF); const isPostElectra = this.config.getForkSeq(signingSlot) >= ForkSeq.electra; - const signingRoot = isPostElectra - ? computeSigningRoot(ssz.electra.AggregateAndProof, aggregateAndProof, domain) - : computeSigningRoot(ssz.phase0.AggregateAndProof, aggregateAndProof, domain); + const signingRoot = computeSigningRoot( + this.config.getForkTypes(signingSlot).AggregateAndProof, + aggregateAndProof, + domain + ); const signableMessage: SignableMessage = { type: isPostElectra ? SignableMessageType.AGGREGATE_AND_PROOF_V2 : SignableMessageType.AGGREGATE_AND_PROOF, diff --git a/packages/validator/src/util/params.ts b/packages/validator/src/util/params.ts index ccbe0cdf32cd..258cf6624388 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -326,9 +326,12 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record - CUSTODY_REQUIREMENT = 4 + + CUSTODY_REQUIREMENT: uint64 = 4 - name: DATA_COLUMN_SIDECAR_SUBNET_COUNT#fulu @@ -229,8 +229,8 @@ - file: packages/config/src/chainConfig/configs/mainnet.ts search: "DATA_COLUMN_SIDECAR_SUBNET_COUNT:" spec: | - - DATA_COLUMN_SIDECAR_SUBNET_COUNT = 128 + + DATA_COLUMN_SIDECAR_SUBNET_COUNT: uint64 = 128 - name: DENEB_FORK_EPOCH#deneb @@ -516,8 +516,8 @@ - file: packages/config/src/chainConfig/configs/mainnet.ts search: "MIN_BUILDER_WITHDRAWABILITY_DELAY:" spec: | - - MIN_BUILDER_WITHDRAWABILITY_DELAY: uint64 = 8192 + + MIN_BUILDER_WITHDRAWABILITY_DELAY: uint64 = 64 - name: MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS#deneb @@ -534,8 +534,8 @@ - file: packages/config/src/chainConfig/configs/mainnet.ts search: "MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS:" spec: | - - MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS = 4096 + + MIN_EPOCHS_FOR_DATA_COLUMN_SIDECARS_REQUESTS: uint64 = 4096 - name: MIN_GENESIS_ACTIVE_VALIDATOR_COUNT#phase0 @@ -588,8 +588,8 @@ - file: packages/config/src/chainConfig/configs/mainnet.ts search: "NUMBER_OF_CUSTODY_GROUPS:" spec: | - - NUMBER_OF_CUSTODY_GROUPS = 128 + + NUMBER_OF_CUSTODY_GROUPS: uint64 = 128 - name: PAYLOAD_ATTESTATION_DUE_BPS#gloas @@ -606,8 +606,8 @@ - file: packages/config/src/chainConfig/configs/mainnet.ts search: "PAYLOAD_DUE_BPS:" spec: | - - PAYLOAD_DUE_BPS: uint64 = 7500 + + PAYLOAD_DUE_BPS: uint64 = 5000 - name: PROPOSER_REORG_CUTOFF_BPS#phase0 @@ -660,8 +660,8 @@ - file: packages/config/src/chainConfig/configs/mainnet.ts search: "SAMPLES_PER_SLOT:" spec: | - - SAMPLES_PER_SLOT = 8 + + SAMPLES_PER_SLOT: uint64 = 8 - name: SECONDS_PER_ETH1_BLOCK#phase0 diff --git a/specrefs/constants.yml b/specrefs/constants.yml index 7eb9dab55a00..90eb03fa256a 100644 --- a/specrefs/constants.yml +++ b/specrefs/constants.yml @@ -98,8 +98,8 @@ - file: packages/params/src/index.ts search: export const BUILDER_WITHDRAWAL_PREFIX = spec: | - - BUILDER_WITHDRAWAL_PREFIX: Bytes1 = '0x03' + + BUILDER_WITHDRAWAL_PREFIX: Bytes1 = '0xB0' - name: BYTES_PER_COMMITMENT#deneb @@ -427,8 +427,8 @@ - name: KZG_SETUP_G2_LENGTH#deneb sources: [] spec: | - - KZG_SETUP_G2_LENGTH = 65 + + KZG_SETUP_G2_LENGTH: uint64 = 65 - name: KZG_SETUP_G2_MONOMIAL#deneb @@ -590,8 +590,8 @@ - file: packages/params/src/index.ts search: export const SYNC_COMMITTEE_SUBNET_COUNT = spec: | - - SYNC_COMMITTEE_SUBNET_COUNT = 4 + + SYNC_COMMITTEE_SUBNET_COUNT: uint64 = 2**2 - name: SYNC_REWARD_WEIGHT#altair @@ -617,8 +617,8 @@ - file: packages/params/src/index.ts search: export const TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE = spec: | - - TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE = 2**4 + + TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: uint64 = 2**4 - name: TIMELY_HEAD_FLAG_INDEX#altair diff --git a/specrefs/containers.yml b/specrefs/containers.yml index 6461b202a5e1..1571d300cca0 100644 --- a/specrefs/containers.yml +++ b/specrefs/containers.yml @@ -40,16 +40,29 @@ - file: packages/types/src/electra/sszTypes.ts search: export const Attestation = spec: | - + class Attestation(Container): # [Modified in Electra:EIP7549] - aggregation_bits: Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] + aggregation_bits: AggregationBits data: AttestationData signature: BLSSignature # [New in Electra:EIP7549] committee_bits: Bitvector[MAX_COMMITTEES_PER_SLOT] +- name: Attestation#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const Attestation = + spec: | + + class Attestation(ProgressiveContainer(active_fields=[1] * 4)): # type: ignore + aggregation_bits: AggregationBits + data: AttestationData + signature: BLSSignature + committee_bits: Bitvector[MAX_COMMITTEES_PER_SLOT] + + - name: AttestationData#phase0 sources: - file: packages/types/src/phase0/sszTypes.ts @@ -244,20 +257,26 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const BeaconBlockBody = spec: | - - class BeaconBlockBody(Container): + + class BeaconBlockBody(ProgressiveContainer(active_fields=[1] * 13)): # type: ignore randao_reveal: BLSSignature eth1_data: Eth1Data graffiti: Bytes32 - proposer_slashings: List[ProposerSlashing, MAX_PROPOSER_SLASHINGS] - attester_slashings: List[AttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA] - attestations: List[Attestation, MAX_ATTESTATIONS_ELECTRA] - deposits: List[Deposit, MAX_DEPOSITS] - voluntary_exits: List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS] + # [Modified in Gloas:EIP7688] + proposer_slashings: ProgressiveList[ProposerSlashing] + # [Modified in Gloas:EIP7688] + attester_slashings: ProgressiveList[AttesterSlashing] + # [Modified in Gloas:EIP7688] + attestations: ProgressiveList[Attestation] + # [Modified in Gloas:EIP7688] + deposits: ProgressiveList[Deposit] + # [Modified in Gloas:EIP7688] + voluntary_exits: ProgressiveList[SignedVoluntaryExit] sync_aggregate: SyncAggregate # [Modified in Gloas:EIP7732] # Removed `execution_payload` - bls_to_execution_changes: List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES] + # [Modified in Gloas:EIP7688] + bls_to_execution_changes: ProgressiveList[SignedBLSToExecutionChange] # [Modified in Gloas:EIP7732] # Removed `blob_kzg_commitments` # [Modified in Gloas:EIP7732] @@ -265,7 +284,7 @@ # [New in Gloas:EIP7732] signed_execution_payload_bid: SignedExecutionPayloadBid # [New in Gloas:EIP7732] - payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] + payload_attestations: ProgressiveList[PayloadAttestation] # [New in Gloas:EIP7732] parent_execution_requests: ExecutionRequests @@ -574,8 +593,8 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const BeaconState = spec: | - - class BeaconState(Container): + + class BeaconState(ProgressiveContainer(active_fields=[1] * 46)): # type: ignore genesis_time: uint64 genesis_validators_root: Root slot: Slot @@ -587,17 +606,22 @@ eth1_data: Eth1Data eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index: uint64 - validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] - balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + # [Modified in Gloas:EIP7688] + validators: ProgressiveList[Validator] + # [Modified in Gloas:EIP7688] + balances: ProgressiveList[Gwei] randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] - previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] - current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + # [Modified in Gloas:EIP7688] + previous_epoch_participation: ProgressiveList[ParticipationFlags] + # [Modified in Gloas:EIP7688] + current_epoch_participation: ProgressiveList[ParticipationFlags] justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] previous_justified_checkpoint: Checkpoint current_justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint - inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + # [Modified in Gloas:EIP7688] + inactivity_scores: ProgressiveList[uint64] current_sync_committee: SyncCommittee next_sync_committee: SyncCommittee # [Modified in Gloas:EIP7732] @@ -613,12 +637,15 @@ earliest_exit_epoch: Epoch consolidation_balance_to_consume: Gwei earliest_consolidation_epoch: Epoch - pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT] - pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT] - pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] + # [Modified in Gloas:EIP7688] + pending_deposits: ProgressiveList[PendingDeposit] + # [Modified in Gloas:EIP7688] + pending_partial_withdrawals: ProgressiveList[PendingPartialWithdrawal] + # [Modified in Gloas:EIP7688] + pending_consolidations: ProgressiveList[PendingConsolidation] proposer_lookahead: Vector[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH] # [New in Gloas:EIP7732] - builders: List[Builder, BUILDER_REGISTRY_LIMIT] + builders: ProgressiveList[Builder] # [New in Gloas:EIP7732] next_withdrawal_builder_index: BuilderIndex # [New in Gloas:EIP7732] @@ -626,11 +653,11 @@ # [New in Gloas:EIP7732] builder_pending_payments: Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH] # [New in Gloas:EIP7732] - builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT] + builder_pending_withdrawals: ProgressiveList[BuilderPendingWithdrawal] # [New in Gloas:EIP7732] latest_execution_payload_bid: ExecutionPayloadBid # [New in Gloas:EIP7732] - payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + payload_expected_withdrawals: ProgressiveList[Withdrawal] # [New in Gloas:EIP7732] ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] @@ -638,8 +665,8 @@ - name: BeaconState#heze sources: [] spec: | - - class BeaconState(Container): + + class BeaconState(ProgressiveContainer(active_fields=[1] * 46)): # type: ignore genesis_time: uint64 genesis_validators_root: Root slot: Slot @@ -651,22 +678,19 @@ eth1_data: Eth1Data eth1_data_votes: List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH] eth1_deposit_index: uint64 - validators: List[Validator, VALIDATOR_REGISTRY_LIMIT] - balances: List[Gwei, VALIDATOR_REGISTRY_LIMIT] + validators: ProgressiveList[Validator] + balances: ProgressiveList[Gwei] randao_mixes: Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR] slashings: Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR] - previous_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] - current_epoch_participation: List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT] + previous_epoch_participation: ProgressiveList[ParticipationFlags] + current_epoch_participation: ProgressiveList[ParticipationFlags] justification_bits: Bitvector[JUSTIFICATION_BITS_LENGTH] previous_justified_checkpoint: Checkpoint current_justified_checkpoint: Checkpoint finalized_checkpoint: Checkpoint - inactivity_scores: List[uint64, VALIDATOR_REGISTRY_LIMIT] + inactivity_scores: ProgressiveList[uint64] current_sync_committee: SyncCommittee next_sync_committee: SyncCommittee - # [Modified in Gloas:EIP7732] - # Removed `latest_execution_payload_header` - # [New in Gloas:EIP7732] latest_block_hash: Hash32 next_withdrawal_index: WithdrawalIndex next_withdrawal_validator_index: ValidatorIndex @@ -677,25 +701,18 @@ earliest_exit_epoch: Epoch consolidation_balance_to_consume: Gwei earliest_consolidation_epoch: Epoch - pending_deposits: List[PendingDeposit, PENDING_DEPOSITS_LIMIT] - pending_partial_withdrawals: List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT] - pending_consolidations: List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT] + pending_deposits: ProgressiveList[PendingDeposit] + pending_partial_withdrawals: ProgressiveList[PendingPartialWithdrawal] + pending_consolidations: ProgressiveList[PendingConsolidation] proposer_lookahead: Vector[ValidatorIndex, (MIN_SEED_LOOKAHEAD + 1) * SLOTS_PER_EPOCH] - # [New in Gloas:EIP7732] - builders: List[Builder, BUILDER_REGISTRY_LIMIT] - # [New in Gloas:EIP7732] + builders: ProgressiveList[Builder] next_withdrawal_builder_index: BuilderIndex - # [New in Gloas:EIP7732] execution_payload_availability: Bitvector[SLOTS_PER_HISTORICAL_ROOT] - # [New in Gloas:EIP7732] builder_pending_payments: Vector[BuilderPendingPayment, 2 * SLOTS_PER_EPOCH] - # [New in Gloas:EIP7732] - builder_pending_withdrawals: List[BuilderPendingWithdrawal, BUILDER_PENDING_WITHDRAWALS_LIMIT] - # [New in Gloas:EIP7732] + builder_pending_withdrawals: ProgressiveList[BuilderPendingWithdrawal] + # [Modified in Heze:EIP7805] latest_execution_payload_bid: ExecutionPayloadBid - # [New in Gloas:EIP7732] - payload_expected_withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] - # [New in Gloas:EIP7732] + payload_expected_withdrawals: ProgressiveList[Withdrawal] ptc_window: Vector[Vector[ValidatorIndex, PTC_SIZE], (2 + MIN_SEED_LOOKAHEAD) * SLOTS_PER_EPOCH] @@ -843,13 +860,15 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const DataColumnSidecar = spec: | - + class DataColumnSidecar(Container): index: ColumnIndex - column: List[Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK] + # [Modified in Gloas:EIP7688] + column: ProgressiveList[Cell] # [Modified in Gloas:EIP7732] # Removed `kzg_commitments` - kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + # [Modified in Gloas:EIP7688] + kzg_proofs: ProgressiveList[KZGProof] # [Modified in Gloas:EIP7732] # Removed `signed_block_header` # [Modified in Gloas:EIP7732] @@ -1026,8 +1045,8 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const ExecutionPayload = spec: | - - class ExecutionPayload(Container): + + class ExecutionPayload(ProgressiveContainer(active_fields=[1] * 19)): # type: ignore parent_hash: Hash32 fee_recipient: ExecutionAddress state_root: Bytes32 @@ -1041,8 +1060,10 @@ extra_data: ByteList[MAX_EXTRA_DATA_BYTES] base_fee_per_gas: uint256 block_hash: Hash32 - transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] - withdrawals: List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD] + # [Modified in Gloas:EIP7688] + transactions: ProgressiveList[Transaction] + # [Modified in Gloas:EIP7688] + withdrawals: ProgressiveList[Withdrawal] blob_gas_used: uint64 excess_blob_gas: uint64 # [New in Gloas:EIP7928] @@ -1056,8 +1077,8 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const ExecutionPayloadBid = spec: | - - class ExecutionPayloadBid(Container): + + class ExecutionPayloadBid(ProgressiveContainer(active_fields=[1] * 12)): # type: ignore parent_block_hash: Hash32 parent_block_root: Root block_hash: Hash32 @@ -1068,15 +1089,15 @@ slot: Slot value: Gwei execution_payment: Gwei - blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + blob_kzg_commitments: ProgressiveList[KZGCommitment] execution_requests_root: Root - name: ExecutionPayloadBid#heze sources: [] spec: | - - class ExecutionPayloadBid(Container): + + class ExecutionPayloadBid(ProgressiveContainer(active_fields=[1] * 13)): # type: ignore parent_block_hash: Hash32 parent_block_root: Root block_hash: Hash32 @@ -1087,8 +1108,10 @@ slot: Slot value: Gwei execution_payment: Gwei - blob_kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK] + blob_kzg_commitments: ProgressiveList[KZGCommitment] execution_requests_root: Root + # [New in Heze:EIP7805] + inclusion_list_bits: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE] - name: ExecutionPayloadEnvelope#gloas @@ -1096,8 +1119,8 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const ExecutionPayloadEnvelope = spec: | - - class ExecutionPayloadEnvelope(Container): + + class ExecutionPayloadEnvelope(ProgressiveContainer(active_fields=[1] * 5)): # type: ignore payload: ExecutionPayload execution_requests: ExecutionRequests builder_index: BuilderIndex @@ -1186,14 +1209,14 @@ - file: packages/types/src/electra/sszTypes.ts search: export const ExecutionRequests = spec: | - + class ExecutionRequests(Container): # [New in Electra:EIP6110] - deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD] + deposits: DepositRequests # [New in Electra:EIP7002:EIP7251] - withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD] + withdrawals: WithdrawalRequests # [New in Electra:EIP7251] - consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD] + consolidations: ConsolidationRequests - name: ExecutionRequests#gloas @@ -1201,15 +1224,15 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const ExecutionRequests = spec: | - - class ExecutionRequests(Container): - deposits: List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD] - withdrawals: List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD] - consolidations: List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD] + + class ExecutionRequests(ProgressiveContainer(active_fields=[1] * 5)): # type: ignore + deposits: DepositRequests + withdrawals: WithdrawalRequests + consolidations: ConsolidationRequests # [New in Gloas:EIP8282] - builder_deposits: List[BuilderDepositRequest, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD] + builder_deposits: BuilderDepositRequests # [New in Gloas:EIP8282] - builder_exits: List[BuilderExitRequest, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD] + builder_exits: BuilderExitRequests - name: Fork#phase0 @@ -1260,12 +1283,12 @@ - name: InclusionList#heze sources: [] spec: | - + class InclusionList(Container): slot: Slot validator_index: ValidatorIndex inclusion_list_committee_root: Root - transactions: List[Transaction, MAX_TRANSACTIONS_PER_PAYLOAD] + transactions: ProgressiveList[Transaction] - name: IndexedAttestation#phase0 @@ -1285,10 +1308,22 @@ - file: packages/types/src/electra/sszTypes.ts search: export const IndexedAttestation = spec: | - + class IndexedAttestation(Container): # [Modified in Electra:EIP7549] - attesting_indices: List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] + attesting_indices: AttestingIndices + data: AttestationData + signature: BLSSignature + + +- name: IndexedAttestation#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const IndexedAttestation = + spec: | + + class IndexedAttestation(ProgressiveContainer(active_fields=[1] * 3)): # type: ignore + attesting_indices: AttestingIndices data: AttestationData signature: BLSSignature @@ -1298,8 +1333,8 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const IndexedPayloadAttestation = spec: | - - class IndexedPayloadAttestation(Container): + + class IndexedPayloadAttestation(ProgressiveContainer(active_fields=[1] * 3)): # type: ignore attesting_indices: List[ValidatorIndex, PTC_SIZE] data: PayloadAttestationData signature: BLSSignature @@ -1484,23 +1519,32 @@ row_index: RowIndex +- name: PartialDataColumnGroupID#fulu + sources: [] + spec: | + + class PartialDataColumnGroupID(Container): + beacon_block_root: Root + + - name: PartialDataColumnGroupID#gloas sources: [] spec: | - + class PartialDataColumnGroupID(Container): - slot: Slot beacon_block_root: Root + # [New in Gloas:EIP7732] + slot: Slot - name: PartialDataColumnSidecar#gloas sources: [] spec: | - + class PartialDataColumnSidecar(Container): - cells_present_bitmap: Bitlist[MAX_BLOB_COMMITMENTS_PER_BLOCK] - partial_column: List[Cell, MAX_BLOB_COMMITMENTS_PER_BLOCK] - kzg_proofs: List[KZGProof, MAX_BLOB_COMMITMENTS_PER_BLOCK] + cells_present_bitmap: ProgressiveBitlist + partial_column: ProgressiveList[Cell] + kzg_proofs: ProgressiveList[KZGProof] - name: PayloadAttestation#gloas @@ -1508,8 +1552,8 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const PayloadAttestation = spec: | - - class PayloadAttestation(Container): + + class PayloadAttestation(ProgressiveContainer(active_fields=[1] * 3)): # type: ignore aggregation_bits: Bitvector[PTC_SIZE] data: PayloadAttestationData signature: BLSSignature @@ -1708,8 +1752,9 @@ - name: SignedExecutionPayloadBid#heze sources: [] spec: | - + class SignedExecutionPayloadBid(Container): + # [Modified in Heze:EIP7805] message: ExecutionPayloadBid signature: BLSSignature diff --git a/specrefs/functions.yml b/specrefs/functions.yml index 54bfc184ec35..887775f14d0c 100644 --- a/specrefs/functions.yml +++ b/specrefs/functions.yml @@ -195,7 +195,7 @@ - file: packages/state-transition/src/block/processParentExecutionPayload.ts search: export function applyParentExecutionPayload( spec: | - + def apply_parent_execution_payload( state: BeaconState, requests: ExecutionRequests, @@ -204,6 +204,11 @@ parent_slot = parent_bid.slot parent_epoch = compute_epoch_at_slot(parent_slot) + assert len(requests.withdrawals) <= MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD + assert len(requests.consolidations) <= MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD + assert len(requests.builder_deposits) <= MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD + assert len(requests.builder_exits) <= MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD + # Process execution requests from parent's payload. The execution # requests are processed at state.slot (child's slot), not the parent's slot. def for_ops(operations: Sequence[Any], fn: Callable[[BeaconState, Any], None]) -> None: @@ -213,9 +218,7 @@ for_ops(requests.deposits, process_deposit_request) for_ops(requests.withdrawals, process_withdrawal_request) for_ops(requests.consolidations, process_consolidation_request) - # [New in Gloas:EIP8282] for_ops(requests.builder_deposits, process_builder_deposit_request) - # [New in Gloas:EIP8282] for_ops(requests.builder_exits, process_builder_exit_request) # Settle the builder payment @@ -1212,14 +1215,14 @@ - name: compute_on_chain_aggregate#electra sources: [] spec: | - + def compute_on_chain_aggregate(network_aggregates: Sequence[Attestation]) -> Attestation: aggregates = sorted( network_aggregates, key=lambda a: get_committee_indices(a.committee_bits)[0] ) data = aggregates[0].data - aggregation_bits = Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT]() + aggregation_bits = AggregationBits() for a in aggregates: for b in a.aggregation_bits: aggregation_bits.append(b) @@ -1885,6 +1888,23 @@ return CURRENT_SYNC_COMMITTEE_GINDEX +- name: current_sync_committee_gindex_at_slot#gloas + sources: + - file: packages/state-transition/src/lightClient/spec/utils.ts + search: export function currentSyncCommitteeGindexAtFork( + spec: | + + def current_sync_committee_gindex_at_slot(slot: Slot) -> GeneralizedIndex: + epoch = compute_epoch_at_slot(slot) + + # [Modified in Gloas:EIP7688] + if epoch >= GLOAS_FORK_EPOCH: + return CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS + if epoch >= ELECTRA_FORK_EPOCH: + return CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA + return CURRENT_SYNC_COMMITTEE_GINDEX + + - name: decrease_balance#phase0 sources: - file: packages/state-transition/src/util/balance.ts @@ -2056,6 +2076,23 @@ return FINALIZED_ROOT_GINDEX +- name: finalized_root_gindex_at_slot#gloas + sources: + - file: packages/state-transition/src/lightClient/spec/utils.ts + search: export function finalizedRootGindexAtFork( + spec: | + + def finalized_root_gindex_at_slot(slot: Slot) -> GeneralizedIndex: + epoch = compute_epoch_at_slot(slot) + + # [Modified in Gloas:EIP7688] + if epoch >= GLOAS_FORK_EPOCH: + return FINALIZED_ROOT_GINDEX_GLOAS + if epoch >= ELECTRA_FORK_EPOCH: + return FINALIZED_ROOT_GINDEX_ELECTRA + return FINALIZED_ROOT_GINDEX + + - name: find_latest_confirmed_descendant#phase0 sources: [] spec: | @@ -3556,40 +3593,6 @@ ) -- name: get_dependent_root#phase0 - sources: [] - spec: | - - def get_dependent_root(store: Store, root: Root) -> Root: - epoch = get_current_store_epoch(store) - if epoch <= MIN_SEED_LOOKAHEAD: - # Genesis block parent - return Root() - - node = ForkChoiceNode(root=root) - dependent_slot = Slot(compute_start_slot_at_epoch(epoch - MIN_SEED_LOOKAHEAD) - 1) - return get_ancestor(store, node, dependent_slot).root - - -- name: get_dependent_root#gloas - sources: [] - spec: | - - def get_dependent_root(store: Store, root: Root) -> Root: - epoch = get_current_store_epoch(store) - if epoch <= MIN_SEED_LOOKAHEAD: - # Genesis block parent - return Root() - - # [Modified in Gloas:EIP7732] - node = ForkChoiceNode( - root=root, - payload_status=PAYLOAD_STATUS_PENDING, - ) - dependent_slot = Slot(compute_start_slot_at_epoch(epoch - MIN_SEED_LOOKAHEAD) - 1) - return get_ancestor(store, node, dependent_slot).root - - - name: get_domain#phase0 sources: - file: packages/config/src/genesisConfig/index.ts @@ -3803,7 +3806,7 @@ - file: packages/beacon-node/src/execution/engine/types.ts search: export function deserializeExecutionRequests( spec: | - + def get_execution_requests(execution_requests_list: Sequence[bytes]) -> ExecutionRequests: deposits = [] withdrawals = [] @@ -3829,17 +3832,11 @@ prev_request_type = request_type if request_type == DEPOSIT_REQUEST_TYPE: - deposits = ssz_deserialize( - List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD], request_data - ) + deposits = ssz_deserialize(DepositRequests, request_data) elif request_type == WITHDRAWAL_REQUEST_TYPE: - withdrawals = ssz_deserialize( - List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], request_data - ) + withdrawals = ssz_deserialize(WithdrawalRequests, request_data) elif request_type == CONSOLIDATION_REQUEST_TYPE: - consolidations = ssz_deserialize( - List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD], request_data - ) + consolidations = ssz_deserialize(ConsolidationRequests, request_data) return ExecutionRequests( deposits=deposits, @@ -3853,7 +3850,7 @@ - file: packages/beacon-node/src/execution/engine/types.ts search: export function deserializeExecutionRequests( spec: | - + def get_execution_requests(execution_requests_list: Sequence[bytes]) -> ExecutionRequests: deposits = [] withdrawals = [] @@ -3887,28 +3884,17 @@ prev_request_type = request_type if request_type == DEPOSIT_REQUEST_TYPE: - deposits = ssz_deserialize( - List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD], request_data - ) + deposits = ssz_deserialize(DepositRequests, request_data) elif request_type == WITHDRAWAL_REQUEST_TYPE: - withdrawals = ssz_deserialize( - List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD], request_data - ) + withdrawals = ssz_deserialize(WithdrawalRequests, request_data) elif request_type == CONSOLIDATION_REQUEST_TYPE: - consolidations = ssz_deserialize( - List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD], request_data - ) + consolidations = ssz_deserialize(ConsolidationRequests, request_data) # [New in Gloas:EIP8282] elif request_type == BUILDER_DEPOSIT_REQUEST_TYPE: - builder_deposits = ssz_deserialize( - List[BuilderDepositRequest, MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD], - request_data, - ) + builder_deposits = ssz_deserialize(BuilderDepositRequests, request_data) # [New in Gloas:EIP8282] elif request_type == BUILDER_EXIT_REQUEST_TYPE: - builder_exits = ssz_deserialize( - List[BuilderExitRequest, MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD], request_data - ) + builder_exits = ssz_deserialize(BuilderExitRequests, request_data) return ExecutionRequests( deposits=deposits, @@ -3926,9 +3912,9 @@ - file: packages/beacon-node/src/execution/engine/types.ts search: export function serializeExecutionRequests( spec: | - + def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequence[bytes]: - requests = [ + requests: Sequence[Tuple[Bytes1, List]] = [ (DEPOSIT_REQUEST_TYPE, execution_requests.deposits), (WITHDRAWAL_REQUEST_TYPE, execution_requests.withdrawals), (CONSOLIDATION_REQUEST_TYPE, execution_requests.consolidations), @@ -3946,9 +3932,9 @@ - file: packages/beacon-node/src/execution/engine/types.ts search: export function serializeExecutionRequests( spec: | - + def get_execution_requests_list(execution_requests: ExecutionRequests) -> Sequence[bytes]: - requests = [ + requests: Sequence[Tuple[Bytes1, ProgressiveList]] = [ (DEPOSIT_REQUEST_TYPE, execution_requests.deposits), (WITHDRAWAL_REQUEST_TYPE, execution_requests.withdrawals), (CONSOLIDATION_REQUEST_TYPE, execution_requests.consolidations), @@ -4431,6 +4417,36 @@ return rewards, penalties +- name: get_inclusion_list_bits#heze + sources: [] + spec: | + + def get_inclusion_list_bits( + store: InclusionListStore, state: BeaconState, slot: Slot, only_timely: bool = True + ) -> Bitvector[INCLUSION_LIST_COMMITTEE_SIZE]: + """ + Return a ``Bitvector`` over inclusion list committee indices with bits set + for those who provided valid, non-equivocating inclusion lists for the given ``slot``. + """ + committee = get_inclusion_list_committee(state, slot) + key = hash_tree_root(committee) + + inclusion_lists = store.inclusion_lists[key] + equivocators = store.equivocators[key] + timeliness = store.inclusion_list_timeliness + + validator_indices = [ + inclusion_lists[inclusion_list_root].validator_index + for inclusion_list_root in inclusion_lists + if inclusion_lists[inclusion_list_root].validator_index not in equivocators + if not only_timely or timeliness[inclusion_list_root] + ] + + return Bitvector[INCLUSION_LIST_COMMITTEE_SIZE]( + validator_index in validator_indices for validator_index in committee + ) + + - name: get_inclusion_list_committee#heze sources: [] spec: | @@ -5202,25 +5218,12 @@ return GENESIS_EPOCH if current_epoch == GENESIS_EPOCH else Epoch(current_epoch - 1) -- name: get_proposer_dependent_root#gloas - sources: [] - spec: | - - def get_proposer_dependent_root(state: BeaconState, epoch: Epoch) -> Root: - """ - Return the dependent root for the proposer lookahead at ``epoch``. - """ - return get_block_root_at_slot( - state, Slot(compute_start_slot_at_epoch(Epoch(epoch - MIN_SEED_LOOKAHEAD)) - 1) - ) - - - name: get_proposer_head#phase0 sources: - file: packages/fork-choice/src/forkChoice/forkChoice.ts search: "* Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head" spec: | - + def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: head_block = store.blocks[head_node.root] parent_root = head_block.parent_root @@ -5231,7 +5234,7 @@ head_late = is_head_late(store, head_node.root) # Do not re-org on an epoch boundary. - epoch_boundary = is_epoch_boundary(slot) + not_epoch_boundary = is_not_epoch_boundary(slot) # Ensure that the FFG information of the new head will be competitive with the current head. ffg_competitive = is_ffg_competitive(store, head_node.root, parent_root) @@ -5259,7 +5262,7 @@ if all([ head_late, - epoch_boundary, + not_epoch_boundary, ffg_competitive, finalization_ok, proposing_on_time, @@ -5280,7 +5283,7 @@ - file: packages/fork-choice/src/forkChoice/forkChoice.ts search: "* Same as https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.4/specs/phase0/fork-choice.md#get_proposer_head" spec: | - + def get_proposer_head(store: Store, head_node: ForkChoiceNode, slot: Slot) -> ForkChoiceNode: head_block = store.blocks[head_node.root] parent_root = head_block.parent_root @@ -5293,7 +5296,7 @@ head_late = is_head_late(store, head_node.root) # Do not re-org on an epoch boundary. - epoch_boundary = is_epoch_boundary(slot) + not_epoch_boundary = is_not_epoch_boundary(slot) # Ensure that the FFG information of the new head will be competitive with the current head. ffg_competitive = is_ffg_competitive(store, head_node.root, parent_root) @@ -5321,7 +5324,7 @@ if all([ head_late, - epoch_boundary, + not_epoch_boundary, ffg_competitive, finalization_ok, proposing_on_time, @@ -5337,20 +5340,6 @@ return head_node -- name: get_proposer_preferences_signature#gloas - sources: [] - spec: | - - def get_proposer_preferences_signature( - state: BeaconState, preferences: ProposerPreferences, privkey: int - ) -> BLSSignature: - domain = get_domain( - state, DOMAIN_PROPOSER_PREFERENCES, compute_epoch_at_slot(preferences.proposal_slot) - ) - signing_root = compute_signing_root(preferences, domain) - return bls.Sign(privkey, signing_root) - - - name: get_proposer_reorg_cutoff_ms#phase0 sources: - file: packages/config/src/forkConfig/index.ts @@ -5503,6 +5492,67 @@ return hash(domain_type + uint_to_bytes(epoch) + mix) +- name: get_shuffling_dependent_root#phase0 + sources: [] + spec: | + + def get_shuffling_dependent_root(store: Store, root: Root, epoch: Epoch) -> Root: + if epoch <= MIN_SEED_LOOKAHEAD: + # Genesis block parent + return Root() + + node = ForkChoiceNode(root=root) + dependent_slot = Slot(compute_start_slot_at_epoch(epoch - MIN_SEED_LOOKAHEAD) - 1) + return get_ancestor(store, node, dependent_slot).root + + +- name: get_shuffling_dependent_root#gloas + sources: [] + spec: | + + def get_shuffling_dependent_root(store: Store, root: Root, epoch: Epoch) -> Root: + if epoch <= MIN_SEED_LOOKAHEAD: + # Genesis block parent + return Root() + + # [Modified in Gloas:EIP7732] + node = ForkChoiceNode( + root=root, + payload_status=PAYLOAD_STATUS_PENDING, + ) + dependent_slot = Slot(compute_start_slot_at_epoch(epoch - MIN_SEED_LOOKAHEAD) - 1) + return get_ancestor(store, node, dependent_slot).root + + +- name: get_signed_proposer_preferences#gloas + sources: [] + spec: | + + def get_signed_proposer_preferences( + store: Store, + state: BeaconState, + head_root: Root, + proposal_slot: Slot, + validator_index: ValidatorIndex, + fee_recipient: ExecutionAddress, + target_gas_limit: uint64, + privkey: int, + ) -> SignedProposerPreferences: + proposal_epoch = compute_epoch_at_slot(proposal_slot) + dependent_root = get_shuffling_dependent_root(store, head_root, proposal_epoch) + preferences = ProposerPreferences( + dependent_root=dependent_root, + proposal_slot=proposal_slot, + validator_index=validator_index, + fee_recipient=fee_recipient, + target_gas_limit=target_gas_limit, + ) + domain = get_domain(state, DOMAIN_PROPOSER_PREFERENCES, proposal_epoch) + signing_root = compute_signing_root(preferences, domain) + signature = bls.Sign(privkey, signing_root) + return SignedProposerPreferences(message=preferences, signature=signature) + + - name: get_slot_committee#phase0 sources: [] spec: | @@ -6790,16 +6840,6 @@ ) -- name: is_epoch_boundary#phase0 - sources: - - file: packages/state-transition/src/util/epoch.ts - search: export function isStartSlotOfEpoch( - spec: | - - def is_epoch_boundary(slot: Slot) -> bool: - return slot % SLOTS_PER_EPOCH != 0 - - - name: is_execution_block#bellatrix sources: [] spec: | @@ -6988,6 +7028,31 @@ return get_finality_delay(state) > MIN_EPOCHS_TO_INACTIVITY_PENALTY +- name: is_inclusion_list_bits_inclusive#heze + sources: [] + spec: | + + def is_inclusion_list_bits_inclusive( + store: InclusionListStore, + state: BeaconState, + slot: Slot, + inclusion_list_bits: Bitvector[INCLUSION_LIST_COMMITTEE_SIZE], + only_timely: bool = True, + ) -> bool: + """ + Return ``True`` if and only if ``inclusion_list_bits`` has a bit set for + every bit set in the local inclusion list bits for the given ``slot``. + """ + local_inclusion_list_bits = get_inclusion_list_bits(store, state, slot, only_timely) + + return not any( + local_inclusion_bit and not inclusion_bit + for inclusion_bit, local_inclusion_bit in zip( + inclusion_list_bits, local_inclusion_list_bits, strict=True + ) + ) + + - name: is_merge_transition_block#bellatrix sources: [] spec: | @@ -7037,6 +7102,16 @@ return False +- name: is_not_epoch_boundary#phase0 + sources: + - file: packages/state-transition/src/util/epoch.ts + search: export function isStartSlotOfEpoch( + spec: | + + def is_not_epoch_boundary(slot: Slot) -> bool: + return slot % SLOTS_PER_EPOCH != 0 + + - name: is_one_confirmed#phase0 sources: [] spec: | @@ -7418,6 +7493,34 @@ return bls.FastAggregateVerify(pubkeys, signing_root, indexed_attestation.signature) +- name: is_valid_indexed_attestation#gloas + sources: + - file: packages/state-transition/src/block/isValidIndexedAttestation.ts + search: export function isValidIndexedAttestationIndices( + spec: | + + def is_valid_indexed_attestation( + state: BeaconState, indexed_attestation: IndexedAttestation + ) -> bool: + """ + Check if ``indexed_attestation`` is not empty, has sorted and unique indices and has a valid aggregate signature. + """ + # Verify indices are sorted and unique + indices = indexed_attestation.attesting_indices + if ( + len(indices) == 0 + # [New in Gloas:EIP7688] + or len(indices) > MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT + or indices != sorted(set(indices)) + ): + return False + # Verify aggregate signature + pubkeys = [state.validators[i].pubkey for i in indices] + domain = get_domain(state, DOMAIN_BEACON_ATTESTER, indexed_attestation.data.target.epoch) + signing_root = compute_signing_root(indexed_attestation.data, domain) + return bls.FastAggregateVerify(pubkeys, signing_root, indexed_attestation.signature) + + - name: is_valid_indexed_payload_attestation#gloas sources: - file: packages/state-transition/src/block/isValidIndexedPayloadAttestation.ts @@ -7750,6 +7853,23 @@ return NEXT_SYNC_COMMITTEE_GINDEX +- name: next_sync_committee_gindex_at_slot#gloas + sources: + - file: packages/state-transition/src/lightClient/spec/utils.ts + search: export function nextSyncCommitteeGindexAtFork( + spec: | + + def next_sync_committee_gindex_at_slot(slot: Slot) -> GeneralizedIndex: + epoch = compute_epoch_at_slot(slot) + + # [Modified in Gloas:EIP7688] + if epoch >= GLOAS_FORK_EPOCH: + return NEXT_SYNC_COMMITTEE_GINDEX_GLOAS + if epoch >= ELECTRA_FORK_EPOCH: + return NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA + return NEXT_SYNC_COMMITTEE_GINDEX + + - name: normalize_merkle_branch#electra sources: - file: packages/state-transition/src/lightClient/spec/utils.ts @@ -9201,15 +9321,19 @@ - file: packages/state-transition/src/block/processBuilderDepositRequest.ts search: export function processBuilderDepositRequest( spec: | - + def process_builder_deposit_request(state: BeaconState, request: BuilderDepositRequest) -> None: + # Ignore deposits with unexpected withdrawal credential prefixes + if not is_builder_withdrawal_credential(request.withdrawal_credentials): + return + builder_pubkeys = [b.pubkey for b in state.builders] if request.pubkey not in builder_pubkeys: if is_valid_builder_deposit_signature(request): add_builder_to_registry( state, request.pubkey, - uint8(request.withdrawal_credentials[0]), + PAYLOAD_BUILDER_VERSION, ExecutionAddress(request.withdrawal_credentials[12:]), request.amount, state.slot, @@ -9218,13 +9342,13 @@ builder_index = BuilderIndex(builder_pubkeys.index(request.pubkey)) builder = state.builders[builder_index] - # Increase balance by deposit amount - builder.balance += request.amount - - # If exited, reset the withdrawable epoch - if builder.withdrawable_epoch != FAR_FUTURE_EPOCH: + # If exited and swept, reset the withdrawable epoch + if builder.withdrawable_epoch != FAR_FUTURE_EPOCH and builder.balance == 0: epoch = get_current_epoch(state) builder.withdrawable_epoch = epoch + MIN_BUILDER_WITHDRAWABILITY_DELAY + + # Increase balance by deposit amount + builder.balance += request.amount - name: process_builder_exit_request#gloas @@ -10452,7 +10576,7 @@ - name: process_operations#gloas sources: [] spec: | - + def process_operations(state: BeaconState, body: BeaconBlockBody) -> None: assert len(body.deposits) == 0 @@ -10460,6 +10584,14 @@ for operation in operations: fn(state, operation) + # [New in Gloas:EIP7688] + assert len(body.proposer_slashings) <= MAX_PROPOSER_SLASHINGS + assert len(body.attester_slashings) <= MAX_ATTESTER_SLASHINGS_ELECTRA + assert len(body.attestations) <= MAX_ATTESTATIONS_ELECTRA + assert len(body.voluntary_exits) <= MAX_VOLUNTARY_EXITS + assert len(body.bls_to_execution_changes) <= MAX_BLS_TO_EXECUTION_CHANGES + assert len(body.payload_attestations) <= MAX_PAYLOAD_ATTESTATIONS + # [Modified in Gloas:EIP7732] for_ops(body.proposer_slashings, process_proposer_slashing) for_ops(body.attester_slashings, process_attester_slashing) @@ -12209,11 +12341,11 @@ - name: update_payload_expected_withdrawals#gloas sources: [] spec: | - + def update_payload_expected_withdrawals( state: BeaconState, withdrawals: Sequence[Withdrawal] ) -> None: - state.payload_expected_withdrawals = List[Withdrawal, MAX_WITHDRAWALS_PER_PAYLOAD](withdrawals) + state.payload_expected_withdrawals = ProgressiveList[Withdrawal](withdrawals) - name: update_pending_partial_withdrawals#electra @@ -12231,11 +12363,14 @@ - name: update_proposer_boost_root#phase0 sources: [] spec: | - + def update_proposer_boost_root(store: Store, head: Root, root: Root) -> None: is_first_block = store.proposer_boost_root == Root() is_timely = store.block_timeliness[root] - is_same_dependent_root = get_dependent_root(store, root) == get_dependent_root(store, head) + epoch = get_current_store_epoch(store) + head_dependent_root = get_shuffling_dependent_root(store, head, epoch) + block_dependent_root = get_shuffling_dependent_root(store, root, epoch) + is_same_dependent_root = head_dependent_root == block_dependent_root # Add proposer score boost if the block is timely, not conflicting with an # existing block, with the same dependent root as the canonical chain head. @@ -12246,12 +12381,15 @@ - name: update_proposer_boost_root#gloas sources: [] spec: | - + def update_proposer_boost_root(store: Store, head: Root, root: Root) -> None: is_first_block = store.proposer_boost_root == Root() # [Modified in Gloas:EIP7732] is_timely = store.block_timeliness[root][ATTESTATION_TIMELINESS_INDEX] - is_same_dependent_root = get_dependent_root(store, root) == get_dependent_root(store, head) + epoch = get_current_store_epoch(store) + head_dependent_root = get_shuffling_dependent_root(store, head, epoch) + block_dependent_root = get_shuffling_dependent_root(store, root, epoch) + is_same_dependent_root = head_dependent_root == block_dependent_root # Add proposer score boost if the block is timely, not conflicting with an # existing block, with the same dependent root as the canonical chain head. @@ -12283,6 +12421,49 @@ store.unrealized_finalized_checkpoint = unrealized_finalized_checkpoint +- name: upgrade_attestation_to_gloas#gloas + sources: + - file: packages/state-transition/src/slot/upgradeStateToGloas.ts + search: export function upgradeAttestationToGloas( + spec: | + + def upgrade_attestation_to_gloas(pre: fulu.Attestation) -> Attestation: + return Attestation( + aggregation_bits=AggregationBits(list(pre.aggregation_bits)), + data=pre.data, + signature=pre.signature, + committee_bits=pre.committee_bits, + ) + + +- name: upgrade_attester_slashing_to_gloas#gloas + sources: + - file: packages/state-transition/src/slot/upgradeStateToGloas.ts + search: export function upgradeAttesterSlashingToGloas( + spec: | + + def upgrade_attester_slashing_to_gloas(pre: fulu.AttesterSlashing) -> AttesterSlashing: + return AttesterSlashing( + attestation_1=upgrade_indexed_attestation_to_gloas(pre.attestation_1), + attestation_2=upgrade_indexed_attestation_to_gloas(pre.attestation_2), + ) + + +- name: upgrade_indexed_attestation_to_gloas#gloas + sources: + - file: packages/state-transition/src/slot/upgradeStateToGloas.ts + search: '^export function upgradeIndexedAttestationToGloas\($' + regex: true + spec: | + + def upgrade_indexed_attestation_to_gloas(pre: fulu.IndexedAttestation) -> IndexedAttestation: + return IndexedAttestation( + attesting_indices=AttestingIndices(list(pre.attesting_indices)), + data=pre.data, + signature=pre.signature, + ) + + - name: upgrade_lc_bootstrap_to_capella#capella sources: - file: packages/state-transition/src/lightClient/spec/utils.ts @@ -12330,12 +12511,14 @@ - name: upgrade_lc_bootstrap_to_gloas#gloas sources: [] spec: | - + def upgrade_lc_bootstrap_to_gloas(pre: electra.LightClientBootstrap) -> LightClientBootstrap: return LightClientBootstrap( header=upgrade_lc_header_to_gloas(pre.header), current_sync_committee=pre.current_sync_committee, - current_sync_committee_branch=pre.current_sync_committee_branch, + current_sync_committee_branch=normalize_merkle_branch( + pre.current_sync_committee_branch, CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS + ), ) @@ -12396,14 +12579,14 @@ - name: upgrade_lc_finality_update_to_gloas#gloas sources: [] spec: | - + def upgrade_lc_finality_update_to_gloas( pre: electra.LightClientFinalityUpdate, ) -> LightClientFinalityUpdate: return LightClientFinalityUpdate( attested_header=upgrade_lc_header_to_gloas(pre.attested_header), finalized_header=upgrade_lc_header_to_gloas(pre.finalized_header), - finality_branch=pre.finality_branch, + finality_branch=normalize_merkle_branch(pre.finality_branch, FINALIZED_ROOT_GINDEX_GLOAS), sync_aggregate=pre.sync_aggregate, signature_slot=pre.signature_slot, ) @@ -12736,14 +12919,16 @@ - name: upgrade_lc_update_to_gloas#gloas sources: [] spec: | - + def upgrade_lc_update_to_gloas(pre: electra.LightClientUpdate) -> LightClientUpdate: return LightClientUpdate( attested_header=upgrade_lc_header_to_gloas(pre.attested_header), next_sync_committee=pre.next_sync_committee, - next_sync_committee_branch=pre.next_sync_committee_branch, + next_sync_committee_branch=normalize_merkle_branch( + pre.next_sync_committee_branch, NEXT_SYNC_COMMITTEE_GINDEX_GLOAS + ), finalized_header=upgrade_lc_header_to_gloas(pre.finalized_header), - finality_branch=pre.finality_branch, + finality_branch=normalize_merkle_branch(pre.finality_branch, FINALIZED_ROOT_GINDEX_GLOAS), sync_aggregate=pre.sync_aggregate, signature_slot=pre.signature_slot, ) @@ -13154,7 +13339,7 @@ - file: packages/state-transition/src/slot/upgradeStateToGloas.ts search: export function upgradeStateToGloas( spec: | - + def upgrade_to_gloas(pre: fulu.BeaconState) -> BeaconState: epoch = fulu.get_current_epoch(pre) @@ -13175,17 +13360,26 @@ eth1_data=pre.eth1_data, eth1_data_votes=pre.eth1_data_votes, eth1_deposit_index=pre.eth1_deposit_index, - validators=pre.validators, - balances=pre.balances, + # [Modified in Gloas:EIP7688] + validators=ProgressiveList[Validator](list(pre.validators)), + # [Modified in Gloas:EIP7688] + balances=ProgressiveList[Gwei](list(pre.balances)), randao_mixes=pre.randao_mixes, slashings=pre.slashings, - previous_epoch_participation=pre.previous_epoch_participation, - current_epoch_participation=pre.current_epoch_participation, + # [Modified in Gloas:EIP7688] + previous_epoch_participation=ProgressiveList[ParticipationFlags]( + list(pre.previous_epoch_participation) + ), + # [Modified in Gloas:EIP7688] + current_epoch_participation=ProgressiveList[ParticipationFlags]( + list(pre.current_epoch_participation) + ), justification_bits=pre.justification_bits, previous_justified_checkpoint=pre.previous_justified_checkpoint, current_justified_checkpoint=pre.current_justified_checkpoint, finalized_checkpoint=pre.finalized_checkpoint, - inactivity_scores=pre.inactivity_scores, + # [Modified in Gloas:EIP7688] + inactivity_scores=ProgressiveList[uint64](list(pre.inactivity_scores)), current_sync_committee=pre.current_sync_committee, next_sync_committee=pre.next_sync_committee, # [Modified in Gloas:EIP7732] @@ -13201,9 +13395,16 @@ earliest_exit_epoch=pre.earliest_exit_epoch, consolidation_balance_to_consume=pre.consolidation_balance_to_consume, earliest_consolidation_epoch=pre.earliest_consolidation_epoch, - pending_deposits=pre.pending_deposits, - pending_partial_withdrawals=pre.pending_partial_withdrawals, - pending_consolidations=pre.pending_consolidations, + # [Modified in Gloas:EIP7688] + pending_deposits=ProgressiveList[PendingDeposit](list(pre.pending_deposits)), + # [Modified in Gloas:EIP7688] + pending_partial_withdrawals=ProgressiveList[PendingPartialWithdrawal]( + list(pre.pending_partial_withdrawals) + ), + # [Modified in Gloas:EIP7688] + pending_consolidations=ProgressiveList[PendingConsolidation]( + list(pre.pending_consolidations) + ), proposer_lookahead=pre.proposer_lookahead, # [New in Gloas:EIP7732] builders=[], @@ -13236,9 +13437,25 @@ - name: upgrade_to_heze#heze sources: [] spec: | - + def upgrade_to_heze(pre: gloas.BeaconState) -> BeaconState: epoch = gloas.get_current_epoch(pre) + latest_execution_payload_bid = ExecutionPayloadBid( + parent_block_hash=pre.latest_execution_payload_bid.parent_block_hash, + parent_block_root=pre.latest_execution_payload_bid.parent_block_root, + block_hash=pre.latest_execution_payload_bid.block_hash, + prev_randao=pre.latest_execution_payload_bid.prev_randao, + fee_recipient=pre.latest_execution_payload_bid.fee_recipient, + gas_limit=pre.latest_execution_payload_bid.gas_limit, + builder_index=pre.latest_execution_payload_bid.builder_index, + slot=pre.latest_execution_payload_bid.slot, + value=pre.latest_execution_payload_bid.value, + execution_payment=pre.latest_execution_payload_bid.execution_payment, + blob_kzg_commitments=pre.latest_execution_payload_bid.blob_kzg_commitments, + execution_requests_root=pre.latest_execution_payload_bid.execution_requests_root, + # [New in Heze:EIP7805] + inclusion_list_bits=Bitvector[INCLUSION_LIST_COMMITTEE_SIZE](), + ) post = BeaconState( genesis_time=pre.genesis_time, @@ -13289,7 +13506,8 @@ execution_payload_availability=pre.execution_payload_availability, builder_pending_payments=pre.builder_pending_payments, builder_pending_withdrawals=pre.builder_pending_withdrawals, - latest_execution_payload_bid=pre.latest_execution_payload_bid, + # [Modified in Heze:EIP7805] + latest_execution_payload_bid=latest_execution_payload_bid, payload_expected_withdrawals=pre.payload_expected_withdrawals, ptc_window=pre.ptc_window, ) @@ -14858,15 +15076,15 @@ - name: validate_partial_data_column_sidecar_gossip#fulu sources: [] spec: | - + def validate_partial_data_column_sidecar_gossip( seen: Seen, store: Store, state: BeaconState, sidecar: PartialDataColumnSidecar, - block_root: Root, - column_index: ColumnIndex, current_time_ms: uint64, + group_id: PartialDataColumnGroupID, + column_index: ColumnIndex, ) -> None: """ Validate a PartialDataColumnSidecar for gossip propagation on a subnet. @@ -14880,11 +15098,11 @@ if not (has_header or has_cells): raise GossipReject("partial message is semantically empty") - # [REJECT] The cell count equals the number of bits set in cells_present_bitmap + # [REJECT] The cell count equals the number of set bits in the bitmap if len(sidecar.partial_column) != num_cells_present: raise GossipReject("number of cells does not match number of set bits") - # [REJECT] The proof count equals the number of bits set in cells_present_bitmap + # [REJECT] The proof count equals the number of set bits in the bitmap if len(sidecar.kzg_proofs) != num_cells_present: raise GossipReject("number of proofs does not match number of set bits") @@ -14893,13 +15111,13 @@ block_header = header.signed_block_header.message # [REJECT] The received header MUST equal any previously validated header for this block - prior_header = seen.partial_data_column_headers.get(block_root) + prior_header = seen.partial_data_column_headers.get(group_id.beacon_block_root) if prior_header is not None and prior_header != header: raise GossipReject("header differs from previously validated header") # [REJECT] The signed_block_header hash matches the partial message's group id - if hash_tree_root(block_header) != block_root: - raise GossipReject("header's block root does not match partial message group id") + if hash_tree_root(block_header) != group_id.beacon_block_root: + raise GossipReject("header's block root does not match group id's block root") # [REJECT] The header's kzg_commitments list is non-empty if len(header.kzg_commitments) == 0: @@ -14959,11 +15177,11 @@ raise GossipReject("header proposer_index does not match expected proposer") # Mark this header as seen - seen.partial_data_column_headers[block_root] = header + seen.partial_data_column_headers[group_id.beacon_block_root] = header if has_cells: # [IGNORE] A valid corresponding PartialDataColumnHeader has been seen - header = seen.partial_data_column_headers.get(block_root) + header = seen.partial_data_column_headers.get(group_id.beacon_block_root) if header is None: raise GossipIgnore("valid corresponding header has not been seen") @@ -14981,11 +15199,11 @@ "corresponding header is not from a slot greater than the latest finalized slot" ) - # [REJECT] The cells_present_bitmap length equals the number of header kzg_commitments + # [REJECT] The cells present bitmap length equals the number of header commitments if len(sidecar.cells_present_bitmap) != len(header.kzg_commitments): raise GossipReject("bitmap length does not match commitments length") - # [REJECT] The sidecar's cell and proof data is valid + # [REJECT] The sidecar's cell and proof data passes KZG verification if not verify_partial_data_column_sidecar_kzg_proofs( sidecar, header.kzg_commitments, column_index ): @@ -15295,11 +15513,11 @@ - file: packages/beacon-node/src/chain/validation/dataColumnSidecar.ts search: function verifyGloasDataColumnSidecar( spec: | - + def verify_data_column_sidecar( sidecar: DataColumnSidecar, # [New in Gloas:EIP7732] - kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], + kzg_commitments: ProgressiveList[KZGCommitment], ) -> bool: """ Verify if the data column sidecar is valid. @@ -15369,11 +15587,11 @@ - file: packages/beacon-node/src/chain/validation/dataColumnSidecar.ts search: export async function verifyDataColumnSidecarKzgProofs( spec: | - + def verify_data_column_sidecar_kzg_proofs( sidecar: DataColumnSidecar, # [New in Gloas:EIP7732] - kzg_commitments: List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK], + kzg_commitments: ProgressiveList[KZGCommitment], ) -> bool: """ Verify if the KZG proofs are correct. diff --git a/specrefs/presets.yml b/specrefs/presets.yml index cb7bef8a6500..4233892e8d6b 100644 --- a/specrefs/presets.yml +++ b/specrefs/presets.yml @@ -7,24 +7,6 @@ BASE_REWARD_FACTOR: uint64 = 64 -- name: BUILDER_PENDING_WITHDRAWALS_LIMIT#gloas - sources: - - file: packages/params/src/presets/mainnet.ts - search: "BUILDER_PENDING_WITHDRAWALS_LIMIT:" - spec: | - - BUILDER_PENDING_WITHDRAWALS_LIMIT: uint64 = 1048576 - - -- name: BUILDER_REGISTRY_LIMIT#gloas - sources: - - file: packages/params/src/presets/mainnet.ts - search: "BUILDER_REGISTRY_LIMIT:" - spec: | - - BUILDER_REGISTRY_LIMIT: uint64 = 1099511627776 - - - name: BYTES_PER_LOGS_BLOOM#bellatrix sources: - file: packages/params/src/presets/mainnet.ts @@ -208,8 +190,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_ATTESTATIONS:" spec: | - - MAX_ATTESTATIONS = 128 + + MAX_ATTESTATIONS: uint64 = 128 - name: MAX_ATTESTATIONS_ELECTRA#electra @@ -217,8 +199,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_ATTESTATIONS_ELECTRA:" spec: | - - MAX_ATTESTATIONS_ELECTRA = 8 + + MAX_ATTESTATIONS_ELECTRA: uint64 = 8 - name: MAX_ATTESTER_SLASHINGS#phase0 @@ -226,8 +208,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_ATTESTER_SLASHINGS:" spec: | - - MAX_ATTESTER_SLASHINGS = 2 + + MAX_ATTESTER_SLASHINGS: uint64 = 2 - name: MAX_ATTESTER_SLASHINGS_ELECTRA#electra @@ -235,8 +217,17 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_ATTESTER_SLASHINGS_ELECTRA:" spec: | - - MAX_ATTESTER_SLASHINGS_ELECTRA = 1 + + MAX_ATTESTER_SLASHINGS_ELECTRA: uint64 = 1 + + +- name: MAX_ATTESTER_SLASHING_SIZE#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_ATTESTER_SLASHING_SIZE:" + spec: | + + MAX_ATTESTER_SLASHING_SIZE: uint64 = 2097616 - name: MAX_BLOB_COMMITMENTS_PER_BLOCK#deneb @@ -253,8 +244,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_BLS_TO_EXECUTION_CHANGES:" spec: | - - MAX_BLS_TO_EXECUTION_CHANGES = 16 + + MAX_BLS_TO_EXECUTION_CHANGES: uint64 = 16 - name: MAX_BUILDERS_PER_WITHDRAWALS_SWEEP#gloas @@ -262,8 +253,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_BUILDERS_PER_WITHDRAWALS_SWEEP:" spec: | - - MAX_BUILDERS_PER_WITHDRAWALS_SWEEP = 16384 + + MAX_BUILDERS_PER_WITHDRAWALS_SWEEP: uint64 = 16384 - name: MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD#gloas @@ -271,8 +262,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD:" spec: | - - MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: uint64 = 256 + + MAX_BUILDER_DEPOSIT_REQUESTS_PER_PAYLOAD: uint64 = 64 - name: MAX_BUILDER_EXIT_REQUESTS_PER_PAYLOAD#gloas @@ -311,13 +302,22 @@ MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD: uint64 = 2 +- name: MAX_DATA_COLUMN_SIDECAR_SIZE#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_DATA_COLUMN_SIDECAR_SIZE:" + spec: | + + MAX_DATA_COLUMN_SIDECAR_SIZE: uint64 = 8585272 + + - name: MAX_DEPOSITS#phase0 sources: - file: packages/params/src/presets/mainnet.ts search: "MAX_DEPOSITS:" spec: | - - MAX_DEPOSITS = 16 + + MAX_DEPOSITS: uint64 = 16 - name: MAX_DEPOSIT_REQUESTS_PER_PAYLOAD#electra @@ -352,8 +352,17 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_EXTRA_DATA_BYTES:" spec: | - - MAX_EXTRA_DATA_BYTES = 32 + + MAX_EXTRA_DATA_BYTES: uint64 = 32 + + +- name: MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE:" + spec: | + + MAX_PARTIAL_DATA_COLUMN_SIDECAR_SIZE: uint64 = 8585741 - name: MAX_PAYLOAD_ATTESTATIONS#gloas @@ -361,8 +370,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_PAYLOAD_ATTESTATIONS:" spec: | - - MAX_PAYLOAD_ATTESTATIONS = 4 + + MAX_PAYLOAD_ATTESTATIONS: uint64 = 4 - name: MAX_PENDING_DEPOSITS_PER_EPOCH#electra @@ -388,8 +397,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_PROPOSER_SLASHINGS:" spec: | - - MAX_PROPOSER_SLASHINGS = 16 + + MAX_PROPOSER_SLASHINGS: uint64 = 16 - name: MAX_SEED_LOOKAHEAD#phase0 @@ -401,6 +410,38 @@ MAX_SEED_LOOKAHEAD: uint64 = 4 +- name: MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE:" + spec: | + + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE: uint64 = 16829 + + +- name: MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE#gloas + sources: + - file: packages/params/src/presets/mainnet.ts + search: "MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE:" + spec: | + + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE: uint64 = 196932 + + +- name: MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE#heze + sources: [] + spec: | + + MAX_SIGNED_EXECUTION_PAYLOAD_BID_SIZE_HEZE: uint64 = 196934 + + +- name: MAX_SIGNED_INCLUSION_LIST_SIZE#heze + sources: [] + spec: | + + MAX_SIGNED_INCLUSION_LIST_SIZE: uint64 = 8348 + + - name: MAX_TRANSACTIONS_PER_PAYLOAD#bellatrix sources: - file: packages/params/src/presets/mainnet.ts @@ -424,8 +465,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP:" spec: | - - MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP = 16384 + + MAX_VALIDATORS_PER_WITHDRAWALS_SWEEP: uint64 = 16384 - name: MAX_VOLUNTARY_EXITS#phase0 @@ -433,8 +474,8 @@ - file: packages/params/src/presets/mainnet.ts search: "MAX_VOLUNTARY_EXITS:" spec: | - - MAX_VOLUNTARY_EXITS = 16 + + MAX_VOLUNTARY_EXITS: uint64 = 16 - name: MAX_WITHDRAWALS_PER_PAYLOAD#capella @@ -550,8 +591,8 @@ - file: packages/params/src/presets/mainnet.ts search: "NUMBER_OF_COLUMNS:" spec: | - - NUMBER_OF_COLUMNS: uint64 = 128 + + NUMBER_OF_COLUMNS = 128 - name: PENDING_CONSOLIDATIONS_LIMIT#electra diff --git a/specrefs/types.yml b/specrefs/types.yml index 09b3007cdbcd..f0028ee3aa41 100644 --- a/specrefs/types.yml +++ b/specrefs/types.yml @@ -1,3 +1,39 @@ +- name: AggregationBits#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const AggregationBits = + spec: | + + AggregationBits = Bitlist[MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] + + +- name: AggregationBits#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const AggregationBits = + spec: | + + AggregationBits = ProgressiveBitlist + + +- name: AttestingIndices#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const AttestingIndices = + spec: | + + AttestingIndices = List[ValidatorIndex, MAX_VALIDATORS_PER_COMMITTEE * MAX_COMMITTEES_PER_SLOT] + + +- name: AttestingIndices#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const AttestingIndices = + spec: | + + AttestingIndices = ProgressiveList[ValidatorIndex] + + - name: BLSPubkey#phase0 sources: - file: packages/types/src/primitive/sszTypes.ts @@ -39,8 +75,26 @@ - file: packages/types/src/gloas/sszTypes.ts search: export const BlockAccessList = spec: | - - BlockAccessList = ByteList[MAX_BYTES_PER_TRANSACTION] + + BlockAccessList = ProgressiveByteList + + +- name: BuilderDepositRequests#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const BuilderDepositRequests = + spec: | + + BuilderDepositRequests = ProgressiveList[BuilderDepositRequest] + + +- name: BuilderExitRequests#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const BuilderExitRequests = + spec: | + + BuilderExitRequests = ProgressiveList[BuilderExitRequest] - name: BuilderIndex#gloas @@ -93,6 +147,24 @@ CommitteeIndex = uint64 +- name: ConsolidationRequests#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const ConsolidationRequests = + spec: | + + ConsolidationRequests = List[ConsolidationRequest, MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD] + + +- name: ConsolidationRequests#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const ConsolidationRequests = + spec: | + + ConsolidationRequests = ProgressiveList[ConsolidationRequest] + + - name: CurrentSyncCommitteeBranch#altair sources: - file: packages/types/src/altair/sszTypes.ts @@ -111,6 +183,15 @@ CurrentSyncCommitteeBranch = Vector[Bytes32, floorlog2(CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA)] +- name: CurrentSyncCommitteeBranch#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const CurrentSyncCommitteeBranch = + spec: | + + CurrentSyncCommitteeBranch = Vector[Bytes32, floorlog2(CURRENT_SYNC_COMMITTEE_GINDEX_GLOAS)] + + - name: CustodyIndex#fulu sources: - file: packages/types/src/primitive/sszTypes.ts @@ -120,6 +201,24 @@ CustodyIndex = uint64 +- name: DepositRequests#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const DepositRequests = + spec: | + + DepositRequests = List[DepositRequest, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD] + + +- name: DepositRequests#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const DepositRequests = + spec: | + + DepositRequests = ProgressiveList[DepositRequest] + + - name: Domain#phase0 sources: - file: packages/types/src/primitive/sszTypes.ts @@ -197,6 +296,15 @@ FinalityBranch = Vector[Bytes32, floorlog2(FINALIZED_ROOT_GINDEX_ELECTRA)] +- name: FinalityBranch#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const FinalityBranch = + spec: | + + FinalityBranch = Vector[Bytes32, floorlog2(FINALIZED_ROOT_GINDEX_GLOAS)] + + - name: ForkDigest#phase0 sources: - file: packages/types/src/primitive/sszTypes.ts @@ -276,6 +384,15 @@ NextSyncCommitteeBranch = Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA)] +- name: NextSyncCommitteeBranch#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const NextSyncCommitteeBranch = + spec: | + + NextSyncCommitteeBranch = Vector[Bytes32, floorlog2(NEXT_SYNC_COMMITTEE_GINDEX_GLOAS)] + + - name: NodeID#phase0 sources: [] spec: | @@ -356,6 +473,15 @@ Transaction = ByteList[MAX_BYTES_PER_TRANSACTION] +- name: Transaction#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const Transaction = + spec: | + + Transaction = ProgressiveByteList + + - name: ValidatorIndex#phase0 sources: - file: packages/types/src/primitive/sszTypes.ts @@ -391,3 +517,21 @@ WithdrawalIndex = uint64 + +- name: WithdrawalRequests#electra + sources: + - file: packages/types/src/electra/sszTypes.ts + search: export const WithdrawalRequests = + spec: | + + WithdrawalRequests = List[WithdrawalRequest, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD] + + +- name: WithdrawalRequests#gloas + sources: + - file: packages/types/src/gloas/sszTypes.ts + search: export const WithdrawalRequests = + spec: | + + WithdrawalRequests = ProgressiveList[WithdrawalRequest] +