Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
79 changes: 79 additions & 0 deletions packages/beacon-node/src/db/repositories/backfillState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {ContainerType, ListBasicType, ValueOf} from "@chainsafe/ssz";
import {ChainForkConfig} from "@lodestar/config";
import {DatabaseController, Repository} from "@lodestar/db";
import {ForkName, NUMBER_OF_COLUMNS} from "@lodestar/params";
import {computeStartSlotAtEpoch} from "@lodestar/state-transition";
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: ssz.Boolean,
columnIndices: new ListBasicType(ssz.ColumnIndex, NUMBER_OF_COLUMNS),
},
{typeName: "EpochBackfillState", jsonCase: "eth2"}
);
export type EpochBackfillStateWrapper = ValueOf<typeof epochBackfillStateSSZ>;
export type EpochBackfillState = {
hasBlock: boolean;
hasBlobs: boolean | null;
columnIndices: number[] | null;
};

export class BackfillState extends Repository<Epoch, EpochBackfillStateWrapper> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the term "state" here could be misleading, I'd rather call this BackfillTracker or BackfillProgress

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@matthewkeil please confirm

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");
}

async _get(epoch: Epoch): Promise<EpochBackfillState | null> {
const wrappedValue = await super.get(epoch);
if (!wrappedValue) return null;
return this.unwrapEpochBackfillState(wrappedValue, epoch);
}

async _put(epoch: Epoch, value: EpochBackfillState): Promise<void> {
const wrappedValue = this.wrapEpochBackfillState(value);
await super.put(epoch, wrappedValue);
}
Copy link
Contributor Author

@vedant-asati vedant-asati Aug 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't override get and put functions as the function signatures aren't matching with the methods in parent class due to different types: EpochBackfillState and EpochBackfillStateWrapper. So, temporarily changed name to _get and _put.
This doesn't seem to be a good practice. What are your thoughts? What else can be done?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if @nflaig has any ideas on how to do this. Like perhaps massaging the schema a bit.

Copy link
Member

@matthewkeil matthewkeil Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can sort something so that we dont need to change the type to EpochBackfillState. So not needing blobs will just get interpreted correctly by the consumer depending on the epoch.... @wemeetagain might also have a suggestion

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another idea would be to add a field for DAType and that will be a

export const enum DAType = {
    PreData = "PreData",
    Blobs = "Blobs",
    Columns = "Columns",
};

We have this exported already by our BlockInput types so can refactor that to a better place in the code for reuse.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the reason we have EpochBackfillState type which deviates from repository type?

Copy link
Contributor Author

@vedant-asati vedant-asati Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the DB, we can't store EpochBackfillState with nullable fields as it is not compatible with ssz types. We need null in certain situations, for example, to differentiate between blobs' availability and when they are not even present i.e. pre-Deneb (hasBlobs: boolean | null).
So thought to store EpochBackfillStateWrapper in DB and convert it to EpochBackfillState while putting and getting as needed.

Copy link
Member

@nflaig nflaig Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we shouldn't even implement backfill for pre-fulu and only consider data columns, this might simplify things

Edit: even if we don't support pre-fulu we might still need to handle the case if we backfill outside of the DA period (~18 days) after which we only need to backfill sync blocks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can use Optional ssz type which allows the value to be nullable

Copy link
Contributor Author

@vedant-asati vedant-asati Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Added OptionalType, modified the unit tests, they're successfully passing.
Please check once.


private unwrapEpochBackfillState(value: EpochBackfillStateWrapper, epoch: Epoch): EpochBackfillState {
const fork = this.config.getForkName(computeStartSlotAtEpoch(epoch));
return {
hasBlock: value.hasBlock,
hasBlobs: this.shouldIncludeBlobs(fork) ? value.hasBlobs : null,
columnIndices: this.shouldIncludeCustodyColumns(fork) ? value.columnIndices : null,
};
}

private wrapEpochBackfillState(value: EpochBackfillState): EpochBackfillStateWrapper {
return {
hasBlock: value.hasBlock,
// Using default values for fields that don't exist in the fork at the epoch
hasBlobs: value.hasBlobs ?? false,
columnIndices: value.columnIndices ?? [],
};
}

private shouldIncludeBlobs(fork: ForkName): boolean {
return fork >= ForkName.deneb;
}

private shouldIncludeCustodyColumns(fork: ForkName): boolean {
return fork >= ForkName.fulu;
}
}
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";