Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compact proofs API #5155

Merged
merged 5 commits into from
Feb 20, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 2 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/persistent-merkle-tree": "^0.4.2",
"@chainsafe/ssz": "^0.9.2",
"@chainsafe/persistent-merkle-tree": "^0.5.0",
"@chainsafe/ssz": "^0.10.1",
"@lodestar/config": "^1.4.3",
"@lodestar/params": "^1.4.3",
"@lodestar/types": "^1.4.3",
Expand Down
43 changes: 39 additions & 4 deletions packages/api/src/beacon/client/proof.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {IChainForkConfig} from "@lodestar/config";
import {deserializeProof} from "@chainsafe/persistent-merkle-tree";
import {CompactMultiProof, ProofType} from "@chainsafe/persistent-merkle-tree";
import {Api, ReqTypes, routesData, getReqSerializers} from "../routes/proof.js";
import {IHttpClient, getFetchOptsSerializers, HttpError} from "../../utils/client/index.js";
import {HttpStatusCode} from "../../utils/client/httpStatusCode.js";
Expand All @@ -14,10 +14,45 @@ export function getClient(_config: IChainForkConfig, httpClient: IHttpClient): A
const fetchOptsSerializers = getFetchOptsSerializers<Api, ReqTypes>(routesData, reqSerializers);

return {
async getStateProof(stateId, paths) {
async getStateProof(stateId, descriptor) {
try {
const res = await httpClient.arrayBuffer(fetchOptsSerializers.getStateProof(stateId, paths));
const proof = deserializeProof(new Uint8Array(res.body));
const res = await httpClient.arrayBuffer(fetchOptsSerializers.getStateProof(stateId, descriptor));
// reuse the response ArrayBuffer
if (!Number.isInteger(res.body.byteLength / 32)) {
throw new Error("Invalid proof data: Length not divisible by 32");
}

const proof: CompactMultiProof = {
type: ProofType.compactMulti,
descriptor,
leaves: Array.from({length: res.body.byteLength / 32}, (_, i) => new Uint8Array(res.body, i * 32, 32)),
};

return {ok: true, response: {data: proof}, status: HttpStatusCode.OK};
} catch (err) {
if (err instanceof HttpError) {
return {
ok: false,
error: {code: err.status, message: err.message, operationId: "proof.getStateProof"},
status: err.status,
};
}
throw err;
}
},
async getBlockProof(blockId, descriptor) {
try {
const res = await httpClient.arrayBuffer(fetchOptsSerializers.getBlockProof(blockId, descriptor));
// reuse the response ArrayBuffer
if (!Number.isInteger(res.body.byteLength / 32)) {
throw new Error("Invalid proof data: Length not divisible by 32");
}

const proof: CompactMultiProof = {
type: ProofType.compactMulti,
descriptor,
leaves: Array.from({length: res.body.byteLength / 32}, (_, i) => new Uint8Array(res.body, i * 32, 32)),
};

return {ok: true, response: {data: proof}, status: HttpStatusCode.OK};
} catch (err) {
Expand Down
31 changes: 23 additions & 8 deletions packages/api/src/beacon/routes/proof.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,27 @@
import {JsonPath} from "@chainsafe/ssz";
import {Proof} from "@chainsafe/persistent-merkle-tree";
import {fromHexString, toHexString} from "@chainsafe/ssz";
import {ReturnTypes, RoutesData, Schema, sameType, ReqSerializers} from "../../utils/index.js";
import {queryParseProofPathsArr, querySerializeProofPathsArr} from "../../utils/serdes.js";
import {HttpStatusCode} from "../../utils/client/httpStatusCode.js";
import {ApiClientResponse} from "../../interfaces.js";

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

export type Api = {
/**
* Returns a multiproof of `jsonPaths` at the requested `stateId`.
* Returns a multiproof of `descriptor` at the requested `stateId`.
* The requested `stateId` may not be available. Regular nodes only keep recent states in memory.
*/
getStateProof(
stateId: string,
jsonPaths: JsonPath[]
descriptor: Uint8Array
): Promise<ApiClientResponse<{[HttpStatusCode.OK]: {data: Proof}}>>;
/**
* Returns a multiproof of `descriptor` at the requested `blockId`.
* The requested `blockId` may not be available. Regular nodes only keep recent states in memory.
*/
getBlockProof(
blockId: string,
descriptor: Uint8Array
): Promise<ApiClientResponse<{[HttpStatusCode.OK]: {data: Proof}}>>;
};

Expand All @@ -23,19 +30,26 @@ export type Api = {
*/
export const routesData: RoutesData<Api> = {
getStateProof: {url: "/eth/v0/beacon/proof/state/{state_id}", method: "GET"},
getBlockProof: {url: "/eth/v0/beacon/proof/block/{block_id}", method: "GET"},
};

/* eslint-disable @typescript-eslint/naming-convention */
export type ReqTypes = {
getStateProof: {params: {state_id: string}; query: {paths: string[]}};
getStateProof: {params: {state_id: string}; query: {format: string}};
getBlockProof: {params: {block_id: string}; query: {format: string}};
};

export function getReqSerializers(): ReqSerializers<Api, ReqTypes> {
return {
getStateProof: {
writeReq: (state_id, paths) => ({params: {state_id}, query: {paths: querySerializeProofPathsArr(paths)}}),
parseReq: ({params, query}) => [params.state_id, queryParseProofPathsArr(query.paths)],
schema: {params: {state_id: Schema.StringRequired}, body: Schema.AnyArray},
writeReq: (state_id, descriptor) => ({params: {state_id}, query: {format: toHexString(descriptor)}}),
parseReq: ({params, query}) => [params.state_id, fromHexString(query.format)],
schema: {params: {state_id: Schema.StringRequired}, query: {format: Schema.StringRequired}},
},
getBlockProof: {
writeReq: (block_id, descriptor) => ({params: {block_id}, query: {format: toHexString(descriptor)}}),
parseReq: ({params, query}) => [params.block_id, fromHexString(query.format)],
schema: {params: {block_id: Schema.StringRequired}, query: {format: Schema.StringRequired}},
},
};
}
Expand All @@ -44,5 +58,6 @@ export function getReturnTypes(): ReturnTypes<Api> {
return {
// Just sent the proof JSON as-is
getStateProof: sameType(),
getBlockProof: sameType(),
};
}
23 changes: 21 additions & 2 deletions packages/api/src/beacon/server/proof.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {IChainForkConfig} from "@lodestar/config";
import {serializeProof} from "@chainsafe/persistent-merkle-tree";
import {CompactMultiProof} from "@chainsafe/persistent-merkle-tree";
import {Api, ReqTypes, routesData, getReturnTypes, getReqSerializers} from "../routes/proof.js";
import {ServerRoutes, getGenericJsonServer} from "../../utils/server/index.js";
import {ServerApi} from "../../interfaces.js";
Expand All @@ -19,8 +19,27 @@ export function getRoutes(config: IChainForkConfig, api: ServerApi<Api>): Server
handler: async (req) => {
const args = reqSerializers.getStateProof.parseReq(req);
const {data} = await api.getStateProof(...args);
const leaves = (data as CompactMultiProof).leaves;
const response = new Uint8Array(32 * leaves.length);
for (let i = 0; i < leaves.length; i++) {
response.set(leaves[i], i * 32);
}
// Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer
return Buffer.from(serializeProof(data));
return Buffer.from(response);
},
},
getBlockProof: {
...serverRoutes.getBlockProof,
handler: async (req) => {
const args = reqSerializers.getBlockProof.parseReq(req);
const {data} = await api.getBlockProof(...args);
const leaves = (data as CompactMultiProof).leaves;
const response = new Uint8Array(32 * leaves.length);
for (let i = 0; i < leaves.length; i++) {
response.set(leaves[i], i * 32);
}
// Fastify 3.x.x will automatically add header `Content-Type: application/octet-stream` if Buffer
return Buffer.from(response);
},
},
};
Expand Down
33 changes: 18 additions & 15 deletions packages/api/test/unit/beacon/testData/proofs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,33 @@ import {Api} from "../../../../src/beacon/routes/proof.js";
import {GenericServerTestCases} from "../../../utils/genericServerTest.js";

const root = Uint8Array.from(Buffer.alloc(32, 1));
const descriptor = Uint8Array.from([0, 0, 0, 0]);

export const testData: GenericServerTestCases<Api> = {
getStateProof: {
args: [
"head",
[
// ["validator", 0, "balance"],
["finalized_checkpoint", 0, "root", 12000],
],
],
args: ["head", descriptor],
res: {
data: {
type: ProofType.treeOffset,
offsets: [1, 2, 3],
type: ProofType.compactMulti,
descriptor,
leaves: [root, root, root, root],
},
},
/* eslint-disable quotes */
query: {
paths: [
// '["validator",0,"balance"]',
'["finalized_checkpoint",0,"root",12000]',
],
format: "0x00000000",
},
},
getBlockProof: {
args: ["head", descriptor],
res: {
data: {
type: ProofType.compactMulti,
descriptor,
leaves: [root, root, root, root],
},
},
query: {
format: "0x00000000",
},
/* eslint-enable quotes */
},
};
4 changes: 2 additions & 2 deletions packages/beacon-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,9 @@
"@chainsafe/discv5": "^3.0.0",
"@chainsafe/libp2p-gossipsub": "^6.1.0",
"@chainsafe/libp2p-noise": "^11.0.0",
"@chainsafe/persistent-merkle-tree": "^0.4.2",
"@chainsafe/persistent-merkle-tree": "^0.5.0",
"@chainsafe/snappy-stream": "^5.1.2",
"@chainsafe/ssz": "^0.9.2",
"@chainsafe/ssz": "^0.10.1",
"@chainsafe/threads": "^1.10.0",
"@ethersproject/abi": "^5.0.0",
"@libp2p/bootstrap": "^6.0.0",
Expand Down
36 changes: 22 additions & 14 deletions packages/beacon-node/src/api/impl/proof/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {routes, ServerApi} from "@lodestar/api";
import {ProofType, Tree} from "@chainsafe/persistent-merkle-tree";
import {createProof, ProofType} from "@chainsafe/persistent-merkle-tree";
import {ApiModules} from "../types.js";
import {resolveStateId} from "../beacon/state/utils.js";
import {resolveBlockId} from "../beacon/blocks/utils.js";
import {IApiOptions} from "../../options.js";

export function getProofApi(
Expand All @@ -13,29 +14,36 @@ export function getProofApi(
const maxGindicesInProof = opts.maxGindicesInProof ?? 512;

return {
async getStateProof(stateId, jsonPaths) {
async getStateProof(stateId, descriptor) {
const {state} = await resolveStateId(config, chain, db, stateId);

// Commit any changes before computing the state root. In normal cases the state should have no changes here
state.commit();
const stateNode = state.node;
const tree = new Tree(stateNode);

const gindexes = state.type.tree_createProofGindexes(stateNode, jsonPaths);
// TODO: Is it necessary to de-duplicate?
// It's not a problem if we overcount gindexes
const gindicesSet = new Set(gindexes);
const data = createProof(stateNode, {type: ProofType.compactMulti, descriptor});

if (gindicesSet.size > maxGindicesInProof) {
// descriptor.length / 2 is a rough approximation of # of gindices
if (descriptor.length / 2 > maxGindicesInProof) {
throw new Error("Requested proof is too large.");
}

return {
data: tree.getProof({
type: ProofType.treeOffset,
gindices: Array.from(gindicesSet),
}),
};
return {data};
},
async getBlockProof(blockId, descriptor) {
const {block} = await resolveBlockId(chain.forkChoice, db, blockId);

// Commit any changes before computing the state root. In normal cases the state should have no changes here
const blockNode = config.getForkTypes(block.message.slot).BeaconBlock.toView(block.message).node;

const data = createProof(blockNode, {type: ProofType.compactMulti, descriptor});

// descriptor.length / 2 is a rough approximation of # of gindices
if (descriptor.length / 2 > maxGindicesInProof) {
wemeetagain marked this conversation as resolved.
Show resolved Hide resolved
throw new Error("Requested proof is too large.");
}

return {data};
},
};
}
6 changes: 4 additions & 2 deletions packages/beacon-node/test/e2e/chain/lightclient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {expect} from "chai";
import {IChainConfig} from "@lodestar/config";
import {ssz, altair} from "@lodestar/types";
import {JsonPath, toHexString, fromHexString} from "@chainsafe/ssz";
import {TreeOffsetProof} from "@chainsafe/persistent-merkle-tree";
import {computeDescriptor, TreeOffsetProof} from "@chainsafe/persistent-merkle-tree";
import {TimestampFormatCode} from "@lodestar/utils";
import {EPOCHS_PER_SYNC_COMMITTEE_PERIOD, SLOTS_PER_EPOCH} from "@lodestar/params";
import {Lightclient} from "@lodestar/light-client";
Expand Down Expand Up @@ -194,7 +194,9 @@ async function getHeadStateProof(
): Promise<{proof: TreeOffsetProof; header: altair.LightClientHeader}> {
const header = lightclient.getHead();
const stateId = toHexString(header.beacon.stateRoot);
const res = await api.proof.getStateProof(stateId, paths);
const gindices = paths.map((path) => ssz.bellatrix.BeaconState.getPathInfo(path).gindex);
const descriptor = computeDescriptor(gindices);
const res = await api.proof.getStateProof(stateId, descriptor);
ApiError.assert(res);
return {
proof: res.response.data as TreeOffsetProof,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"@chainsafe/bls-keystore": "^2.0.0",
"@chainsafe/blst": "^0.2.8",
"@chainsafe/discv5": "^3.0.0",
"@chainsafe/ssz": "^0.9.2",
"@chainsafe/ssz": "^0.10.1",
"@libp2p/peer-id-factory": "^2.0.1",
"@lodestar/api": "^1.4.3",
"@lodestar/beacon-node": "^1.4.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
"blockchain"
],
"dependencies": {
"@chainsafe/ssz": "^0.9.2",
"@chainsafe/ssz": "^0.10.1",
"@lodestar/params": "^1.4.3",
"@lodestar/types": "^1.4.3"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/db/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/ssz": "^0.9.2",
"@chainsafe/ssz": "^0.10.1",
"@lodestar/config": "^1.4.3",
"@lodestar/utils": "^1.4.3",
"@types/levelup": "^4.3.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/fork-choice/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"check-readme": "typescript-docs-verifier"
},
"dependencies": {
"@chainsafe/ssz": "^0.9.2",
"@chainsafe/ssz": "^0.10.1",
"@lodestar/config": "^1.4.3",
"@lodestar/params": "^1.4.3",
"@lodestar/state-transition": "^1.4.3",
Expand Down
4 changes: 2 additions & 2 deletions packages/light-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@
},
"dependencies": {
"@chainsafe/bls": "7.1.1",
"@chainsafe/persistent-merkle-tree": "^0.4.2",
"@chainsafe/ssz": "^0.9.2",
"@chainsafe/persistent-merkle-tree": "^0.5.0",
"@chainsafe/ssz": "^0.10.1",
"@lodestar/api": "^1.4.3",
"@lodestar/config": "^1.4.3",
"@lodestar/params": "^1.4.3",
Expand Down
11 changes: 7 additions & 4 deletions packages/light-client/test/mocks/LightclientServerApiMock.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {concat} from "uint8arrays";
import {digest} from "@chainsafe/as-sha256";
import {Proof} from "@chainsafe/persistent-merkle-tree";
import {JsonPath} from "@chainsafe/ssz";
import {createProof, Proof, ProofType} from "@chainsafe/persistent-merkle-tree";
import {routes, ServerApi} from "@lodestar/api";
import {altair, RootHex, SyncPeriod} from "@lodestar/types";
import {notNullish} from "@lodestar/utils";
Expand All @@ -11,10 +10,14 @@ import {BeaconStateAltair} from "../utils/types.js";
export class ProofServerApiMock implements ServerApi<routes.proof.Api> {
readonly states = new Map<RootHex, BeaconStateAltair>();

async getStateProof(stateId: string, paths: JsonPath[]): Promise<{data: Proof}> {
async getStateProof(stateId: string, descriptor: Uint8Array): Promise<{data: Proof}> {
const state = this.states.get(stateId);
if (!state) throw Error(`stateId ${stateId} not available`);
return {data: state.createProof(paths)};
return {data: createProof(state.node, {type: ProofType.compactMulti, descriptor})};
}

async getBlockProof(blockId: string, _descriptor: Uint8Array): Promise<{data: Proof}> {
throw Error(`blockId ${blockId} not available`);
}
}

Expand Down
Loading