From 1bbc7ed841a783a31665713358f81078653c45ef Mon Sep 17 00:00:00 2001 From: Cayman Date: Wed, 20 May 2026 12:21:45 -0400 Subject: [PATCH 1/3] feat: support eip-7688 progressive containers --- package.json | 1 + packages/api/package.json | 4 +- packages/beacon-node/package.json | 4 +- .../blocks/verifyExecutionPayloadEnvelope.ts | 6 +- packages/beacon-node/src/chain/chain.ts | 5 +- packages/beacon-node/src/chain/interface.ts | 3 +- .../src/chain/lightClient/index.ts | 19 +- .../src/chain/lightClient/proofs.ts | 37 ++- .../src/chain/lightClient/types.ts | 4 +- .../chain/produceBlock/produceBlockBody.ts | 8 +- .../validation/executionPayloadEnvelope.ts | 2 +- .../signatureSets/aggregateAndProof.ts | 6 +- .../lightclientSyncCommitteeWitness.ts | 66 ++++- .../src/network/gossip/encoding.ts | 20 +- .../src/network/gossip/gossipsub.ts | 9 +- .../beacon-node/src/network/gossip/topic.ts | 33 ++- .../beacon-node/src/network/peers/discover.ts | 22 +- .../test/spec/general/index.test.ts | 11 - .../test/spec/general/ssz_generic_types.ts | 176 +++++++++++- .../light_client/single_merkle_proof.ts | 26 +- .../replaceUintTypeWithUintBigintType.ts | 23 +- .../test/spec/utils/specTestIterator.ts | 5 +- .../test/unit/chain/lightclient/proof.test.ts | 68 ++++- .../validation/aggregateAndProof.test.ts | 27 +- .../test/unit/network/gossip/topic.test.ts | 94 +++++- packages/cli/package.json | 4 +- packages/config/package.json | 2 +- packages/db/package.json | 2 +- packages/fork-choice/package.json | 2 +- packages/logger/package.json | 2 +- packages/params/src/index.ts | 42 +++ packages/params/src/presets/mainnet.ts | 9 + packages/params/src/presets/minimal.ts | 9 + packages/params/src/types.ts | 12 + packages/reqresp/package.json | 2 +- packages/spec-test-util/package.json | 2 +- packages/state-transition/package.json | 4 +- .../src/block/processOperations.ts | 29 +- .../block/processParentExecutionPayload.ts | 32 ++- .../src/block/processWithdrawals.ts | 2 +- .../state-transition/src/cache/stateCache.ts | 6 +- .../epoch/processParticipationFlagUpdates.ts | 15 +- .../src/epoch/processRewardsAndPenalties.ts | 3 +- .../src/lightClient/proofs.ts | 37 ++- .../src/lightClient/spec/index.ts | 12 +- .../src/lightClient/spec/utils.ts | 218 ++++++++++++-- .../spec/validateLightClientBootstrap.ts | 13 +- .../spec/validateLightClientUpdate.ts | 32 +-- .../state-transition/src/lightClient/types.ts | 4 +- packages/state-transition/src/metrics.ts | 6 +- .../src/slot/upgradeStateToGloas.ts | 32 ++- .../src/stateView/beaconStateView.ts | 2 +- .../src/stateView/interface.ts | 2 +- .../state-transition/src/util/execution.ts | 13 +- .../processParticipationFlagUpdates.test.ts | 25 ++ .../epoch/processRewardsAndPenalties.test.ts | 62 ++++ packages/types/package.json | 2 +- packages/types/src/gloas/sszTypes.ts | 271 ++++++++++++++++-- packages/types/src/gloas/types.ts | 31 ++ packages/types/src/types.ts | 26 +- packages/types/src/utils/typeguards.ts | 7 +- .../test/unit/constants/lightclient.test.ts | 72 ++++- .../types/test/unit/gloas/eip7688.test.ts | 52 ++++ packages/utils/package.json | 2 +- packages/validator/package.json | 2 +- .../validator/src/services/validatorStore.ts | 8 +- packages/validator/src/util/params.ts | 6 + pnpm-lock.yaml | 87 +++--- scripts/kurtosis/run.sh | 4 +- 69 files changed, 1575 insertions(+), 311 deletions(-) create mode 100644 packages/state-transition/test/unit/epoch/processParticipationFlagUpdates.test.ts create mode 100644 packages/state-transition/test/unit/epoch/processRewardsAndPenalties.test.ts create mode 100644 packages/types/test/unit/gloas/eip7688.test.ts diff --git a/package.json b/package.json index d9afee7aa9b7..a3bb660924e0 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@biomejs/biome": "^2.2.0", "@chainsafe/benchmark": "^2.0.2", "@chainsafe/biomejs-config": "^1.0.0", + "@chainsafe/ssz": "^1.6.0", "@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 f326b8c15950..cad7dc4b0284 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.0", "@lodestar/config": "workspace:^", "@lodestar/params": "workspace:^", "@lodestar/types": "workspace:^", diff --git a/packages/beacon-node/package.json b/packages/beacon-node/package.json index b29cb0d36340..49f6621b2263 100644 --- a/packages/beacon-node/package.json +++ b/packages/beacon-node/package.json @@ -114,11 +114,11 @@ "@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/pubkey-index-map": "^3.0.0", "@chainsafe/snappy-wasm": "^0.5.0", - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.0", "@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/chain/blocks/verifyExecutionPayloadEnvelope.ts b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts index 8b838acc45b5..91a9cf441181 100644 --- a/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/blocks/verifyExecutionPayloadEnvelope.ts @@ -74,7 +74,7 @@ export function verifyExecutionPayloadEnvelope( // Verify execution_requests_root matches bid commitment. // Can be skipped if already verified during gossip validation. if (verifyExecutionRequestsRoot) { - const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); + const requestsRoot = ssz.gloas.ExecutionRequests.hashTreeRoot(envelope.executionRequests); if (!byteArrayEquals(requestsRoot, bid.executionRequestsRoot)) { throw new Error( `Execution requests root mismatch envelope=${toRootHex(requestsRoot)} bid=${toRootHex(bid.executionRequestsRoot)}` @@ -102,8 +102,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/chain.ts b/packages/beacon-node/src/chain/chain.ts index 67041b166150..e8397964ce37 100644 --- a/packages/beacon-node/src/chain/chain.ts +++ b/packages/beacon-node/src/chain/chain.ts @@ -40,7 +40,6 @@ import { ValidatorIndex, Wei, deneb, - electra, gloas, isBlindedBeaconBlock, phase0, @@ -916,10 +915,10 @@ export class BeaconChain implements IBeaconChain { async getParentExecutionRequests( parentBlockSlot: Slot, parentBlockRootHex: RootHex - ): Promise { + ): Promise { // at the fork boundary, parent is pre-gloas if (!isForkPostGloas(this.config.getForkName(parentBlockSlot))) { - return ssz.electra.ExecutionRequests.defaultValue(); + return ssz.gloas.ExecutionRequests.defaultValue(); } const envelope = await this.getExecutionPayloadEnvelope(parentBlockSlot, parentBlockRootHex); if (envelope === null) { diff --git a/packages/beacon-node/src/chain/interface.ts b/packages/beacon-node/src/chain/interface.ts index a303e4f51842..077b2e87b4ba 100644 --- a/packages/beacon-node/src/chain/interface.ts +++ b/packages/beacon-node/src/chain/interface.ts @@ -18,7 +18,6 @@ import { altair, capella, deneb, - electra, gloas, phase0, rewards, @@ -236,7 +235,7 @@ export interface IBeaconChain { blockSlot: Slot, blockRootHex: string ): Promise; - getParentExecutionRequests(parentBlockSlot: Slot, parentBlockRootHex: RootHex): Promise; + getParentExecutionRequests(parentBlockSlot: Slot, parentBlockRootHex: RootHex): Promise; produceCommonBlockBody(blockAttributes: BlockAttributes): Promise; produceBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise}): Promise<{ 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/produceBlock/produceBlockBody.ts b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts index ec853bdb6e2b..66e8ccd9d06c 100644 --- a/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts +++ b/packages/beacon-node/src/chain/produceBlock/produceBlockBody.ts @@ -112,7 +112,7 @@ export type ProduceFullGloas = { type: BlockType.Full; fork: ForkPostGloas; executionPayload: ExecutionPayload; - executionRequests: electra.ExecutionRequests; + executionRequests: gloas.ExecutionRequests; blobsBundle: BlobsBundle; cells: fulu.Cell[][]; parentBlockRoot: Root; @@ -223,7 +223,7 @@ export async function produceBlockBody( // Get execution payload from EL let parentBlockHash: Bytes32; - let parentExecutionRequests: electra.ExecutionRequests; + let parentExecutionRequests: gloas.ExecutionRequests; // Apply parent payload once here as it's reused by EL prep and voluntary exit filtering below let stateAfterParentPayload: IBeaconStateViewBellatrix = currentState; const isExtendingPayload = this.forkChoice.shouldExtendPayload(toRootHex(parentBlockRoot)); @@ -233,7 +233,7 @@ export async function produceBlockBody( stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests); } else { parentBlockHash = currentState.latestExecutionPayloadBid.parentBlockHash; - parentExecutionRequests = ssz.electra.ExecutionRequests.defaultValue(); + parentExecutionRequests = ssz.gloas.ExecutionRequests.defaultValue(); } const prepareRes = await prepareExecutionPayload( this, @@ -288,7 +288,7 @@ export async function produceBlockBody( value: 0, executionPayment: 0, blobKzgCommitments: blobsBundle.commitments, - executionRequestsRoot: ssz.electra.ExecutionRequests.hashTreeRoot(executionRequests), + executionRequestsRoot: ssz.gloas.ExecutionRequests.hashTreeRoot(executionRequests), }; const signedBid: gloas.SignedExecutionPayloadBid = { message: bid, diff --git a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts index 862f86ca6784..f39d98687811 100644 --- a/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts +++ b/packages/beacon-node/src/chain/validation/executionPayloadEnvelope.ts @@ -108,7 +108,7 @@ async function validateExecutionPayloadEnvelope( } // [REJECT] `hash_tree_root(envelope.execution_requests) == bid.execution_requests_root` - const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(envelope.executionRequests); + const requestsRoot = ssz.gloas.ExecutionRequests.hashTreeRoot(envelope.executionRequests); if (!byteArrayEquals(requestsRoot, payloadInput.getBid().executionRequestsRoot)) { throw new ExecutionPayloadEnvelopeError(GossipAction.REJECT, { code: ExecutionPayloadEnvelopeErrorCode.EXECUTION_REQUESTS_ROOT_MISMATCH, 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 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}`); } @@ -132,9 +133,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 f5d5b0a22683..7039bb788dac 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"; @@ -178,7 +183,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 2ad9b5173a65..2e98b10d1221 100644 --- a/packages/beacon-node/src/network/gossip/topic.ts +++ b/packages/beacon-node/src/network/gossip/topic.ts @@ -1,12 +1,19 @@ +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_BEACON_BLOCK_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 +92,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 +137,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 the preset p2p bounds instead. + switch (topic.type) { + case GossipType.beacon_block: + return isForkPostGloas(fork) ? MAX_SIGNED_BEACON_BLOCK_SIZE : 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/peers/discover.ts b/packages/beacon-node/src/network/peers/discover.ts index 3da865f8bbc7..59fcc26fb701 100644 --- a/packages/beacon-node/src/network/peers/discover.ts +++ b/packages/beacon-node/src/network/peers/discover.ts @@ -141,6 +141,17 @@ export class PeerDiscovery { this.discv5FirstQueryDelayMs = opts.discv5FirstQueryDelayMs; this.connectToDiscv5BootnodesOnStart = opts.connectToDiscv5Bootnodes; + // Transport tags vary by library: @libp2p/tcp uses '@libp2p/tcp', @chainsafe/libp2p-quic uses 'quic' + // Normalize to simple 'tcp' / 'quic' strings for matching + this.transports = libp2p.services.components.transportManager + .getTransports() + .map((t) => t[Symbol.toStringTag]) + .map((tag) => { + if (tag?.includes("tcp")) return "tcp"; + if (tag?.includes("quic")) return "quic"; + return tag; + }); + this.libp2p.addEventListener("peer:discovery", this.onDiscoveredPeer); this.discv5.on("discovered", this.onDiscoveredENR); @@ -183,17 +194,6 @@ export class PeerDiscovery { } }); } - - // Transport tags vary by library: @libp2p/tcp uses '@libp2p/tcp', @chainsafe/libp2p-quic uses 'quic' - // Normalize to simple 'tcp' / 'quic' strings for matching - this.transports = libp2p.services.components.transportManager - .getTransports() - .map((t) => t[Symbol.toStringTag]) - .map((tag) => { - if (tag?.includes("tcp")) return "tcp"; - if (tag?.includes("quic")) return "quic"; - return tag; - }); } static async init(modules: PeerDiscoveryModules, opts: PeerDiscoveryOpts): Promise { 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..76a1c5f8115b 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,15 @@ import { BitListType, BitVectorType, BooleanType, + ByteListType, + CompatibleUnionType, ContainerType, ListBasicType, + ListCompositeType, + ProgressiveBitListType, + ProgressiveContainerType, + ProgressiveListBasicType, + ProgressiveListCompositeType, Type, UintBigintType, UintNumberType, @@ -66,12 +73,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 ProgressiveListBasicType(byte), + 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 +105,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 +265,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 +292,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 +309,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/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/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 c0ee63af2bcb..38a65a0a3ca4 100644 --- a/packages/beacon-node/test/spec/utils/specTestIterator.ts +++ b/packages/beacon-node/test/spec/utils/specTestIterator.ts @@ -77,9 +77,8 @@ export const defaultSkipOpts: SkipOpts = { // cell level DAS is ready /^fulu\/ssz_static\/PartialDataColumn(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. 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/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/network/gossip/topic.test.ts b/packages/beacon-node/test/unit/network/gossip/topic.test.ts index 5caa4be1802e..826b9c56e93c 100644 --- a/packages/beacon-node/test/unit/network/gossip/topic.test.ts +++ b/packages/beacon-node/test/unit/network/gossip/topic.test.ts @@ -1,9 +1,25 @@ import {describe, expect, it} from "vitest"; import {createBeaconConfig} from "@lodestar/config"; import {config as chainConfig} from "@lodestar/config/default"; -import {ForkName, GENESIS_EPOCH, ZERO_HASH} from "@lodestar/params"; +import { + ForkName, + GENESIS_EPOCH, + MAX_ATTESTER_SLASHING_SIZE, + MAX_DATA_COLUMN_SIDECAR_SIZE, + MAX_SIGNED_AGGREGATE_AND_PROOF_SIZE, + MAX_SIGNED_BEACON_BLOCK_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 {parseGossipTopic, stringifyGossipTopic} from "../../../../src/network/gossip/topic.js"; +import { + GossipTopicCache, + getGossipSSZMaxSize, + getGossipSSZType, + parseGossipTopic, + stringifyGossipTopic, +} from "../../../../src/network/gossip/topic.js"; describe("network / gossip / topic", () => { const config = createBeaconConfig({...chainConfig, GLOAS_FORK_EPOCH: 700000}, ZERO_HASH); @@ -216,4 +232,78 @@ describe("network / gossip / topic", () => { expect(() => parseGossipTopic(config, topicStr)).toThrow(); }); } + + 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]: MAX_SIGNED_BEACON_BLOCK_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}`); + }); }); diff --git a/packages/cli/package.json b/packages/cli/package.json index b4c6df389d42..54d21812954d 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -62,9 +62,9 @@ "@chainsafe/blst": "^2.2.0", "@chainsafe/discv5": "^12.0.1", "@chainsafe/enr": "^6.0.1", - "@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.0", "@chainsafe/threads": "^1.11.3", "@libp2p/crypto": "^5.1.13", "@libp2p/interface": "^3.1.0", diff --git a/packages/config/package.json b/packages/config/package.json index 736250e4324d..acec410a6110 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.0", "@lodestar/params": "workspace:^", "@lodestar/spec-test-util": "workspace:^", "@lodestar/types": "workspace:^", diff --git a/packages/db/package.json b/packages/db/package.json index c5137548a800..ee4197f545d2 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.0", "@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 c6642eca4207..403be76bb1fc 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.0", "@lodestar/config": "workspace:^", "@lodestar/params": "workspace:^", "@lodestar/state-transition": "workspace:^", diff --git a/packages/logger/package.json b/packages/logger/package.json index dfe5a47893e6..c637da2fa034 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.0", "@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 34bc96977b4f..84b17728e995 100644 --- a/packages/params/src/index.ts +++ b/packages/params/src/index.ts @@ -120,6 +120,12 @@ export const { 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, + MAX_SIGNED_BEACON_BLOCK_SIZE, } = activePreset; //////////// @@ -243,6 +249,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 = 11424; +export const EXECUTION_BLOCK_HASH_DEPTH_GLOAS = 13; +export const EXECUTION_BLOCK_HASH_INDEX_GLOAS = 3232; + /** * ```ts * config.types.altair.BeaconState.getPathGindex(["currentSyncCommittee"]) @@ -316,6 +349,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 4ec676b16471..80ccdafa06e3 100644 --- a/packages/params/src/presets/mainnet.ts +++ b/packages/params/src/presets/mainnet.ts @@ -148,4 +148,13 @@ export const mainnetPreset: BeaconPreset = { 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, + MAX_SIGNED_BEACON_BLOCK_SIZE: 4034304, }; diff --git a/packages/params/src/presets/minimal.ts b/packages/params/src/presets/minimal.ts index 9d46b81f02e9..9e44b6b0228d 100644 --- a/packages/params/src/presets/minimal.ts +++ b/packages/params/src/presets/minimal.ts @@ -149,4 +149,13 @@ export const minimalPreset: BeaconPreset = { 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, + MAX_SIGNED_BEACON_BLOCK_SIZE: 1944980, }; diff --git a/packages/params/src/types.ts b/packages/params/src/types.ts index e4451914fdfd..c1ad2273ed0d 100644 --- a/packages/params/src/types.ts +++ b/packages/params/src/types.ts @@ -110,6 +110,12 @@ export type BeaconPreset = { 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; + MAX_SIGNED_BEACON_BLOCK_SIZE: number; }; /** @@ -225,6 +231,12 @@ export const beaconPresetTypes: BeaconPresetTypes = { 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", + MAX_SIGNED_BEACON_BLOCK_SIZE: "number", }; type BeaconPresetTypes = { diff --git a/packages/reqresp/package.json b/packages/reqresp/package.json index 335c73087101..db2eeaf6590e 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.0", "@libp2p/crypto": "^5.1.13", "@libp2p/logger": "^6.2.2", "@libp2p/peer-id": "^6.0.4", diff --git a/packages/spec-test-util/package.json b/packages/spec-test-util/package.json index 7d6a415f4f7e..98998e21a7f7 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.0", "@lodestar/types": "workspace:^", "vitest": "catalog:" } diff --git a/packages/state-transition/package.json b/packages/state-transition/package.json index ed1c6274f00b..511fee71d667 100644 --- a/packages/state-transition/package.json +++ b/packages/state-transition/package.json @@ -67,10 +67,10 @@ "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/persistent-ts": "^1.0.0", "@chainsafe/pubkey-index-map": "^3.0.0", - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.0", "@chainsafe/swap-or-not-shuffle": "^1.2.1", "@lodestar/config": "workspace:^", "@lodestar/params": "workspace:^", diff --git a/packages/state-transition/src/block/processOperations.ts b/packages/state-transition/src/block/processOperations.ts index b4455f5315d5..4303a3caf9f3 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 const maxDeposits = getEth1DepositCount(state); if (body.deposits.length !== maxDeposits) { @@ -93,3 +105,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 0781bb68113b..5e213ed0f0ca 100644 --- a/packages/state-transition/src/block/processParentExecutionPayload.ts +++ b/packages/state-transition/src/block/processParentExecutionPayload.ts @@ -1,5 +1,12 @@ -import {ForkPostGloas, SLOTS_PER_EPOCH, SLOTS_PER_HISTORICAL_ROOT} from "@lodestar/params"; -import {BeaconBlock, electra, ssz} from "@lodestar/types"; +import { + ForkPostGloas, + MAX_CONSOLIDATION_REQUESTS_PER_PAYLOAD, + MAX_DEPOSIT_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"; import {computeEpochAtSlot} from "../util/epoch.js"; @@ -26,7 +33,8 @@ export function processParentExecutionPayload(state: CachedBeaconStateGloas, blo } // Parent was FULL -- verify the bid commitment and apply the payload - const requestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot(requests); + assertExecutionRequestsWithinLimits(requests); + const requestsRoot = ssz.gloas.ExecutionRequests.hashTreeRoot(requests); if (!byteArrayEquals(requestsRoot, parentBid.executionRequestsRoot)) { throw new Error( `Parent execution requests root mismatch actual=${toRootHex(requestsRoot)} expected=${toRootHex(parentBid.executionRequestsRoot)}` @@ -45,7 +53,9 @@ 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: electra.ExecutionRequests): void { +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; @@ -110,7 +120,19 @@ function settleBuilderPayment(state: CachedBeaconStateGloas, paymentIndex: numbe state.builderPendingPayments.set(paymentIndex, ssz.gloas.BuilderPendingPayment.defaultViewDU()); } -function assertEmptyExecutionRequests(requests: electra.ExecutionRequests): void { +function assertExecutionRequestsWithinLimits(requests: gloas.ExecutionRequests): void { + assertMaxLength("deposits", requests.deposits.length, MAX_DEPOSIT_REQUESTS_PER_PAYLOAD); + assertMaxLength("withdrawals", requests.withdrawals.length, MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD); + assertMaxLength("consolidations", requests.consolidations.length, MAX_CONSOLIDATION_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 || requests.withdrawals.length !== 0 || requests.consolidations.length !== 0) { throw new Error("Parent execution requests must be empty when parent block is EMPTY"); } 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/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/upgradeStateToGloas.ts b/packages/state-transition/src/slot/upgradeStateToGloas.ts index 786f2b76a8e0..241b24ce4439 100644 --- a/packages/state-transition/src/slot/upgradeStateToGloas.ts +++ b/packages/state-transition/src/slot/upgradeStateToGloas.ts @@ -34,23 +34,27 @@ 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 = ssz.gloas.Validators.toViewDU(stateGloasCloned.validators.getAllReadonlyValues()); + 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; stateGloasView.latestExecutionPayloadBid.gasLimit = BigInt(stateFulu.latestExecutionPayloadHeader.gasLimit); - stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = ssz.electra.ExecutionRequests.hashTreeRoot( - ssz.electra.ExecutionRequests.defaultValue() + stateGloasView.latestExecutionPayloadBid.executionRequestsRoot = ssz.gloas.ExecutionRequests.hashTreeRoot( + ssz.gloas.ExecutionRequests.defaultValue() ); stateGloasView.nextWithdrawalIndex = stateGloasCloned.nextWithdrawalIndex; stateGloasView.nextWithdrawalValidatorIndex = stateGloasCloned.nextWithdrawalValidatorIndex; @@ -61,9 +65,15 @@ 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 = ssz.gloas.PendingDeposits.toViewDU( + stateGloasCloned.pendingDeposits.getAllReadonlyValues() + ); + stateGloasView.pendingPartialWithdrawals = ssz.gloas.PendingPartialWithdrawals.toViewDU( + stateGloasCloned.pendingPartialWithdrawals.getAllReadonlyValues() + ); + stateGloasView.pendingConsolidations = ssz.gloas.PendingConsolidations.toViewDU( + stateGloasCloned.pendingConsolidations.getAllReadonlyValues() + ); stateGloasView.proposerLookahead = stateGloasCloned.proposerLookahead; stateGloasView.ptcWindow = ssz.gloas.PtcWindow.toViewDU(initializePtcWindow(stateFulu)); @@ -93,7 +103,7 @@ function onboardBuildersFromPendingDeposits(state: CachedBeaconStateGloas): void // Track pubkeys of new builders added when applying deposits 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/stateView/beaconStateView.ts b/packages/state-transition/src/stateView/beaconStateView.ts index 5fcdea7258f0..200a670578b8 100644 --- a/packages/state-transition/src/stateView/beaconStateView.ts +++ b/packages/state-transition/src/stateView/beaconStateView.ts @@ -814,7 +814,7 @@ export class BeaconStateView implements IBeaconStateViewLatestFork { /** * Spec: https://github.com/ethereum/consensus-specs/blob/v1.7.0-alpha.5/specs/gloas/validator.md#executionpayload */ - withParentPayloadApplied(executionRequests: electra.ExecutionRequests): IBeaconStateViewGloas { + withParentPayloadApplied(executionRequests: gloas.ExecutionRequests): IBeaconStateViewGloas { if (this.config.getForkSeq(this.cachedState.slot) < ForkSeq.gloas) { throw new Error("withParentPayloadApplied is not available before Gloas"); } diff --git a/packages/state-transition/src/stateView/interface.ts b/packages/state-transition/src/stateView/interface.ts index 7f5fd7006939..f3bf77226ee3 100644 --- a/packages/state-transition/src/stateView/interface.ts +++ b/packages/state-transition/src/stateView/interface.ts @@ -260,7 +260,7 @@ export interface IBeaconStateViewGloas extends IBeaconStateViewFulu { * operation selection (e.g. voluntary exits) see the same post-apply state that the block * processor will see at import. */ - withParentPayloadApplied(executionRequests: electra.ExecutionRequests): IBeaconStateViewGloas; + withParentPayloadApplied(executionRequests: gloas.ExecutionRequests): IBeaconStateViewGloas; } /** 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/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/types/package.json b/packages/types/package.json index 83f60521777c..5486de5e6cb3 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.0", "@lodestar/params": "workspace:^", "ethereum-cryptography": "^2.0.0" }, diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index f6661f25aa43..d8c47a022530 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -4,16 +4,23 @@ import { 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_BYTES_PER_TRANSACTION, - MAX_PAYLOAD_ATTESTATIONS, MIN_SEED_LOOKAHEAD, + NEXT_SYNC_COMMITTEE_DEPTH_GLOAS, NUMBER_OF_COLUMNS, PTC_SIZE, SLOTS_PER_EPOCH, @@ -46,8 +53,141 @@ const { Uint8, BuilderIndex, EpochInf, + ParticipationFlags, } = primitiveSsz; +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( + { + aggregationBits: AggregationBits, + data: phase0Ssz.AttestationData, + signature: BLSSignature, + committeeBits: electraSsz.CommitteeBits, + }, + activeFields(4), + {typeName: "Attestation", jsonCase: "eth2"} +); + +export const IndexedAttestation = new ProgressiveContainerType( + { + attestingIndices: AttestingIndices, + data: phase0Ssz.AttestationData, + signature: BLSSignature, + }, + activeFields(3), + {typeName: "IndexedAttestation", jsonCase: "eth2"} +); + +/** Same as `IndexedAttestation` but epoch, slot and index are not bounded and must be a bigint */ +export const IndexedAttestationBigint = new ProgressiveContainerType( + { + attestingIndices: AttestingIndices, + data: phase0Ssz.AttestationDataBigint, + signature: BLSSignature, + }, + 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", +}); + +export const ExecutionRequests = new ProgressiveContainerType( + { + deposits: DepositRequests, + withdrawals: WithdrawalRequests, + consolidations: ConsolidationRequests, + }, + activeFields(3), + {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, @@ -77,6 +217,11 @@ export const BuilderPendingPayment = new ContainerType( {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, @@ -102,6 +247,10 @@ export const PayloadAttestation = new ContainerType( {typeName: "PayloadAttestation", jsonCase: "eth2"} ); +export const PayloadAttestations = new ProgressiveListCompositeType(PayloadAttestation, { + typeName: "PayloadAttestations", +}); + export const PayloadAttestationMessage = new ContainerType( { validatorIndex: ValidatorIndex, @@ -151,7 +300,7 @@ export const ExecutionPayloadBid = new ContainerType( slot: Slot, value: UintNum64, executionPayment: UintNum64, - blobKzgCommitments: denebSsz.BlobKzgCommitments, + blobKzgCommitments: BlobKzgCommitments, executionRequestsRoot: Root, }, {typeName: "ExecutionPayloadBid", jsonCase: "eth2"} @@ -167,19 +316,22 @@ export const SignedExecutionPayloadBid = new ContainerType( export const BlockAccessList = new ByteListType(MAX_BYTES_PER_TRANSACTION); -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( { payload: ExecutionPayload, - executionRequests: electraSsz.ExecutionRequests, + executionRequests: ExecutionRequests, builderIndex: BuilderIndex, beaconBlockRoot: Root, parentBeaconBlockRoot: Root, @@ -195,25 +347,26 @@ export const SignedExecutionPayloadEnvelope = new ContainerType( {typeName: "SignedExecutionPayloadEnvelope", jsonCase: "eth2"} ); -export const BeaconBlockBody = new ContainerType( +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 - parentExecutionRequests: electraSsz.ExecutionRequests, // New in GLOAS:EIP7732 + payloadAttestations: PayloadAttestations, // New in GLOAS:EIP7732 + parentExecutionRequests: ExecutionRequests, // New in GLOAS:EIP7732 }, + activeFields(13), {typeName: "BeaconBlockBody", jsonCase: "eth2", cachePermanentRootStruct: true} ); @@ -233,7 +386,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, @@ -250,21 +462,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, @@ -282,28 +494,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 de7949e54651..e35bb3a65abc 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; @@ -21,6 +46,12 @@ export type SignedExecutionPayloadEnvelope = 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 e4cf1d2e7406..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,13 +315,13 @@ 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; - ExecutionRequests: electra.ExecutionRequests; + 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; DataColumnSidecars: gloas.DataColumnSidecars; 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..2fc9f40f0048 --- /dev/null +++ b/packages/types/test/unit/gloas/eip7688.test.ts @@ -0,0 +1,52 @@ +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.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); + }); + + it("keeps Transaction as a byte-list value while using progressive merkleization", () => { + expect(ssz.gloas.Transaction).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); + }); + + 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/utils/package.json b/packages/utils/package.json index 18d15078bc5b..fbe7561f1456 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -53,7 +53,7 @@ "js-yaml": "^4.1.0" }, "devDependencies": { - "@chainsafe/ssz": "^1.4.0", + "@chainsafe/ssz": "^1.6.0", "@types/js-yaml": "^4.0.5", "@types/yargs": "^17.0.24", "prom-client": "^15.1.0" diff --git a/packages/validator/package.json b/packages/validator/package.json index 8eac5dbbb2cc..cf7edf34d9a8 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.0", "@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 072ce8265be1..6797941aa5ac 100644 --- a/packages/validator/src/util/params.ts +++ b/packages/validator/src/util/params.ts @@ -327,6 +327,12 @@ function getSpecCriticalParams(localConfig: ChainConfig): Record Date: Thu, 21 May 2026 12:10:40 -0400 Subject: [PATCH 2/3] chore: fix unit tests --- .../upgradeLightClientHeader.test.ts | 54 +++++++++++++++++-- 1 file changed, 50 insertions(+), 4 deletions(-) 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 + ), + }; +} From fc5ec13f41f1905fb9e27659c561a697754fefe7 Mon Sep 17 00:00:00 2001 From: Cayman Date: Fri, 22 May 2026 11:06:19 -0400 Subject: [PATCH 3/3] chore: BlockAccessList to ProgressiveByteList --- packages/types/src/gloas/sszTypes.ts | 4 +--- packages/types/test/unit/gloas/eip7688.test.ts | 7 ++++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/types/src/gloas/sszTypes.ts b/packages/types/src/gloas/sszTypes.ts index d8c47a022530..a8d0be188859 100644 --- a/packages/types/src/gloas/sszTypes.ts +++ b/packages/types/src/gloas/sszTypes.ts @@ -1,6 +1,5 @@ import { BitVectorType, - ByteListType, ContainerType, ListBasicType, ListCompositeType, @@ -18,7 +17,6 @@ import { EXECUTION_BLOCK_HASH_DEPTH_GLOAS, FINALIZED_ROOT_DEPTH_GLOAS, HISTORICAL_ROOTS_LIMIT, - MAX_BYTES_PER_TRANSACTION, MIN_SEED_LOOKAHEAD, NEXT_SYNC_COMMITTEE_DEPTH_GLOAS, NUMBER_OF_COLUMNS, @@ -314,7 +312,7 @@ 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 ProgressiveContainerType( { diff --git a/packages/types/test/unit/gloas/eip7688.test.ts b/packages/types/test/unit/gloas/eip7688.test.ts index 2fc9f40f0048..21fd0f8649d9 100644 --- a/packages/types/test/unit/gloas/eip7688.test.ts +++ b/packages/types/test/unit/gloas/eip7688.test.ts @@ -23,12 +23,17 @@ describe("Gloas EIP-7688 SSZ types", () => { expect(ssz.gloas.DataColumn).toBeInstanceOf(ProgressiveListCompositeType); }); - it("keeps Transaction as a byte-list value while using progressive merkleization", () => { + 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", () => {