Skip to content

Commit

Permalink
LightClient server no state cache + tracks head (#3461)
Browse files Browse the repository at this point in the history
* WIP

* Refactor lightclient server db repositories

* WIP Lightclient refactor

* Lightclient tracking head

* Fix branch proof sorting

* Move lightclient code to package root

* Move LightclientServer code to module index

* Add e2e test and fix bugs

* Log sync committee periods

* Sync 3 periods

* Clean logging in LC

* Fix broken tests

* Rename getCommitteeUpdates route

* Use getStateV2 to download altair states

* Remove blockRoot from LightclientHeaderUpdate

* Polish lightclient

* Remove clock

* Remove beacon state transition dependency

* Add mock sync test

* Change getStateProof to GET

* Deprecate un-used db buckets

* Test fetching proofs on lightclient head state

* Test fetching proofs in sim test

* Test query serializtion

* Fix proof paths serdes in api

* Use Promise.all in storeSyncCommittee

* Rename finalizedHeader to checkpointHeader

* Remove genesisWitness

* Remove genesis proof dead code

* Rename lightclient_update to lightclient_header_update

* Pass only checkpoint root

* Use console levels in getLcLoggerConsole

* Remove stateProofPaths dead code

* Rename to initializeFromCheckpointRoot

* lightclient snapshot type has a single SyncCommittee

* Simplify tree position constants

* Log 'New sync committee period' message only once
  • Loading branch information
dapplion authored Dec 1, 2021
1 parent 2d15bc1 commit be50e4e
Show file tree
Hide file tree
Showing 81 changed files with 2,891 additions and 2,013 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ packages/lodestar/test-logs/
packages/beacon-state-transition/test-cache
packages/*/benchmark_data
benchmark_data
invalidSszObjects/

# Autogenerated docs
packages/**/docs
Expand Down
5 changes: 0 additions & 5 deletions packages/api/src/client/lightclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,5 @@ export function getClient(_config: IChainForkConfig, httpClient: IHttpClient): A
const proof = deserializeProof(new Uint8Array(buffer));
return {data: proof};
},
async getInitProof(epoch) {
const buffer = await httpClient.arrayBuffer(fetchOptsSerializers.getInitProof(epoch));
const proof = deserializeProof(new Uint8Array(buffer));
return {data: proof};
},
};
}
21 changes: 20 additions & 1 deletion packages/api/src/routes/events.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import {Epoch, Number64, phase0, Slot, ssz, StringType, RootHex} from "@chainsafe/lodestar-types";
import {Epoch, Number64, phase0, Slot, ssz, StringType, RootHex, altair} from "@chainsafe/lodestar-types";
import {ContainerType, Json, Type} from "@chainsafe/ssz";
import {jsonOpts, RouteDef, TypeJson} from "../utils";

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes

export type LightclientHeaderUpdate = {
syncAggregate: altair.SyncAggregate;
header: phase0.BeaconBlockHeader;
};

export enum EventType {
/**
* The node has finished processing, resulting in a new head. previous_duty_dependent_root is
Expand All @@ -22,6 +27,8 @@ export enum EventType {
finalizedCheckpoint = "finalized_checkpoint",
/** The node has reorganized its chain */
chainReorg = "chain_reorg",
/** New or better header update available */
lightclientHeaderUpdate = "lightclient_header_update",
}

export type EventData = {
Expand All @@ -46,6 +53,7 @@ export type EventData = {
newHeadState: RootHex;
epoch: Epoch;
};
[EventType.lightclientHeaderUpdate]: LightclientHeaderUpdate;
};

export type BeaconEvent = {[K in EventType]: {type: K; message: EventData[K]}}[EventType];
Expand Down Expand Up @@ -142,6 +150,17 @@ export function getTypeByEvent(): {[K in EventType]: Type<EventData[K]>} {
epoch: "epoch",
},
}),

[EventType.lightclientHeaderUpdate]: new ContainerType<EventData[EventType.lightclientHeaderUpdate]>({
fields: {
syncAggregate: ssz.altair.SyncAggregate,
header: ssz.phase0.BeaconBlockHeader,
},
casingMap: {
syncAggregate: "sync_aggregate",
header: "header",
},
}),
};
}

Expand Down
97 changes: 53 additions & 44 deletions packages/api/src/routes/lightclient.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,70 @@
import {Path} from "@chainsafe/ssz";
import {ContainerType, Path, VectorType} from "@chainsafe/ssz";
import {Proof} from "@chainsafe/persistent-merkle-tree";
import {altair, ssz, SyncPeriod} from "@chainsafe/lodestar-types";
import {
ArrayOf,
reqEmpty,
ReturnTypes,
RoutesData,
Schema,
sameType,
ContainerData,
ReqSerializers,
ReqEmpty,
} from "../utils";
import {altair, phase0, ssz, SyncPeriod} from "@chainsafe/lodestar-types";
import {ArrayOf, ReturnTypes, RoutesData, Schema, sameType, ContainerData, ReqSerializers} from "../utils";
import {queryParseProofPathsArr, querySerializeProofPathsArr} from "../utils/serdes";

// See /packages/api/src/routes/index.ts for reasoning and instructions to add new routes

export type LightclientSnapshotWithProof = {
header: phase0.BeaconBlockHeader;
currentSyncCommittee: altair.SyncCommittee;
/** Single branch proof from state root to currentSyncCommittee */
currentSyncCommitteeBranch: Uint8Array[];
};

export type Api = {
/** TODO: description */
/**
* Returns a multiproof of `paths` at the requested `stateId`.
* The requested `stateId` may not be available. Regular nodes only keep recent states in memory.
*/
getStateProof(stateId: string, paths: Path[]): Promise<{data: Proof}>;
/** TODO: description */
getBestUpdates(from: SyncPeriod, to: SyncPeriod): Promise<{data: altair.LightClientUpdate[]}>;
/** TODO: description */
getLatestUpdateFinalized(): Promise<{data: altair.LightClientUpdate}>;
/** TODO: description */
getLatestUpdateNonFinalized(): Promise<{data: altair.LightClientUpdate}>;
/**
* Fetch a proof needed for light client initialization
* Returns an array of best updates in the requested periods within the inclusive range `from` - `to`.
* Best is defined by (in order of priority):
* - Is finalized update
* - Has most bits
* - Oldest update
*/
getCommitteeUpdates(from: SyncPeriod, to: SyncPeriod): Promise<{data: altair.LightClientUpdate[]}>;
/**
* Fetch a snapshot with a proof to a trusted block root.
* The trusted block root should be fetched with similar means to a weak subjectivity checkpoint.
* Only block roots for checkpoints are guaranteed to be available.
*/
getInitProof(blockRoot: string): Promise<{data: Proof}>;
getSnapshot(blockRoot: string): Promise<{data: LightclientSnapshotWithProof}>;
};

/**
* Define javascript values for each route
*/
export const routesData: RoutesData<Api> = {
getStateProof: {url: "/eth/v1/lightclient/proof/:stateId", method: "POST"},
getBestUpdates: {url: "/eth/v1/lightclient/best_updates", method: "GET"},
getLatestUpdateFinalized: {url: "/eth/v1/lightclient/latest_update_finalized", method: "GET"},
getLatestUpdateNonFinalized: {url: "/eth/v1/lightclient/latest_update_nonfinalized", method: "GET"},
getInitProof: {url: "/eth/v1/lightclient/init_proof/:blockRoot", method: "GET"},
getStateProof: {url: "/eth/v1/lightclient/proof/:stateId", method: "GET"},
getCommitteeUpdates: {url: "/eth/v1/lightclient/committee_updates", method: "GET"},
getSnapshot: {url: "/eth/v1/lightclient/snapshot/:blockRoot", method: "GET"},
};

export type ReqTypes = {
getStateProof: {params: {stateId: string}; body: Path[]};
getBestUpdates: {query: {from: number; to: number}};
getLatestUpdateFinalized: ReqEmpty;
getLatestUpdateNonFinalized: ReqEmpty;
getInitProof: {params: {blockRoot: string}};
getStateProof: {params: {stateId: string}; query: {paths: string[]}};
getCommitteeUpdates: {query: {from: number; to: number}};
getSnapshot: {params: {blockRoot: string}};
};

export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
return {
getStateProof: {
writeReq: (stateId, paths) => ({params: {stateId}, body: paths}),
parseReq: ({params, body}) => [params.stateId, body],
writeReq: (stateId, paths) => ({params: {stateId}, query: {paths: querySerializeProofPathsArr(paths)}}),
parseReq: ({params, query}) => [params.stateId, queryParseProofPathsArr(query.paths)],
schema: {params: {stateId: Schema.StringRequired}, body: Schema.AnyArray},
},

getBestUpdates: {
getCommitteeUpdates: {
writeReq: (from, to) => ({query: {from, to}}),
parseReq: ({query}) => [query.from, query.to],
schema: {query: {from: Schema.UintRequired, to: Schema.UintRequired}},
},

getLatestUpdateFinalized: reqEmpty,
getLatestUpdateNonFinalized: reqEmpty,

getInitProof: {
getSnapshot: {
writeReq: (blockRoot) => ({params: {blockRoot}}),
parseReq: ({params}) => [params.blockRoot],
schema: {params: {blockRoot: Schema.StringRequired}},
Expand All @@ -75,13 +73,24 @@ export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
}

export function getReturnTypes(): ReturnTypes<Api> {
const lightclientSnapshotWithProofType = new ContainerType<LightclientSnapshotWithProof>({
fields: {
header: ssz.phase0.BeaconBlockHeader,
currentSyncCommittee: ssz.altair.SyncCommittee,
currentSyncCommitteeBranch: new VectorType({elementType: ssz.Root, length: 5}),
},
// Custom type, not in the consensus specs
casingMap: {
header: "header",
currentSyncCommittee: "current_sync_committee",
currentSyncCommitteeBranch: "current_sync_committee_branch",
},
});

return {
// Just sent the proof JSON as-is
getStateProof: sameType(),
getBestUpdates: ContainerData(ArrayOf(ssz.altair.LightClientUpdate)),
getLatestUpdateFinalized: ContainerData(ssz.altair.LightClientUpdate),
getLatestUpdateNonFinalized: ContainerData(ssz.altair.LightClientUpdate),
// Just sent the proof JSON as-is
getInitProof: sameType(),
getCommitteeUpdates: ContainerData(ArrayOf(ssz.altair.LightClientUpdate)),
getSnapshot: ContainerData(lightclientSnapshotWithProofType),
};
}
20 changes: 10 additions & 10 deletions packages/api/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ export function registerRoutes(
): void {
const routesByNamespace: {
// Enforces that we are declaring routes for every routeId in `Api`
[K in keyof Api]: {
[K in keyof Api]: () => {
// The ReqTypes are enforced in each getRoutes return type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[K2 in keyof Api[K]]: ServerRoute<any>;
};
} = {
// Initializes route types and their definitions
beacon: beacon.getRoutes(config, api.beacon),
config: configApi.getRoutes(config, api.config),
debug: debug.getRoutes(config, api.debug),
events: events.getRoutes(config, api.events),
lightclient: lightclient.getRoutes(config, api.lightclient),
lodestar: lodestar.getRoutes(config, api.lodestar),
node: node.getRoutes(config, api.node),
validator: validator.getRoutes(config, api.validator),
beacon: () => beacon.getRoutes(config, api.beacon),
config: () => configApi.getRoutes(config, api.config),
debug: () => debug.getRoutes(config, api.debug),
events: () => events.getRoutes(config, api.events),
lightclient: () => lightclient.getRoutes(config, api.lightclient),
lodestar: () => lodestar.getRoutes(config, api.lodestar),
node: () => node.getRoutes(config, api.node),
validator: () => validator.getRoutes(config, api.validator),
};

for (const namespace of enabledNamespaces) {
Expand All @@ -48,7 +48,7 @@ export function registerRoutes(
throw Error(`Unknown api namespace ${namespace}`);
}

registerRoutesGroup(server, routes);
registerRoutesGroup(server, routes());
}
}

Expand Down
10 changes: 0 additions & 10 deletions packages/api/src/server/lightclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,5 @@ export function getRoutes(config: IChainForkConfig, api: Api): ServerRoutes<Api,
return Buffer.from(serializeProof(proof));
},
},
// Non-JSON route. Return binary
getInitProof: {
...serverRoutes.getInitProof,
handler: async (req) => {
const args = reqSerializers.getInitProof.parseReq(req);
const {data: proof} = await api.getInitProof(...args);
// Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer
return Buffer.from(serializeProof(proof));
},
},
};
}
45 changes: 45 additions & 0 deletions packages/api/src/utils/serdes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {Path} from "@chainsafe/ssz";

/**
* Serialize proof path to JSON.
* @param paths `[["finalized_checkpoint", 0, "root", 12000]]`
* @returns `['["finalized_checkpoint",0,"root",12000]']`
*/
export function querySerializeProofPathsArr(paths: Path[]): string[] {
return paths.map((path) => JSON.stringify(path));
}

/**
* Deserialize JSON proof path to proof path
* @param pathStrs `['["finalized_checkpoint",0,"root",12000]']`
* @returns `[["finalized_checkpoint", 0, "root", 12000]]`
*/
export function queryParseProofPathsArr(pathStrs: string | string[]): Path[] {
if (Array.isArray(pathStrs)) {
return pathStrs.map((pathStr) => queryParseProofPaths(pathStr));
} else {
return [queryParseProofPaths(pathStrs) as Path];
}
}

/**
* Deserialize single JSON proof path to proof path
* @param pathStr `'["finalized_checkpoint",0,"root",12000]'`
* @returns `["finalized_checkpoint", 0, "root", 12000]`
*/
export function queryParseProofPaths(pathStr: string): Path {
const path = JSON.parse(pathStr) as Path;

if (!Array.isArray(path)) {
throw Error("Proof pathStr is not an array");
}

for (let i = 0; i < path.length; i++) {
const elType = typeof path[i];
if (elType !== "string" && elType !== "number") {
throw Error(`Proof pathStr[${i}] not string or number`);
}
}

return path;
}
33 changes: 17 additions & 16 deletions packages/api/test/unit/lightclient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {Api, ReqTypes} from "../../src/routes/lightclient";
import {getClient} from "../../src/client/lightclient";
import {getRoutes} from "../../src/server/lightclient";
import {runGenericServerTest} from "../utils/genericServerTest";
import {toHexString} from "@chainsafe/ssz";

const root = Uint8Array.from(Buffer.alloc(32, 1));

Expand All @@ -16,8 +17,8 @@ describe("lightclient", () => {
args: [
"head",
[
["validator", 0, "balance"],
["finalized_checkpoint", "root"],
// ["validator", 0, "balance"],
["finalized_checkpoint", 0, "root", 12000],
],
],
res: {
Expand All @@ -27,26 +28,26 @@ describe("lightclient", () => {
leaves: [root, root, root, root],
},
},
/* eslint-disable quotes */
query: {
paths: [
// '["validator",0,"balance"]',
'["finalized_checkpoint",0,"root",12000]',
],
},
/* eslint-enable quotes */
},
getBestUpdates: {
getCommitteeUpdates: {
args: [1, 2],
res: {data: [lightClientUpdate]},
},
getLatestUpdateFinalized: {
args: [],
res: {data: lightClientUpdate},
},
getLatestUpdateNonFinalized: {
args: [],
res: {data: lightClientUpdate},
},
getInitProof: {
args: ["0x00"],
getSnapshot: {
args: [toHexString(root)],
res: {
data: {
type: ProofType.treeOffset,
offsets: [1, 2, 3],
leaves: [root, root, root, root],
header: ssz.phase0.BeaconBlockHeader.defaultValue(),
currentSyncCommittee: lightClientUpdate.nextSyncCommittee,
currentSyncCommitteeBranch: [root, root, root, root, root], // Vector(Root, 5)
},
},
},
Expand Down
Loading

0 comments on commit be50e4e

Please sign in to comment.