Skip to content

Commit

Permalink
fix: get seen AttData key from SignedAggregateAndProof electra (#6802)
Browse files Browse the repository at this point in the history
* fix: get seen AttData key from SignedAggregateAndProof electra

* chore: revert the naming change to COMMITTEE_BITS_SIZE and add comment

* fix: add toBase64() util
  • Loading branch information
twoeths committed May 17, 2024
1 parent 4d8ed98 commit 6573be5
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 23 deletions.
10 changes: 7 additions & 3 deletions packages/beacon-node/src/chain/validation/aggregateAndProof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
import {IBeaconChain} from "..";
import {AttestationError, AttestationErrorCode, GossipAction} from "../errors/index.js";
import {RegenCaller} from "../regen/index.js";
import {getAttDataBase64FromSignedAggregateAndProofSerialized} from "../../util/sszBytes.js";
import {
getSeenAttDataKeyFromSignedAggregateAndProof,
} from "../../util/sszBytes.js";
import {getSelectionProofSignatureSet, getAggregateAndProofSignatureSet} from "./signatureSets/index.js";
import {
getAttestationDataSigningRoot,
Expand Down Expand Up @@ -71,8 +73,10 @@ async function validateAggregateAndProof(
const attData = aggregate.data;
const attSlot = attData.slot;

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

let attIndex;
if (ForkSeq[fork] >= ForkSeq.electra) {
Expand Down
60 changes: 48 additions & 12 deletions packages/beacon-node/src/util/sszBytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const ATTESTATION_BEACON_BLOCK_ROOT_OFFSET = VARIABLE_FIELD_OFFSET + 8 + 8;
const ROOT_SIZE = 32;
const SLOT_SIZE = 8;
const ATTESTATION_DATA_SIZE = 128;
// MAX_COMMITTEES_PER_SLOT is in bit, need to convert to byte
const COMMITTEE_BITS_SIZE = Math.max(Math.ceil(MAX_COMMITTEES_PER_SLOT / 8), 1);
const SIGNATURE_SIZE = 96;

Expand Down Expand Up @@ -88,7 +89,7 @@ export function getSeenAttDataKeyElectra(electraAttestationBytes: Uint8Array): A
return null;
}

return Buffer.from(electraAttestationBytes.subarray(startIndex, startIndex + seenKeyLength)).toString("base64");
return toBase64(electraAttestationBytes.subarray(startIndex, startIndex + seenKeyLength));
}

/**
Expand All @@ -101,9 +102,7 @@ export function getSeenAttDataKeyPhase0(data: Uint8Array): AttDataBase64 | null
}

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

/**
Expand Down Expand Up @@ -173,8 +172,9 @@ const SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET = AGGREGATE_OFFSET + VARIABLE_FIELD
const SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + 8 + 8;

/**
* Extract slot from signed aggregate and proof serialized bytes.
* Return null if data is not long enough to extract slot.
* Extract slot from signed aggregate and proof serialized bytes
* Return null if data is not long enough to extract slot
* This works for both phase + electra
*/
export function getSlotFromSignedAggregateAndProofSerialized(data: Uint8Array): Slot | null {
if (data.length < SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + SLOT_SIZE) {
Expand All @@ -185,8 +185,9 @@ export function getSlotFromSignedAggregateAndProofSerialized(data: Uint8Array):
}

/**
* Extract block root from signed aggregate and proof serialized bytes.
* Return null if data is not long enough to extract block root.
* Extract block root from signed aggregate and proof serialized bytes
* Return null if data is not long enough to extract block root
* This works for both phase + electra
*/
export function getBlockRootFromSignedAggregateAndProofSerialized(data: Uint8Array): BlockRootHex | null {
if (data.length < SIGNED_AGGREGATE_AND_PROOF_BLOCK_ROOT_OFFSET + ROOT_SIZE) {
Expand All @@ -201,19 +202,50 @@ 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
* Return null if data is not long enough
*/
export function getSeenAttDataKeyFromSignedAggregateAndProofElectra(data: Uint8Array): SeenAttDataKey | null {
const startIndex = SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET;
const endIndex = startIndex + ATTESTATION_DATA_SIZE + COMMITTEE_BITS_SIZE;

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

// base64 is a bit efficient than hex
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 getAttDataBase64FromSignedAggregateAndProofSerialized(data: Uint8Array): AttDataBase64 | null {
export function getSeenAttDataKeyFromSignedAggregateAndProofPhase0(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 Buffer.from(
data.slice(SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET, SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE)
).toString("base64");
return toBase64(
data.subarray(
SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET,
SIGNED_AGGREGATE_AND_PROOF_SLOT_OFFSET + ATTESTATION_DATA_SIZE
)
);
}

/**
Expand Down Expand Up @@ -267,3 +299,7 @@ function getSlotFromOffset(data: Uint8Array, offset: number): Slot {
// Read only the first 4 bytes of Slot, max value is 4,294,967,295 will be reached 1634 years after genesis
return dv.getUint32(offset, true);
}

function toBase64(data: Uint8Array): string {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("base64");
}
84 changes: 76 additions & 8 deletions packages/beacon-node/test/unit/util/sszBytes.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {describe, it, expect} from "vitest";
import {BitArray} from "@chainsafe/ssz";
import {allForks, deneb, Epoch, isElectraAttestation, phase0, RootHex, Slot, ssz} from "@lodestar/types";
import {allForks, deneb, electra, Epoch, isElectraAttestation, phase0, RootHex, Slot, ssz} from "@lodestar/types";
import {fromHex, toHex} from "@lodestar/utils";
import {ForkName, MAX_COMMITTEES_PER_SLOT} from "@lodestar/params";
import {
getSeenAttDataKeyPhase0,
getAttDataBase64FromSignedAggregateAndProofSerialized,
getSeenAttDataKeyFromSignedAggregateAndProofPhase0,
getAggregationBitsFromAttestationSerialized as getAggregationBitsFromAttestationSerialized,
getBlockRootFromAttestationSerialized,
getBlockRootFromSignedAggregateAndProofSerialized,
Expand All @@ -15,6 +15,7 @@ import {
getSlotFromSignedBeaconBlockSerialized,
getSlotFromBlobSidecarSerialized,
getCommitteeBitsFromAttestationSerialized,
getSeenAttDataKeyFromSignedAggregateAndProofElectra,
} from "../../../src/util/sszBytes.js";

describe("attestation SSZ serialized picking", () => {
Expand Down Expand Up @@ -104,16 +105,15 @@ describe("attestation SSZ serialized picking", () => {
});
});

describe("aggregateAndProof SSZ serialized picking", () => {
describe("phase0 SignedAggregateAndProof SSZ serialized picking", () => {
const testCases: phase0.SignedAggregateAndProof[] = [
ssz.phase0.SignedAggregateAndProof.defaultValue(),
signedAggregateAndProofFromValues(
phase0SignedAggregateAndProofFromValues(
4_000_000,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
200_00,
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffff"
),
ssz.electra.SignedAggregateAndProof.defaultValue(),
];

for (const [i, signedAggregateAndProof] of testCases.entries()) {
Expand All @@ -128,7 +128,7 @@ describe("aggregateAndProof SSZ serialized picking", () => {
);

const attDataBase64 = ssz.phase0.AttestationData.serialize(signedAggregateAndProof.message.aggregate.data);
expect(getAttDataBase64FromSignedAggregateAndProofSerialized(bytes)).toBe(
expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(bytes)).toBe(
Buffer.from(attDataBase64).toString("base64")
);
});
Expand All @@ -151,7 +151,60 @@ describe("aggregateAndProof SSZ serialized picking", () => {
it("getAttDataBase64FromSignedAggregateAndProofSerialized - invalid data", () => {
const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 339];
for (const size of invalidAttDataBase64DataSizes) {
expect(getAttDataBase64FromSignedAggregateAndProofSerialized(Buffer.alloc(size))).toBeNull();
expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(Buffer.alloc(size))).toBeNull();
}
});
});

describe("electra SignedAggregateAndProof SSZ serialized picking", () => {
const testCases: electra.SignedAggregateAndProof[] = [
ssz.electra.SignedAggregateAndProof.defaultValue(),
electraSignedAggregateAndProofFromValues(
4_000_000,
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
200_00,
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeffffffffffffffffffffffffffffffff"
),
];

for (const [i, signedAggregateAndProof] of testCases.entries()) {
it(`signedAggregateAndProof ${i}`, () => {
const bytes = ssz.electra.SignedAggregateAndProof.serialize(signedAggregateAndProof);

expect(getSlotFromSignedAggregateAndProofSerialized(bytes)).toBe(
signedAggregateAndProof.message.aggregate.data.slot
);
expect(getBlockRootFromSignedAggregateAndProofSerialized(bytes)).toBe(
toHex(signedAggregateAndProof.message.aggregate.data.beaconBlockRoot)
);

const attDataBase64 = ssz.phase0.AttestationData.serialize(signedAggregateAndProof.message.aggregate.data);
const committeeBits = ssz.electra.CommitteeBits.serialize(
signedAggregateAndProof.message.aggregate.committeeBits
);
const seenKey = Buffer.concat([attDataBase64, committeeBits]).toString("base64");
expect(getSeenAttDataKeyFromSignedAggregateAndProofElectra(bytes)).toBe(seenKey);
});
}

it("getSlotFromSignedAggregateAndProofSerialized - invalid data", () => {
const invalidSlotDataSizes = [0, 4, 11];
for (const size of invalidSlotDataSizes) {
expect(getSlotFromSignedAggregateAndProofSerialized(Buffer.alloc(size))).toBeNull();
}
});

it("getBlockRootFromSignedAggregateAndProofSerialized - invalid data", () => {
const invalidBlockRootDataSizes = [0, 4, 20, 227];
for (const size of invalidBlockRootDataSizes) {
expect(getBlockRootFromSignedAggregateAndProofSerialized(Buffer.alloc(size))).toBeNull();
}
});

it("getAttDataBase64FromSignedAggregateAndProofSerialized - invalid data", () => {
const invalidAttDataBase64DataSizes = [0, 4, 100, 128, 339];
for (const size of invalidAttDataBase64DataSizes) {
expect(getSeenAttDataKeyFromSignedAggregateAndProofPhase0(Buffer.alloc(size))).toBeNull();
}
});
});
Expand Down Expand Up @@ -206,7 +259,7 @@ function attestationFromValues(
return attestation;
}

function signedAggregateAndProofFromValues(
function phase0SignedAggregateAndProofFromValues(
slot: Slot,
blockRoot: RootHex,
targetEpoch: Epoch,
Expand All @@ -220,6 +273,21 @@ function signedAggregateAndProofFromValues(
return signedAggregateAndProof;
}

function electraSignedAggregateAndProofFromValues(
slot: Slot,
blockRoot: RootHex,
targetEpoch: Epoch,
targetRoot: RootHex
): electra.SignedAggregateAndProof {
const signedAggregateAndProof = ssz.electra.SignedAggregateAndProof.defaultValue();
signedAggregateAndProof.message.aggregate.data.slot = slot;
signedAggregateAndProof.message.aggregate.data.beaconBlockRoot = fromHex(blockRoot);
signedAggregateAndProof.message.aggregate.data.target.epoch = targetEpoch;
signedAggregateAndProof.message.aggregate.data.target.root = fromHex(targetRoot);
signedAggregateAndProof.message.aggregate.committeeBits = BitArray.fromSingleBit(MAX_COMMITTEES_PER_SLOT, 1);
return signedAggregateAndProof;
}

function signedBeaconBlockFromValues(slot: Slot): phase0.SignedBeaconBlock {
const signedBeaconBlock = ssz.phase0.SignedBeaconBlock.defaultValue();
signedBeaconBlock.message.slot = slot;
Expand Down

0 comments on commit 6573be5

Please sign in to comment.