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: implement SingleAttestation #7126

Merged
merged 36 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9017335
feat: refactor SeenAttestationDatas for SinlgeAttestation
twoeths Oct 4, 2024
13aeaa5
feat: add SingleAttestation type
twoeths Oct 4, 2024
982339a
feat: ssz utils for SingleAttestation
twoeths Oct 4, 2024
73faacd
feat: implement SingleAttestation for network processor and gossip queue
twoeths Oct 4, 2024
0852e32
fix: add SingleAttestation for phase0 and altair
twoeths Oct 5, 2024
af6f7e1
fix: define and publish SingleAttestation for all forks
twoeths Oct 5, 2024
1d3c82b
Fix electra SingleAttestation type mapping
nflaig Oct 5, 2024
5c00760
Update api and eventstream
nflaig Oct 5, 2024
3c4fbf9
Update validator client
nflaig Oct 5, 2024
af5a8d6
Update attestation unit test variables
nflaig Oct 5, 2024
695db4e
chore: SeenAttestationDatas unit tests
twoeths Oct 7, 2024
687c960
chore: sszBytes unit tests
twoeths Oct 7, 2024
79b6667
Use CommitteeIndex type
ensi321 Oct 7, 2024
0759d23
refactor: get/set functions of SeenAttestationDatas
twoeths Oct 7, 2024
d43a035
Always emit single_attestation event
nflaig Oct 7, 2024
6df7e5e
Validation use new SeenAttDataKey
ensi321 Oct 8, 2024
c886105
Merge branch 'unstable' into ls/single_attestation
ensi321 Oct 8, 2024
eb72e07
validateAttestationNoSignatureCheck first draft
ensi321 Oct 8, 2024
bc05339
Add aggregation and committee bits to cache
ensi321 Nov 1, 2024
2d8cdc1
AttestationPool accepts SingleAttestation
ensi321 Nov 1, 2024
64077a9
Update SingleAttestation event stream
ensi321 Nov 1, 2024
48c8c3e
Update aggregate validation
ensi321 Nov 1, 2024
1fb1aaa
Polish
ensi321 Nov 1, 2024
25ce2cc
Merge branch 'unstable' into ls/single_attestation
ensi321 Nov 1, 2024
9ad1e26
Merge branch 'unstable' into ls/single_attestation
ensi321 Nov 3, 2024
613cbef
Lint
ensi321 Nov 6, 2024
7c41902
fix check-types
ensi321 Nov 6, 2024
768dbbd
Merge branch 'unstable' into ls/single_attestation
nflaig Nov 8, 2024
6792566
Remove committee bit cache
ensi321 Nov 8, 2024
69e7783
Update attestation pool unit tests
nflaig Nov 20, 2024
d795f25
Lint
nflaig Nov 20, 2024
8acc69d
Merge branch 'unstable' into ls/single_attestation
nflaig Nov 20, 2024
5212974
Remove unused committeeBits from attestation data cache
nflaig Nov 20, 2024
f099369
Fix spec reference comment
nflaig Nov 20, 2024
8340137
fix: getSeenAttDataKeyFromSignedAggregateAndProof
twoeths Nov 20, 2024
3380696
Update beacon-api spec tests to run against v3.0.0-alpha.9
nflaig Nov 26, 2024
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
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
44 changes: 32 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,34 @@ 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, aggregationBits} =
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,
aggregationBits
);
metrics?.opPool.attestationPoolInsertOutcome.inc({insertOutcome});
}

chain.emitter.emit(routes.events.EventType.attestation, attestation);
if (isForkPostElectra(fork)) {
nflaig marked this conversation as resolved.
Show resolved Hide resolved
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
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
44 changes: 30 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,12 @@ export class AttestationPool {
* - Valid committeeIndex
* - Valid data
*/
add(committeeIndex: CommitteeIndex, attestation: Attestation, attDataRootHex: RootHex): InsertOutcome {
add(
committeeIndex: CommitteeIndex,
attestation: SingleAttestation,
attDataRootHex: RootHex,
aggregationBits: BitArray | null
): InsertOutcome {
const slot = attestation.data.slot;
const fork = this.config.getForkName(slot);
const lowestPermissibleSlot = this.lowestPermissibleSlot;
Expand All @@ -129,9 +134,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 +149,10 @@ export class AttestationPool {
const aggregate = aggregateByIndex.get(committeeIndex);
if (aggregate) {
// Aggregate mutating
return aggregateAttestationInto(aggregate, attestation);
return aggregateAttestationInto(aggregate, attestation, aggregationBits);
}
// Create new aggregate
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation));
aggregateByIndex.set(committeeIndex, attestationToAggregate(attestation, aggregationBits));
return InsertOutcome.NewData;
}

Expand Down Expand Up @@ -216,8 +221,19 @@ 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,
aggregationBits: BitArray | null
): InsertOutcome {
let bitIndex: number | null;

if (isElectraSingleAttestation(attestation)) {
assert.notNull(aggregationBits, "aggregationBits missing post-electra");
bitIndex = aggregationBits.getSingleTrueBit();
} 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,13 @@ 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, aggregationBits: BitArray | null): AggregateFast {
if (isElectraSingleAttestation(attestation)) {
assert.notNull(aggregationBits, "aggregationBits missing post-electra to generate aggregate");
return {
data: attestation.data,
// clone because it will be mutated
aggregationBits: attestation.aggregationBits.clone(),
committeeBits: attestation.committeeBits,
aggregationBits,
committeeBits: BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, attestation.committeeIndex),
signature: signatureFromBytesNoCheck(attestation.signature),
};
}
Expand Down
Loading
Loading