Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:^",
Expand Down
4 changes: 2 additions & 2 deletions packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`
Expand Down Expand Up @@ -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)}`
Expand Down
5 changes: 2 additions & 3 deletions packages/beacon-node/src/chain/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
ValidatorIndex,
Wei,
deneb,
electra,
gloas,
isBlindedBeaconBlock,
phase0,
Expand Down Expand Up @@ -916,10 +915,10 @@ export class BeaconChain implements IBeaconChain {
async getParentExecutionRequests(
parentBlockSlot: Slot,
parentBlockRootHex: RootHex
): Promise<electra.ExecutionRequests> {
): Promise<gloas.ExecutionRequests> {
// 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) {
Expand Down
3 changes: 1 addition & 2 deletions packages/beacon-node/src/chain/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
altair,
capella,
deneb,
electra,
gloas,
phase0,
rewards,
Expand Down Expand Up @@ -236,7 +235,7 @@ export interface IBeaconChain {
blockSlot: Slot,
blockRootHex: string
): Promise<gloas.SignedExecutionPayloadEnvelope | null>;
getParentExecutionRequests(parentBlockSlot: Slot, parentBlockRootHex: RootHex): Promise<electra.ExecutionRequests>;
getParentExecutionRequests(parentBlockSlot: Slot, parentBlockRootHex: RootHex): Promise<gloas.ExecutionRequests>;

produceCommonBlockBody(blockAttributes: BlockAttributes): Promise<CommonBlockBody>;
produceBlock(blockAttributes: BlockAttributes & {commonBlockBodyPromise: Promise<CommonBlockBody>}): Promise<{
Expand Down
19 changes: 12 additions & 7 deletions packages/beacon-node/src/chain/lightClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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(),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The finalizedHeader is hardcoded to ssz.electra.LightClientHeader.defaultValue(). While the comment mentions that post-Gloas updates are skipped for now, this PR introduces Gloas light-client header support and upgrades. It would be more consistent and forward-compatible to use the highest fork's default value or explicitly handle Gloas if the server is intended to support it eventually.

Suggested change
finalizedHeader: ssz.electra.LightClientHeader.defaultValue(),
finalizedHeader: ssz.gloas.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(),
Expand Down Expand Up @@ -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}`);
}
Comment on lines +659 to 671
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for validating numWitness and sync committee branches can be simplified using else if blocks to avoid redundant fork checks. Additionally, for Gloas, since explicit branches are used, numWitness (the length of the legacy witness array) should be verified to be 0 to ensure consistency with the generator logic in proofs.ts.

    if (isForkPostGloas(attestedFork)) {
      if (
        syncCommitteeWitness.currentSyncCommitteeBranch === undefined ||
        syncCommitteeWitness.nextSyncCommitteeBranch === undefined
      ) {
        throw Error("Expected post-Gloas sync committee branches");
      }
      if (numWitness !== 0) {
        throw Error(`Expected 0 witnesses in post-Gloas numWitness=${numWitness}`);
      }
    } else if (isForkPostElectra(attestedFork)) {
      if (numWitness !== NUM_WITNESS_ELECTRA) {
        throw Error(`Expected ${NUM_WITNESS_ELECTRA} witnesses in post-Electra numWitness=${numWitness}`);
      }
    } else if (numWitness !== NUM_WITNESS) {
      throw Error(`Expected ${NUM_WITNESS} witnesses in pre-Electra numWitness=${numWitness}`);
    }


Expand Down
37 changes: 36 additions & 1 deletion packages/beacon-node/src/chain/lightClient/proofs.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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;
Expand Down Expand Up @@ -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));
}

Expand Down
4 changes: 3 additions & 1 deletion packages/beacon-node/src/chain/lightClient/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
};
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export type ProduceFullGloas = {
type: BlockType.Full;
fork: ForkPostGloas;
executionPayload: ExecutionPayload<ForkPostGloas>;
executionRequests: electra.ExecutionRequests;
executionRequests: gloas.ExecutionRequests;
blobsBundle: BlobsBundle<ForkPostGloas>;
cells: fulu.Cell[][];
parentBlockRoot: Root;
Expand Down Expand Up @@ -272,7 +272,7 @@ export async function produceBlockBody<T extends BlockType>(

// 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;
// Spec: should_build_on_full(store, head). `parentBlock` is the proposer's head
Expand All @@ -285,7 +285,7 @@ export async function produceBlockBody<T extends BlockType>(
stateAfterParentPayload = currentState.withParentPayloadApplied(parentExecutionRequests);
} else {
parentBlockHash = currentState.latestExecutionPayloadBid.parentBlockHash;
parentExecutionRequests = ssz.electra.ExecutionRequests.defaultValue();
parentExecutionRequests = ssz.gloas.ExecutionRequests.defaultValue();
}
const prepareRes = await prepareExecutionPayload(
this,
Expand Down Expand Up @@ -350,7 +350,7 @@ export async function produceBlockBody<T extends BlockType>(
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -35,6 +37,35 @@ export class SyncCommitteeWitnessRepository extends Repository<Uint8Array, SyncC
// Overrides for multi-fork
encodeValue(value: SyncCommitteeWitness): Uint8Array {
const numWitness = value.witness.length;
const hasGloasBranches =
value.currentSyncCommitteeBranch !== undefined || value.nextSyncCommitteeBranch !== undefined;

if (hasGloasBranches) {
if (
value.currentSyncCommitteeBranch?.length !== CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS ||
value.nextSyncCommitteeBranch?.length !== NEXT_SYNC_COMMITTEE_DEPTH_GLOAS
) {
throw Error(
`Invalid post-Gloas sync committee branch lengths current=${value.currentSyncCommitteeBranch?.length} next=${value.nextSyncCommitteeBranch?.length}`
);
}

const type = new ContainerType({
currentSyncCommitteeBranch: new VectorCompositeType(ssz.Root, CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS),
nextSyncCommitteeBranch: new VectorCompositeType(ssz.Root, NEXT_SYNC_COMMITTEE_DEPTH_GLOAS),
currentSyncCommitteeRoot: ssz.Root,
nextSyncCommitteeRoot: ssz.Root,
});

const valueBytes = type.serialize({
currentSyncCommitteeBranch: value.currentSyncCommitteeBranch,
nextSyncCommitteeBranch: value.nextSyncCommitteeBranch,
currentSyncCommitteeRoot: value.currentSyncCommitteeRoot,
nextSyncCommitteeRoot: value.nextSyncCommitteeRoot,
});

return prefixData(PrefixByte.POST_GLOAS, valueBytes);
}

if (numWitness !== NUM_WITNESS && numWitness !== NUM_WITNESS_ELECTRA) {
throw Error(`Number of witness can only be 4 pre-electra or 5 post-electra numWitness=${numWitness}`);
Expand All @@ -51,19 +82,25 @@ export class SyncCommitteeWitnessRepository extends Repository<Uint8Array, SyncC
// We need to differentiate between post-electra and pre-electra witness
// such that we can deserialize correctly
const isPostElectra = numWitness === NUM_WITNESS_ELECTRA;
const prefixByte = new Uint8Array(1);
prefixByte[0] = isPostElectra ? PrefixByte.POST_ELECTRA : PrefixByte.PRE_ELECTRA;

const prefixedData = new Uint8Array(1 + valueBytes.length);
prefixedData.set(prefixByte, 0);
prefixedData.set(valueBytes, 1);

return prefixedData;
return prefixData(isPostElectra ? PrefixByte.POST_ELECTRA : PrefixByte.PRE_ELECTRA, valueBytes);
}

decodeValue(data: Uint8Array): SyncCommitteeWitness {
// First byte is written
const prefix = data.subarray(0, 1);
const isPostGloas = prefix[0] === PrefixByte.POST_GLOAS;

if (isPostGloas) {
const type = new ContainerType({
currentSyncCommitteeBranch: new VectorCompositeType(ssz.Root, CURRENT_SYNC_COMMITTEE_DEPTH_GLOAS),
nextSyncCommitteeBranch: new VectorCompositeType(ssz.Root, NEXT_SYNC_COMMITTEE_DEPTH_GLOAS),
currentSyncCommitteeRoot: ssz.Root,
nextSyncCommitteeRoot: ssz.Root,
});

return {witness: [], ...type.deserialize(data.subarray(1))};
}

const isPostElectra = prefix[0] === PrefixByte.POST_ELECTRA;

const type = new ContainerType({
Expand All @@ -75,3 +112,14 @@ export class SyncCommitteeWitnessRepository extends Repository<Uint8Array, SyncC
return type.deserialize(data.subarray(1));
}
}

function prefixData(prefix: PrefixByte, valueBytes: Uint8Array): Uint8Array {
const prefixByte = new Uint8Array(1);
prefixByte[0] = prefix;

const prefixedData = new Uint8Array(1 + valueBytes.length);
prefixedData.set(prefixByte, 0);
prefixedData.set(valueBytes, 1);

return prefixedData;
}
Loading
Loading