Skip to content

Commit

Permalink
fix: getSeenAttDataKey apis (#7009)
Browse files Browse the repository at this point in the history
* fix: getSeenAttDataKey apis

* chore: use ForkName instead of ForkSeq
  • Loading branch information
twoeths authored Aug 9, 2024
1 parent 40ec382 commit f0e83b6
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 106 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import {BitArray} from "@chainsafe/ssz";
import {CommitteeIndex, phase0, RootHex, Slot} from "@lodestar/types";
import {MapDef} from "@lodestar/utils";
import {Metrics} from "../../metrics/metrics.js";
import {SeenAttDataKey} from "../../util/sszBytes.js";
import {InsertOutcome} from "../opPools/types.js";

export type SeenAttDataKey = AttDataBase64 | AttDataCommitteeBitsBase64;
// pre-electra, AttestationData is used to cache attestations
type AttDataBase64 = string;
// electra, AttestationData + CommitteeBits are used to cache attestations
type AttDataCommitteeBitsBase64 = string;

export type AttestationDataCacheEntry = {
// part of shuffling data, so this does not take memory
committeeValidatorIndices: Uint32Array;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import {
import {IBeaconChain} from "..";
import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js";
import {RegenCaller} from "../regen/index.js";
import {getSeenAttDataKeyFromSignedAggregateAndProof} from "../../util/sszBytes.js";
import {getSelectionProofSignatureSet, getAggregateAndProofSignatureSet} from "./signatureSets/index.js";
import {
getAttestationDataSigningRoot,
getCommitteeIndices,
getSeenAttDataKeyFromSignedAggregateAndProof,
getShufflingForAttestationVerification,
verifyHeadBlockAndTargetRoot,
verifyPropagationSlotRange,
Expand Down Expand Up @@ -71,9 +71,7 @@ async function validateAggregateAndProof(
const attData = aggregate.data;
const attSlot = attData.slot;

const seenAttDataKey = serializedData
? getSeenAttDataKeyFromSignedAggregateAndProof(ForkSeq[fork], serializedData)
: null;
const seenAttDataKey = serializedData ? getSeenAttDataKeyFromSignedAggregateAndProof(fork, serializedData) : null;
const cachedAttData = seenAttDataKey ? chain.seenAttestationDatas.get(attSlot, seenAttDataKey) : null;

let attIndex;
Expand Down
68 changes: 58 additions & 10 deletions packages/beacon-node/src/chain/validation/attestation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@ import {
IndexedAttestation,
} from "@lodestar/types";
import {ProtoBlock} from "@lodestar/fork-choice";
import {ATTESTATION_SUBNET_COUNT, SLOTS_PER_EPOCH, ForkName, ForkSeq, DOMAIN_BEACON_ATTESTER} from "@lodestar/params";
import {
ATTESTATION_SUBNET_COUNT,
SLOTS_PER_EPOCH,
ForkName,
ForkSeq,
DOMAIN_BEACON_ATTESTER,
isForkPostElectra,
} from "@lodestar/params";
import {
computeEpochAtSlot,
createSingleSignatureSetFromComponents,
Expand All @@ -29,12 +36,15 @@ import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/in
import {MAXIMUM_GOSSIP_CLOCK_DISPARITY_SEC} from "../../constants/index.js";
import {RegenCaller} from "../regen/index.js";
import {
SeenAttDataKey,
getAggregationBitsFromAttestationSerialized,
getSeenAttDataKey,
getAttDataFromAttestationSerialized,
getAttDataFromSignedAggregateAndProofElectra,
getCommitteeBitsFromAttestationSerialized,
getCommitteeBitsFromSignedAggregateAndProofElectra,
getAttDataFromSignedAggregateAndProofPhase0,
getSignatureFromAttestationSerialized,
} from "../../util/sszBytes.js";
import {AttestationDataCacheEntry} from "../seenCache/seenAttestationData.js";
import {AttestationDataCacheEntry, SeenAttDataKey} from "../seenCache/seenAttestationData.js";
import {sszDeserializeAttestation} from "../../network/gossip/topic.js";
import {Result, wrapError} from "../../util/wrapError.js";
import {IBeaconChain} from "../interface.js";
Expand Down Expand Up @@ -66,7 +76,7 @@ export type GossipAttestation = {
attSlot: Slot;
// for old LIFO linear gossip queue we don't have attDataBase64
// for indexed gossip queue we have attDataBase64
seenAttestationKey?: SeenAttDataKey | null;
attDataBase64?: SeenAttDataKey | null;
};

export type Step0Result = AttestationValidationResult & {
Expand Down Expand Up @@ -269,11 +279,7 @@ async function validateGossipAttestationNoSignatureCheck(
if (attestationOrBytes.serializedData) {
// gossip
const attSlot = attestationOrBytes.attSlot;
attDataKey =
// we always have seenAttestationKey from the IndexedGossipQueue, getSeenAttDataKey() just for backward
// compatible in case beaconAttestationBatchValidation is false
// TODO: remove beaconAttestationBatchValidation flag since the batch attestation is stable
attestationOrBytes.seenAttestationKey ?? getSeenAttDataKey(ForkSeq[fork], attestationOrBytes.serializedData);
attDataKey = getSeenAttDataKeyFromGossipAttestation(fork, attestationOrBytes);
const cachedAttData = attDataKey !== null ? chain.seenAttestationDatas.get(attSlot, attDataKey) : null;
if (cachedAttData === null) {
const attestation = sszDeserializeAttestation(fork, attestationOrBytes.serializedData);
Expand Down Expand Up @@ -789,3 +795,45 @@ export function computeSubnetForSlot(shuffling: EpochShuffling, slot: number, co
const committeesSinceEpochStart = shuffling.committeesPerSlot * slotsSinceEpochStart;
return (committeesSinceEpochStart + committeeIndex) % ATTESTATION_SUBNET_COUNT;
}

/**
* Return fork-dependent seen attestation key
* - for pre-electra, it's the AttestationData base64
* - for electra and later, it's the AttestationData base64 + committeeBits base64
*
* we always have attDataBase64 from the IndexedGossipQueue, getAttDataFromAttestationSerialized() just for backward compatible when beaconAttestationBatchValidation is false
* TODO: remove beaconAttestationBatchValidation flag since the batch attestation is stable
*/
export function getSeenAttDataKeyFromGossipAttestation(
fork: ForkName,
attestation: GossipAttestation
): SeenAttDataKey | null {
const {attDataBase64, serializedData} = attestation;
if (isForkPostElectra(fork)) {
const attData = attDataBase64 ?? getAttDataFromAttestationSerialized(serializedData);
const committeeBits = getCommitteeBitsFromAttestationSerialized(serializedData);
return attData && committeeBits ? attDataBase64 + committeeBits : null;
}

// pre-electra
return attDataBase64 ?? getAttDataFromAttestationSerialized(serializedData);
}

/**
* Extract attestation data key from SignedAggregateAndProof Uint8Array to use cached data from SeenAttestationDatas
* - for pre-electra, it's the AttestationData base64
* - for electra and later, it's the AttestationData base64 + committeeBits base64
*/
export function getSeenAttDataKeyFromSignedAggregateAndProof(
fork: ForkName,
aggregateAndProof: Uint8Array
): SeenAttDataKey | null {
if (isForkPostElectra(fork)) {
const attData = getAttDataFromSignedAggregateAndProofElectra(aggregateAndProof);
const committeeBits = getCommitteeBitsFromSignedAggregateAndProofElectra(aggregateAndProof);
return attData && committeeBits ? attData + committeeBits : null;
}

// pre-electra
return getAttDataFromSignedAggregateAndProofPhase0(aggregateAndProof);
}
92 changes: 33 additions & 59 deletions packages/beacon-node/src/util/sszBytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ import {
} from "@lodestar/params";

export type BlockRootHex = RootHex;
export type SeenAttDataKey = AttDataBase64 | AttDataCommitteeBitsBase64;
// pre-electra, AttestationData is used to cache attestations
export type AttDataBase64 = string;
// electra, AttestationData + CommitteeBits are used to cache attestations
export type AttDataCommitteeBitsBase64 = string;
// electra, CommitteeBits
export type CommitteeBitsBase64 = string;

// pre-electra
// class Attestation(Container):
Expand Down Expand Up @@ -71,50 +70,22 @@ export function getBlockRootFromAttestationSerialized(data: Uint8Array): BlockRo
}

/**
* Extract attestation data key from an attestation Uint8Array in order to index gossip queue and cache later in SeenAttestationDatas
*/
export function getSeenAttDataKey(forkSeq: ForkSeq, data: Uint8Array): SeenAttDataKey | null {
return forkSeq >= ForkSeq.electra ? getSeenAttDataKeyElectra(data) : getSeenAttDataKeyPhase0(data);
}

/**
* Extract attestation data + committeeBits base64 from electra attestation serialized bytes.
* Return null if data is not long enough to extract attestation data.
*/
export function getSeenAttDataKeyElectra(electraAttestationBytes: Uint8Array): AttDataCommitteeBitsBase64 | null {
const attestationData = getSeenAttDataKeyPhase0(electraAttestationBytes);

if (attestationData === null) {
return null;
}

const committeeBits = getCommitteeBitsFromAttestationSerialized(electraAttestationBytes);

if (committeeBits === null) {
return null;
}

return attestationData + toBase64(committeeBits.uint8Array);
}

/**
* Extract attestation data base64 from phase0 attestation serialized bytes.
* Extract attestation data base64 from all forks' attestation serialized bytes.
* Return null if data is not long enough to extract attestation data.
*/
export function getSeenAttDataKeyPhase0(data: Uint8Array): AttDataBase64 | null {
export function getAttDataFromAttestationSerialized(data: Uint8Array): AttDataBase64 | null {
if (data.length < VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE) {
return null;
}

// base64 is a bit efficient than hex
return toBase64(data.slice(VARIABLE_FIELD_OFFSET, VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE));
return toBase64(data.subarray(VARIABLE_FIELD_OFFSET, VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE));
}

/**
* Alias of `getSeenAttDataKeyPhase0` specifically for batch handling indexing in gossip queue
* Alias of `getAttDataFromAttestationSerialized` specifically for batch handling indexing in gossip queue
*/
export function getGossipAttestationIndex(data: Uint8Array): AttDataBase64 | null {
return getSeenAttDataKeyPhase0(data);
return getAttDataFromAttestationSerialized(data);
}

/**
Expand Down Expand Up @@ -153,7 +124,7 @@ export function getSignatureFromAttestationSerialized(data: Uint8Array): BLSSign
* Extract committee bits from Electra attestation serialized bytes.
* Return null if data is not long enough to extract committee bits.
*/
export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): BitArray | null {
export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): CommitteeBitsBase64 | null {
const committeeBitsStartIndex = VARIABLE_FIELD_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE;

if (data.length < committeeBitsStartIndex + COMMITTEE_BITS_SIZE) {
Expand All @@ -162,7 +133,7 @@ export function getCommitteeBitsFromAttestationSerialized(data: Uint8Array): Bit

const uint8Array = data.subarray(committeeBitsStartIndex, committeeBitsStartIndex + COMMITTEE_BITS_SIZE);

return new BitArray(uint8Array, MAX_COMMITTEES_PER_SLOT);
return toBase64(uint8Array);
}

//
Expand Down Expand Up @@ -212,49 +183,49 @@ export function getBlockRootFromSignedAggregateAndProofSerialized(data: Uint8Arr
}

/**
* Extract attestation data key from SignedAggregateAndProof Uint8Array to use cached data from SeenAttestationDatas
*/
export function getSeenAttDataKeyFromSignedAggregateAndProof(
forkSeq: ForkSeq,
data: Uint8Array
): SeenAttDataKey | null {
return forkSeq >= ForkSeq.electra
? getSeenAttDataKeyFromSignedAggregateAndProofElectra(data)
: getSeenAttDataKeyFromSignedAggregateAndProofPhase0(data);
}

/**
* Extract AttestationData + CommitteeBits from SignedAggregateAndProof for electra
* Extract AttestationData base64 from SignedAggregateAndProof for electra
* Return null if data is not long enough
*/
export function getSeenAttDataKeyFromSignedAggregateAndProofElectra(data: Uint8Array): SeenAttDataKey | null {
export function getAttDataFromSignedAggregateAndProofElectra(data: Uint8Array): AttDataBase64 | null {
const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET;
const endIndex = startIndex + ATTESTATION_DATA_SIZE;

if (data.length < endIndex + SIGNATURE_SIZE + COMMITTEE_BITS_SIZE) {
return null;
}
return toBase64(data.subarray(startIndex, endIndex));
}

// base64 is a bit efficient than hex
/**
* Extract CommitteeBits base64 from SignedAggregateAndProof for electra
* Return null if data is not long enough
*/
export function getCommitteeBitsFromSignedAggregateAndProofElectra(data: Uint8Array): CommitteeBitsBase64 | null {
const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE + SIGNATURE_SIZE;
const endIndex = startIndex + COMMITTEE_BITS_SIZE;

if (data.length < endIndex) {
return null;
}

return Buffer.concat([
data.subarray(startIndex, endIndex),
data.subarray(endIndex + SIGNATURE_SIZE, endIndex + SIGNATURE_SIZE + COMMITTEE_BITS_SIZE),
]).toString("base64");
return toBase64(data.subarray(startIndex, endIndex));
}

/**
* Extract attestation data base64 from signed aggregate and proof serialized bytes.
* Return null if data is not long enough to extract attestation data.
*/
export function getSeenAttDataKeyFromSignedAggregateAndProofPhase0(data: Uint8Array): AttDataBase64 | null {
export function getAttDataFromSignedAggregateAndProofPhase0(data: Uint8Array): AttDataBase64 | null {
if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE) {
return null;
}

// base64 is a bit efficient than hex
return toBase64(
data.slice(SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET, SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE)
data.subarray(
SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET,
SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE
)
);
}

Expand Down Expand Up @@ -323,6 +294,9 @@ function checkSlotHighBytes(data: Uint8Array, offset: number): boolean {
return (data[offset + 4] | data[offset + 5] | data[offset + 6] | data[offset + 7]) === 0;
}

/**
* base64 is a bit efficient than hex
*/
function toBase64(data: Uint8Array): string {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("base64");
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {ssz} from "@lodestar/types";
import {generateTestCachedBeaconStateOnlyValidators} from "../../../../../state-transition/test/perf/util.js";
import {validateAttestation, validateGossipAttestationsSameAttData} from "../../../../src/chain/validation/index.js";
import {getAttestationValidData} from "../../../utils/validationData/attestation.js";
import {getSeenAttDataKeyPhase0} from "../../../../src/util/sszBytes.js";
import {getAttDataFromAttestationSerialized} from "../../../../src/util/sszBytes.js";

describe("validate gossip attestation", () => {
setBenchOpts({
Expand Down Expand Up @@ -42,7 +42,7 @@ describe("validate gossip attestation", () => {
attestation: null,
serializedData,
attSlot,
seenAttestationKey: getSeenAttDataKeyPhase0(serializedData),
attDataBase64: getAttDataFromAttestationSerialized(serializedData),
},
subnet0
);
Expand All @@ -67,7 +67,7 @@ describe("validate gossip attestation", () => {
attestation: null,
serializedData,
attSlot,
attDataBase64: getSeenAttDataKeyPhase0(serializedData),
attDataBase64: getAttDataFromAttestationSerialized(serializedData),
};
});

Expand Down
Loading

0 comments on commit f0e83b6

Please sign in to comment.