Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/beacon-node/src/db/buckets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export enum Bucket {
// altair_lightClientSyncCommitteeProof = 35, // DEPRECATED on v0.32.0
// index_lightClientInitProof = 36, // DEPRECATED on v0.32.0

backfilled_ranges = 42, // Backfilled From to To, inclusive of both From, To
backfill_state = 42, // stores Epoch -> EpochBackfillState data and backfill range singleton object

// Buckets to support LightClient server v2
lightClient_syncCommitteeWitness = 51, // BlockRoot -> SyncCommitteeWitness
Expand Down
36 changes: 36 additions & 0 deletions packages/beacon-node/src/db/repositories/backfillState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {ContainerType, ListBasicType, OptionalType, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {DatabaseController, Repository} from "@lodestar/db";
import {NUMBER_OF_COLUMNS} from "@lodestar/params";
import {Epoch, ssz} from "@lodestar/types";
import {bytesToInt} from "@lodestar/utils";
import {Bucket, getBucketNameByValue} from "../buckets.js";
import {BACKFILL_RANGE_KEY} from "../single/backfillRange.js";

export const epochBackfillStateSSZ = new ContainerType(
{
hasBlock: ssz.Boolean,
hasBlobs: new OptionalType(ssz.Boolean),
columnIndices: new OptionalType(new ListBasicType(ssz.ColumnIndex, NUMBER_OF_COLUMNS)),
},
{typeName: "EpochBackfillState", jsonCase: "eth2"}
);
export type EpochBackfillState = ValueOf<typeof epochBackfillStateSSZ>;

export class BackfillState extends Repository<Epoch, EpochBackfillState> {
constructor(config: ChainForkConfig, db: DatabaseController<Uint8Array, Uint8Array>) {
const bucket = Bucket.backfill_state;
super(config, db, bucket, epochBackfillStateSSZ, getBucketNameByValue(bucket));
}

encodeKey(key: Epoch): Uint8Array {
if (key === BACKFILL_RANGE_KEY) {
throw new Error("Reserved key for backfill range singleton object");
}
return super.encodeKey(key);
}

decodeKey(data: Uint8Array): number {
return bytesToInt(super.decodeKey(data) as unknown as Uint8Array, "be");
}
}
29 changes: 0 additions & 29 deletions packages/beacon-node/src/db/repositories/backfilledRanges.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/beacon-node/src/db/repositories/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,5 @@ export {BestLightClientUpdateRepository} from "./lightclientBestUpdate.js";
export {CheckpointHeaderRepository} from "./lightclientCheckpointHeader.js";
export {SyncCommitteeRepository} from "./lightclientSyncCommittee.js";
export {SyncCommitteeWitnessRepository} from "./lightclientSyncCommitteeWitness.js";
export {BackfilledRanges} from "./backfilledRanges.js";
export {BackfillState} from "./backfillState.js";
export {BLSToExecutionChangeRepository} from "./blsToExecutionChange.js";
47 changes: 47 additions & 0 deletions packages/beacon-node/src/db/single/backfillRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {ContainerType, Type, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {Db, DbReqOpts, encodeKey as _encodeKey} from "@lodestar/db";
import {ssz} from "@lodestar/types";
import {Bucket, getBucketNameByValue} from "../buckets.js";

export const backfillRangeSSZ = new ContainerType(
{
beginningEpoch: ssz.Epoch,
endingEpoch: ssz.Epoch,
},
{typeName: "BackfillRange", jsonCase: "eth2"}
);
export type BackfillRangeWrapper = ValueOf<typeof backfillRangeSSZ>;

// unique & practically impossible key to store BackfillRange inside 'backfill_state'
// bucket which stores Epoch -> EpochBackfillState key value pairs
export const BACKFILL_RANGE_KEY = -1;

export class BackfillRange {
private readonly bucket: Bucket;
private readonly db: Db;
private readonly dbReqOpts: DbReqOpts; // to record metrics
private readonly key: Uint8Array;
private readonly type: Type<BackfillRangeWrapper>;

constructor(_config: ChainForkConfig, db: Db) {
this.db = db;
this.bucket = Bucket.backfill_state;
this.key = _encodeKey(this.bucket, BACKFILL_RANGE_KEY);
this.type = backfillRangeSSZ;
this.dbReqOpts = {bucketId: getBucketNameByValue(this.bucket)};
}

async put(value: BackfillRangeWrapper): Promise<void> {
await this.db.put(this.key, this.type.serialize(value), this.dbReqOpts);
}

async get(): Promise<BackfillRangeWrapper | null> {
const value = await this.db.get(this.key, this.dbReqOpts);
return value ? this.type.deserialize(value) : null;
}

async delete(): Promise<void> {
await this.db.delete(this.key, this.dbReqOpts);
}
}
1 change: 1 addition & 0 deletions packages/beacon-node/src/db/single/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export {BackfillRange} from "./backfillRange.js";
export {PreGenesisState} from "./preGenesisState.js";
export {PreGenesisStateLastProcessedBlock} from "./preGenesisStateLastProcessedBlock.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {config} from "@lodestar/config/default";
import {LevelDbController} from "@lodestar/db";
import {rimraf} from "rimraf";
import {afterEach, beforeEach, describe, expect, it} from "vitest";
import {BackfillState} from "../../../../../src/db/repositories/backfillState.js";
import {BACKFILL_RANGE_KEY} from "../../../../../src/db/single/backfillRange.js";
import {testLogger} from "../../../../utils/logger.js";

describe("BackfillState repository", () => {
const testDir = "./.tmp";
let backfillState: BackfillState;
let db: LevelDbController;

beforeEach(async () => {
db = await LevelDbController.create({name: testDir}, {logger: testLogger()});
backfillState = new BackfillState(config, db);
});

afterEach(async () => {
await db.close();
rimraf.sync(testDir);
});

it("should reject reserved key `BACKFILL_RANGE_KEY` insertion", () => {
expect(() => {
backfillState.encodeKey(BACKFILL_RANGE_KEY);
}).toThrow("Reserved key for backfill range singleton object");
});

it("should encode/decode regular epoch keys correctly", () => {
const testEpochs = [1, 2, 100, 1000];

for (const epoch of testEpochs) {
expect(() => {
const encoded = backfillState.encodeKey(epoch);
const decoded = backfillState.decodeKey(encoded);
expect(decoded).toBe(epoch);
}).not.toThrow();
}
});

it("should handle regular values", async () => {
const epoch = config.DENEB_FORK_EPOCH - 1;
const testData = {
hasBlock: true,
hasBlobs: true,
columnIndices: [1, 2, 3],
};

await backfillState.put(epoch, testData);
const retrieved = await backfillState.get(epoch);

expect(retrieved).toEqual(testData);
});

it("should handle null values", async () => {
const epoch = config.DENEB_FORK_EPOCH - 1;
const testData = {
hasBlock: true,
hasBlobs: null,
columnIndices: null,
};

await backfillState.put(epoch, testData);
const retrieved = await backfillState.get(epoch);

expect(retrieved).toEqual(testData);
});

it("should handle batch of values", async () => {
const epoch = config.DENEB_FORK_EPOCH - 1;
const testData = [
{key: epoch, value: {hasBlock: true, hasBlobs: true, columnIndices: [1, 2, 3]}},
{key: epoch + 1, value: {hasBlock: true, hasBlobs: null, columnIndices: null}},
{key: epoch + 2, value: {hasBlock: false, hasBlobs: false, columnIndices: []}},
];

await backfillState.batchPut(testData);
const retrievedData = [];
for (const element of testData) {
const retrievedValue = await backfillState.get(element.key);
retrievedData.push({key: element.key, value: retrievedValue});
}

expect(retrievedData).toEqual(testData);
});
});
Loading