Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add devnet-5 support #7246

Merged
merged 27 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
39a579e
feat: implement SingleAttestation (#7126)
twoeths Nov 26, 2024
3cd80bb
fix: select correct gossip type when publishing single attestation (#…
nflaig Nov 28, 2024
56d083a
fix: get attester index from single attestation bytes if cache is use…
nflaig Nov 29, 2024
b12b205
fix: correctly get signature from single attestation bytes (#7266)
nflaig Nov 29, 2024
e82a3f6
fix: remove aggregation bits from seen attestation cache (#7265)
nflaig Nov 29, 2024
70cb9d0
fix: return correct type from attestation validation when using cache…
nflaig Nov 30, 2024
e7c00a8
Bump spec test version
ensi321 Dec 3, 2024
f533089
feat: implement partial spec changes for devnet-5 (#7229)
ensi321 Dec 4, 2024
76ee6b3
feat: exclude empty requests in execution requests list (#7196)
ensi321 Dec 4, 2024
031214e
feat: implement partial spec changes for v1.5.0-alpha.10 (#7288)
ensi321 Dec 14, 2024
cd7b1b4
feat: use 16-bit random value to compute validator indices (#7311)
nflaig Dec 18, 2024
06ec5f6
Merge branch 'unstable' into devnet-5
nflaig Dec 19, 2024
71e73b2
Bump CL spec version to alpha.10
nflaig Dec 19, 2024
4b8de08
fix: parsing ExecutionRequests from EL (#7314)
ensi321 Dec 19, 2024
265579f
test: ensure execution requests are de-/serialized according to EIP-7…
nflaig Jan 6, 2025
42d6982
feat: implement EIP-7691 increase blob throughput (#7309)
ensi321 Jan 9, 2025
3dc5e8d
Resolve merge conflicts with unstable branch
nflaig Jan 9, 2025
b33122f
fix: use Bigint for deposit index to pass spec tests (#7344)
nflaig Jan 9, 2025
793ad49
chore: skip fulu fork spec tests (#7343)
nflaig Jan 9, 2025
cf64359
Bump CL spec version to beta.0
nflaig Jan 10, 2025
4d271ff
Merge branch 'unstable' into devnet-5
nflaig Jan 10, 2025
8e423e7
fix: revert BlobSidecarsByRoot/Range version bump (#7347)
nflaig Jan 10, 2025
3ea46e2
chore: skip light client data_collection spec tests (#7349)
nflaig Jan 13, 2025
0a93016
chore: update lighthouse to latest unstable version in sim tests (#7364)
nflaig Jan 15, 2025
38d40e8
chore: review devnet-5 branch (#7365)
nflaig Jan 15, 2025
717f02a
Merge branch 'unstable' into devnet-5
nflaig Jan 17, 2025
eb2ee60
fix: use correct fork constants to limit max request blocks/blobs cou…
nflaig Jan 17, 2025
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
2 changes: 1 addition & 1 deletion .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
GETH_DOCKER_IMAGE=ethereum/client-go:v1.13.14
# Use either image or local binary for the testing
GETH_BINARY_DIR=
LIGHTHOUSE_DOCKER_IMAGE=sigp/lighthouse:v5.1.1-amd64-modern-dev
LIGHTHOUSE_DOCKER_IMAGE=sigp/lighthouse:latest-amd64-unstable

# We can't upgrade nethermind further due to genesis hash mismatch with the geth
# https://github.com/NethermindEth/nethermind/issues/6683
Expand Down
43 changes: 27 additions & 16 deletions packages/api/src/beacon/routes/beacon/pool.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import {ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {isForkPostElectra} from "@lodestar/params";
import {AttesterSlashing, CommitteeIndex, Slot, capella, electra, phase0, ssz} from "@lodestar/types";
import {ForkPostElectra, ForkPreElectra, isForkPostElectra} from "@lodestar/params";
import {
AttesterSlashing,
CommitteeIndex,
SingleAttestation,
Slot,
capella,
electra,
phase0,
ssz,
} from "@lodestar/types";
import {
ArrayOf,
EmptyArgs,
Expand All @@ -20,6 +29,8 @@ import {MetaHeader, VersionCodec, VersionMeta} from "../../../utils/metadata.js"

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes

const SingleAttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation);
const SingleAttestationListTypeElectra = ArrayOf(ssz.electra.SingleAttestation);
const AttestationListTypePhase0 = ArrayOf(ssz.phase0.Attestation);
const AttestationListTypeElectra = ArrayOf(ssz.electra.Attestation);
const AttesterSlashingListTypePhase0 = ArrayOf(ssz.phase0.AttesterSlashing);
Expand Down Expand Up @@ -142,7 +153,7 @@ export type Endpoints = {
*/
submitPoolAttestations: Endpoint<
"POST",
{signedAttestations: AttestationListPhase0},
{signedAttestations: SingleAttestation<ForkPreElectra>[]},
{body: unknown},
EmptyResponseData,
EmptyMeta
Expand All @@ -158,7 +169,7 @@ export type Endpoints = {
*/
submitPoolAttestationsV2: Endpoint<
"POST",
{signedAttestations: AttestationList},
{signedAttestations: SingleAttestation[]},
{body: unknown; headers: {[MetaHeader.Version]: string}},
EmptyResponseData,
EmptyMeta
Expand Down Expand Up @@ -316,10 +327,10 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
url: "/eth/v1/beacon/pool/attestations",
method: "POST",
req: {
writeReqJson: ({signedAttestations}) => ({body: AttestationListTypePhase0.toJson(signedAttestations)}),
parseReqJson: ({body}) => ({signedAttestations: AttestationListTypePhase0.fromJson(body)}),
writeReqSsz: ({signedAttestations}) => ({body: AttestationListTypePhase0.serialize(signedAttestations)}),
parseReqSsz: ({body}) => ({signedAttestations: AttestationListTypePhase0.deserialize(body)}),
writeReqJson: ({signedAttestations}) => ({body: SingleAttestationListTypePhase0.toJson(signedAttestations)}),
parseReqJson: ({body}) => ({signedAttestations: SingleAttestationListTypePhase0.fromJson(body)}),
writeReqSsz: ({signedAttestations}) => ({body: SingleAttestationListTypePhase0.serialize(signedAttestations)}),
parseReqSsz: ({body}) => ({signedAttestations: SingleAttestationListTypePhase0.deserialize(body)}),
schema: {
body: Schema.ObjectArray,
},
Expand All @@ -334,34 +345,34 @@ export function getDefinitions(config: ChainForkConfig): RouteDefinitions<Endpoi
const fork = config.getForkName(signedAttestations[0]?.data.slot ?? 0);
return {
body: isForkPostElectra(fork)
? AttestationListTypeElectra.toJson(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.toJson(signedAttestations as AttestationListPhase0),
? SingleAttestationListTypeElectra.toJson(signedAttestations as SingleAttestation<ForkPostElectra>[])
: SingleAttestationListTypePhase0.toJson(signedAttestations as SingleAttestation<ForkPreElectra>[]),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqJson: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedAttestations: isForkPostElectra(fork)
? AttestationListTypeElectra.fromJson(body)
: AttestationListTypePhase0.fromJson(body),
? SingleAttestationListTypeElectra.fromJson(body)
: SingleAttestationListTypePhase0.fromJson(body),
};
},
writeReqSsz: ({signedAttestations}) => {
const fork = config.getForkName(signedAttestations[0]?.data.slot ?? 0);
return {
body: isForkPostElectra(fork)
? AttestationListTypeElectra.serialize(signedAttestations as AttestationListElectra)
: AttestationListTypePhase0.serialize(signedAttestations as AttestationListPhase0),
? SingleAttestationListTypeElectra.serialize(signedAttestations as SingleAttestation<ForkPostElectra>[])
: SingleAttestationListTypePhase0.serialize(signedAttestations as SingleAttestation<ForkPreElectra>[]),
headers: {[MetaHeader.Version]: fork},
};
},
parseReqSsz: ({body, headers}) => {
const fork = toForkName(fromHeaders(headers, MetaHeader.Version));
return {
signedAttestations: isForkPostElectra(fork)
? AttestationListTypeElectra.deserialize(body)
: AttestationListTypePhase0.deserialize(body),
? SingleAttestationListTypeElectra.deserialize(body)
: SingleAttestationListTypePhase0.deserialize(body),
};
},
schema: {
Expand Down
6 changes: 6 additions & 0 deletions packages/api/src/beacon/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UintNum64,
altair,
capella,
electra,
phase0,
ssz,
sszTypesFor,
Expand Down Expand Up @@ -51,6 +52,8 @@ export enum EventType {
block = "block",
/** The node has received a valid attestation (from P2P or API) */
attestation = "attestation",
/** The node has received a valid SingleAttestation (from P2P or API) */
singleAttestation = "single_attestation",
/** The node has received a valid voluntary exit (from P2P or API) */
voluntaryExit = "voluntary_exit",
/** The node has received a valid proposer slashing (from P2P or API) */
Expand Down Expand Up @@ -79,6 +82,7 @@ export const eventTypes: {[K in EventType]: K} = {
[EventType.head]: EventType.head,
[EventType.block]: EventType.block,
[EventType.attestation]: EventType.attestation,
[EventType.singleAttestation]: EventType.singleAttestation,
[EventType.voluntaryExit]: EventType.voluntaryExit,
[EventType.proposerSlashing]: EventType.proposerSlashing,
[EventType.attesterSlashing]: EventType.attesterSlashing,
Expand Down Expand Up @@ -108,6 +112,7 @@ export type EventData = {
executionOptimistic: boolean;
};
[EventType.attestation]: Attestation;
[EventType.singleAttestation]: electra.SingleAttestation;
[EventType.voluntaryExit]: phase0.SignedVoluntaryExit;
[EventType.proposerSlashing]: phase0.ProposerSlashing;
[EventType.attesterSlashing]: AttesterSlashing;
Expand Down Expand Up @@ -237,6 +242,7 @@ export function getTypeByEvent(config: ChainForkConfig): {[K in EventType]: Type
return sszTypesFor(fork).Attestation.fromJson(attestation);
},
},
[EventType.singleAttestation]: ssz.electra.SingleAttestation,
[EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit,
[EventType.proposerSlashing]: ssz.phase0.ProposerSlashing,
[EventType.attesterSlashing]: {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/test/unit/beacon/oapiSpec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {testData as validatorTestData} from "./testData/validator.js";
// Solutions: https://stackoverflow.com/questions/46745014/alternative-for-dirname-in-node-js-when-using-es6-modules
const __dirname = path.dirname(fileURLToPath(import.meta.url));

const version = "v3.0.0-alpha.6";
const version = "v3.0.0-alpha.9";
const openApiFile: OpenApiFile = {
url: `https://github.com/ethereum/beacon-APIs/releases/download/${version}/beacon-node-oapi.json`,
filepath: path.join(__dirname, "../../../oapi-schemas/beacon-node-oapi.json"),
Expand Down
13 changes: 13 additions & 0 deletions packages/api/test/unit/beacon/testData/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,19 @@ export const eventTestData: EventData = {
target: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},
},
}),
[EventType.singleAttestation]: ssz.electra.SingleAttestation.fromJson({
committee_index: "1",
attester_index: "1",
data: {
slot: "1",
index: "1",
beacon_block_root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2",
source: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},
target: {epoch: "1", root: "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2"},
},
signature:
"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505",
}),
[EventType.voluntaryExit]: ssz.phase0.SignedVoluntaryExit.fromJson({
message: {epoch: "1", validator_index: "1"},
signature:
Expand Down
45 changes: 33 additions & 12 deletions packages/beacon-node/src/api/impl/beacon/pool/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import {routes} from "@lodestar/api";
import {ApplicationMethods} from "@lodestar/api/server";
import {ForkName, SYNC_COMMITTEE_SUBNET_SIZE, isForkPostElectra} from "@lodestar/params";
import {Attestation, Epoch, isElectraAttestation, ssz} from "@lodestar/types";
import {
ForkName,
ForkPostElectra,
ForkPreElectra,
SYNC_COMMITTEE_SUBNET_SIZE,
isForkPostElectra,
} from "@lodestar/params";
import {Attestation, Epoch, SingleAttestation, isElectraAttestation, ssz} from "@lodestar/types";
import {
AttestationError,
AttestationErrorCode,
Expand All @@ -10,7 +16,7 @@ import {
} from "../../../../chain/errors/index.js";
import {validateApiAttesterSlashing} from "../../../../chain/validation/attesterSlashing.js";
import {validateApiBlsToExecutionChange} from "../../../../chain/validation/blsToExecutionChange.js";
import {validateApiAttestation} from "../../../../chain/validation/index.js";
import {toElectraSingleAttestation, validateApiAttestation} from "../../../../chain/validation/index.js";
import {validateApiProposerSlashing} from "../../../../chain/validation/proposerSlashing.js";
import {validateApiSyncCommittee} from "../../../../chain/validation/syncCommittee.js";
import {validateApiVoluntaryExit} from "../../../../chain/validation/voluntaryExit.js";
Expand Down Expand Up @@ -99,20 +105,35 @@ export function getBeaconPoolApi({
// when a validator is configured with multiple beacon node urls, this attestation data may come from another beacon node
// and the block hasn't been in our forkchoice since we haven't seen / processing that block
// see https://github.com/ChainSafe/lodestar/issues/5098
const {indexedAttestation, subnet, attDataRootHex, committeeIndex} = await validateGossipFnRetryUnknownRoot(
validateFn,
network,
chain,
slot,
beaconBlockRoot
);
const {indexedAttestation, subnet, attDataRootHex, committeeIndex, committeeValidatorIndex, committeeSize} =
await validateGossipFnRetryUnknownRoot(validateFn, network, chain, slot, beaconBlockRoot);

if (network.shouldAggregate(subnet, slot)) {
const insertOutcome = chain.attestationPool.add(committeeIndex, attestation, attDataRootHex);
const insertOutcome = chain.attestationPool.add(
committeeIndex,
attestation,
attDataRootHex,
committeeValidatorIndex,
committeeSize
);
metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome});
}

chain.emitter.emit(routes.events.EventType.attestation, attestation);
if (isForkPostElectra(fork)) {
chain.emitter.emit(
routes.events.EventType.singleAttestation,
attestation as SingleAttestation<ForkPostElectra>
);
} else {
chain.emitter.emit(routes.events.EventType.attestation, attestation as SingleAttestation<ForkPreElectra>);
chain.emitter.emit(
routes.events.EventType.singleAttestation,
toElectraSingleAttestation(
attestation as SingleAttestation<ForkPreElectra>,
indexedAttestation.attestingIndices[0]
)
);
}

const sentPeers = await network.publishBeaconAttestation(attestation, subnet);
metrics?.onPoolSubmitUnaggregatedAttestation(seenTimestampSec, indexedAttestation, subnet, sentPeers);
Expand Down
6 changes: 6 additions & 0 deletions packages/beacon-node/src/api/impl/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import {
BLOB_TX_TYPE,
BLS_WITHDRAWAL_PREFIX,
COMPOUNDING_WITHDRAWAL_PREFIX,
CONSOLIDATION_REQUEST_TYPE,
DEPOSIT_CONTRACT_TREE_DEPTH,
DEPOSIT_REQUEST_TYPE,
DOMAIN_AGGREGATE_AND_PROOF,
DOMAIN_APPLICATION_BUILDER,
DOMAIN_APPLICATION_MASK,
Expand Down Expand Up @@ -40,6 +42,7 @@ import {
UNSET_DEPOSIT_REQUESTS_START_INDEX,
VERSIONED_HASH_VERSION_KZG,
WEIGHT_DENOMINATOR,
WITHDRAWAL_REQUEST_TYPE,
} from "@lodestar/params";

/**
Expand Down Expand Up @@ -108,4 +111,7 @@ export const specConstants = {
// electra
UNSET_DEPOSIT_REQUESTS_START_INDEX,
FULL_EXIT_REQUEST_AMOUNT,
DEPOSIT_REQUEST_TYPE,
WITHDRAWAL_REQUEST_TYPE,
CONSOLIDATION_REQUEST_TYPE,
};
7 changes: 6 additions & 1 deletion packages/beacon-node/src/chain/errors/attestationError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ export enum AttestationErrorCode {
* Electra: Invalid attestationData index: is non-zero
*/
NON_ZERO_ATTESTATION_DATA_INDEX = "ATTESTATION_ERROR_NON_ZERO_ATTESTATION_DATA_INDEX",
/**
* Electra: Attester not in committee
*/
ATTESTER_NOT_IN_COMMITTEE = "ATTESTATION_ERROR_ATTESTER_NOT_IN_COMMITTEE",
}

export type AttestationErrorType =
Expand Down Expand Up @@ -170,7 +174,8 @@ export type AttestationErrorType =
| {code: AttestationErrorCode.INVALID_SERIALIZED_BYTES}
| {code: AttestationErrorCode.TOO_MANY_SKIPPED_SLOTS; headBlockSlot: Slot; attestationSlot: Slot}
| {code: AttestationErrorCode.NOT_EXACTLY_ONE_COMMITTEE_BIT_SET}
| {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX};
| {code: AttestationErrorCode.NON_ZERO_ATTESTATION_DATA_INDEX}
| {code: AttestationErrorCode.ATTESTER_NOT_IN_COMMITTEE};

export class AttestationError extends GossipActionError<AttestationErrorType> {
getMetadata(): Record<string, string | number | null> {
Expand Down
47 changes: 33 additions & 14 deletions packages/beacon-node/src/chain/opPools/attestationPool.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Signature, aggregateSignatures} from "@chainsafe/blst";
import {BitArray} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {isForkPostElectra} from "@lodestar/params";
import {Attestation, RootHex, Slot, isElectraAttestation} from "@lodestar/types";
import {MAX_COMMITTEES_PER_SLOT, isForkPostElectra} from "@lodestar/params";
import {Attestation, RootHex, SingleAttestation, Slot, isElectraSingleAttestation} from "@lodestar/types";
import {assert, MapDef} from "@lodestar/utils";
import {IClock} from "../../util/clock.js";
import {InsertOutcome, OpPoolError, OpPoolErrorCode} from "./types.js";
Expand Down Expand Up @@ -105,7 +105,13 @@ export class AttestationPool {
* - Valid committeeIndex
* - Valid data
*/
add(committeeIndex: CommitteeIndex, attestation: Attestation, attDataRootHex: RootHex): InsertOutcome {
add(
committeeIndex: CommitteeIndex,
attestation: SingleAttestation,
attDataRootHex: RootHex,
committeeValidatorIndex: number,
committeeSize: number
): InsertOutcome {
const slot = attestation.data.slot;
const fork = this.config.getForkName(slot);
const lowestPermissibleSlot = this.lowestPermissibleSlot;
Expand All @@ -129,9 +135,9 @@ export class AttestationPool {
if (isForkPostElectra(fork)) {
// Electra only: this should not happen because attestation should be validated before reaching this
assert.notNull(committeeIndex, "Committee index should not be null in attestation pool post-electra");
assert.true(isElectraAttestation(attestation), "Attestation should be type electra.Attestation");
assert.true(isElectraSingleAttestation(attestation), "Attestation should be type electra.SingleAttestation");
} else {
assert.true(!isElectraAttestation(attestation), "Attestation should be type phase0.Attestation");
assert.true(!isElectraSingleAttestation(attestation), "Attestation should be type phase0.Attestation");
committeeIndex = null; // For pre-electra, committee index info is encoded in attDataRootIndex
}

Expand All @@ -144,10 +150,10 @@ export class AttestationPool {
const aggregate = aggregateByIndex.get(committeeIndex);
if (aggregate) {
// Aggregate mutating
return aggregateAttestationInto(aggregate, attestation);
return aggregateAttestationInto(aggregate, attestation, committeeValidatorIndex);
}
// Create new aggregate
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation));
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, committeeValidatorIndex, committeeSize));
return InsertOutcome.NewData;
}

Expand Down Expand Up @@ -216,8 +222,18 @@ export class AttestationPool {
/**
* Aggregate a new attestation into `aggregate` mutating it
*/
function aggregateAttestationInto(aggregate: AggregateFast, attestation: Attestation): InsertOutcome {
const bitIndex = attestation.aggregationBits.getSingleTrueBit();
function aggregateAttestationInto(
aggregate: AggregateFast,
attestation: SingleAttestation,
committeeValidatorIndex: number
): InsertOutcome {
let bitIndex: number | null;

if (isElectraSingleAttestation(attestation)) {
bitIndex = committeeValidatorIndex;
} else {
bitIndex = attestation.aggregationBits.getSingleTrueBit();
}

// Should never happen, attestations are verified against this exact condition before
assert.notNull(bitIndex, "Invalid attestation in pool, not exactly one bit set");
Expand All @@ -234,13 +250,16 @@ function aggregateAttestationInto(aggregate: AggregateFast, attestation: Attesta
/**
* Format `contribution` into an efficient `aggregate` to add more contributions in with aggregateContributionInto()
*/
function attestationToAggregate(attestation: Attestation): AggregateFast {
if (isElectraAttestation(attestation)) {
function attestationToAggregate(
attestation: SingleAttestation,
committeeValidatorIndex: number,
committeeSize: number
): AggregateFast {
if (isElectraSingleAttestation(attestation)) {
return {
data: attestation.data,
// clone because it will be mutated
aggregationBits: attestation.aggregationBits.clone(),
committeeBits: attestation.committeeBits,
aggregationBits: BitArray.fromSingleBit(committeeSize, committeeValidatorIndex),
committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, attestation.committeeIndex),
signature: signatureFromBytesNoCheck(attestation.signature),
};
}
Expand Down
Loading
Loading