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

feat: add support for browser bundle for lightclient #6673

Merged
merged 27 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7ed55ae
Reorganize the code so it is accessible from one package
nazarhussain Apr 16, 2024
c1e138c
Add support for browser build for lightclient
nazarhussain Apr 16, 2024
9407a08
Update the build config
nazarhussain Apr 17, 2024
d06c336
Improve the bls vite plugin
nazarhussain Apr 17, 2024
6048592
Restructure the vite and vitest scripts
nazarhussain Apr 17, 2024
e447b9e
Simplify vite config
nazarhussain Apr 17, 2024
21e9c50
Remove unused polyfill
nazarhussain Apr 17, 2024
60e36a4
Fix the doc lint error
nazarhussain Apr 17, 2024
d803c0e
Add support for bundle test
nazarhussain Apr 17, 2024
cae32b2
Update the package json files
nazarhussain Apr 17, 2024
b3831a7
Add dist build to default build task
nazarhussain Apr 17, 2024
38bcba0
Fix spelling in the docs
nazarhussain Apr 17, 2024
202cc3c
Fix the lint error
nazarhussain Apr 17, 2024
2557e10
Fix type error
nazarhussain Apr 17, 2024
cfc03a7
Disable eslint errors
nazarhussain Apr 17, 2024
fa9d5d5
Increase the timeout for bundle test
nazarhussain Apr 17, 2024
3b6a171
Fix eslint bundle
nazarhussain Apr 17, 2024
ca35ddb
Fix lint warning
nazarhussain Apr 17, 2024
ba5886a
Remove the unused config
nazarhussain Apr 18, 2024
2451dc1
Add the default export to bundle
nazarhussain Apr 19, 2024
d800cb6
Enable compression on th build
nazarhussain Apr 19, 2024
ebd79e3
Update packages/light-client/README.md
nazarhussain Apr 19, 2024
0929b36
Increase timeout for one test
nazarhussain Apr 19, 2024
bf57160
Merge branch 'nh/lightclient-browser-build' of github.com:ChainSafe/l…
nazarhussain Apr 19, 2024
0b74f5d
Merge branch 'unstable' into nh/lightclient-browser-build
nazarhussain Apr 26, 2024
7e539ad
Optimize package build task
nazarhussain Apr 26, 2024
6d5cec7
Update the readme
nazarhussain Apr 26, 2024
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
9 changes: 8 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,14 @@ module.exports = {
},
overrides: [
{
files: ["**/*.config.js", "**/*.config.mjs", "**/*.config.cjs", "**/*.config.ts"],
files: [
"**/*.config.js",
"**/*.config.mjs",
"**/*.config.cjs",
"**/*.config.ts",
"scripts/vitest/**/*.ts",
"scripts/vite/**/*.ts",
],
rules: {
"@typescript-eslint/naming-convention": "off",
// Allow require in CJS modules
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.__testdb*
node_modules/
lib
dist
.nyc_output/
coverage/**
.DS_Store
Expand All @@ -21,6 +22,7 @@ validators
**/coverage
**/node_modules
**/lib
**/dist
**/.nyc_output
.tmp
.vscode
Expand Down
4 changes: 4 additions & 0 deletions .wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ blockRoot
blockchain
bootnode
bootnodes
bundlers
chainConfig
chainsafe
chiado
Expand Down Expand Up @@ -190,11 +191,14 @@ testnets
todo
typesafe
udp
unpkg
util
utils
validator
validators
vite
vitest
webpack
wip
xcode
yaml
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
"@types/node": "^20.11.28",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"@vitest/coverage-v8": "^1.4.0",
"@vitest/browser": "^1.4.0",
"@vitest/coverage-v8": "^1.5.0",
"@vitest/browser": "^1.5.0",
"crypto-browserify": "^3.12.0",
"dotenv": "^16.4.5",
"electron": "^26.2.2",
Expand All @@ -71,6 +71,7 @@
"path-browserify": "^1.0.1",
"prettier": "^3.2.5",
"process": "^0.11.10",
"rollup-plugin-visualizer": "^5.12.0",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
"supertest": "^6.3.3",
Expand All @@ -79,7 +80,8 @@
"typescript-docs-verifier": "^2.5.0",
"vite-plugin-node-polyfills": "^0.21.0",
"vite-plugin-top-level-await": "^1.4.1",
"vitest": "^1.4.0",
"vite": "^5.2.9",
"vitest": "^1.5.0",
"vitest-when": "^0.3.1",
"wait-port": "^1.1.0",
"webdriverio": "^8.34.1"
Expand All @@ -88,6 +90,6 @@
"@puppeteer/browsers": "^2.1.0",
"dns-over-http-resolver": "^2.1.1",
"loupe": "^2.3.6",
"vite": "^5.0.0"
"vite": "^5.2.9"
}
}
2 changes: 1 addition & 1 deletion packages/beacon-node/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {defineConfig, mergeConfig} from "vitest/config";
import {buildTargetPlugin} from "../../scripts/vitest/plugins/buildTargetPlugin.js";
import {buildTargetPlugin} from "../../scripts/vite/plugins/buildTargetPlugin.js";
import vitestConfig from "../../vitest.base.unit.config";

export default mergeConfig(
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/vitest.e2e.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {defineConfig, mergeConfig} from "vitest/config";
import {buildTargetPlugin} from "../../scripts/vitest/plugins/buildTargetPlugin";
import {buildTargetPlugin} from "../../scripts/vite/plugins/buildTargetPlugin";
import vitestConfig from "../../vitest.base.e2e.config";

export default mergeConfig(
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/vitest.spec.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {defineConfig, mergeConfig} from "vitest/config";
import {buildTargetPlugin} from "../../scripts/vitest/plugins/buildTargetPlugin";
import {buildTargetPlugin} from "../../scripts/vite/plugins/buildTargetPlugin";
import vitestConfig from "../../vitest.base.spec.config";

export default mergeConfig(
Expand Down
46 changes: 37 additions & 9 deletions packages/light-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,22 @@ lodestar lightclient \

## Light-Client Programmatic Example

For this example we will assume there is a running beacon node at `https://beacon-node.your-domain.com`
For this example we will assume there is a running beacon node at `https://lodestar-mainnet.chainsafe.io`
Copy link
Member

Choose a reason for hiding this comment

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

Are we sure we want to broadcast this URL? @philknows @wemeetagain

Copy link
Contributor Author

@nazarhussain nazarhussain Apr 26, 2024

Choose a reason for hiding this comment

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

These URLs are already been shown and used in the light client demo, so if one want's to use they would have already gotten it from the demo page.

image

If anyone have different opinion we can revert it in other PR.


```ts
import {getClient} from "@lodestar/api";
import {createChainForkConfig} from "@lodestar/config";
import {networksChainConfig} from "@lodestar/config/networks";
import {Lightclient, LightclientEvent} from "@lodestar/light-client";
import {LightClientRestTransport} from "@lodestar/light-client/transport";
import {getFinalizedSyncCheckpoint, getGenesisData, getLcLoggerConsole} from "@lodestar/light-client/utils";

const config = createChainForkConfig(networksChainConfig.mainnet);
const logger = getLcLoggerConsole({logDebug: Boolean(process.env.DEBUG)});
const api = getClient({urls: ["https://beacon-node.your-domain.com"]}, {config});
import {
getFinalizedSyncCheckpoint,
getGenesisData,
getConsoleLogger,
getApiFromUrl,
getChainForkConfigFromNetwork,
} from "@lodestar/light-client/utils";

const config = getChainForkConfigFromNetwork("mainnet");
const logger = getConsoleLogger({logDebug: Boolean(process.env.DEBUG)});
const api = getApiFromUrl({urls: ["https://lodestar-mainnet.chainsafe.io"]}, {config});
Copy link
Member

Choose a reason for hiding this comment

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

Same question about using our real URL

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to have an example which people can run by just copying the code. We have similar pattern in the prover already but pointing to sepolia url.

Copy link
Member

Choose a reason for hiding this comment

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

I think that is a fine idea.... just not our mainnet nodes


const lightclient = await Lightclient.initializeFromCheckpointRoot({
config,
Expand Down Expand Up @@ -88,6 +91,31 @@ lightclient.emitter.on(LightclientEvent.lightClientOptimisticHeader, async (opti
});
```

## Browser Integration

If you want to use Lightclient in browser and facing some issues in building it with bundlers like webpack, vite. We suggest to use our distribution build. The support for single distribution build is started from `1.18.0` version.
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved

Directly link the dist build with the `<script />` tag with tools like unpkg or other. e.g.

```html
<script src="https://www.unpkg.com/@lodestar/light-client@1.18.0/dist/lightclient.es.min.js" type="module">
```

Then the lightclient package will be exposed to `globalThis`, in case of browser environment that will be `window`. You can access the package as `window.lodestar.lightclient`. All named exports will also be available from this interface. e.g. `window.lodestar.lightclient.transport`.

NOTE: Due to `top-level-await` used in one of dependent library, the package will not be available right after the load. You have to use a hack to clear up that await from the event loop.

```html
Copy link
Contributor

Choose a reason for hiding this comment

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

Would be nice to find a way to remove the need for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's in the bls library. @matthewkeil is working on a newer version. Hope that will solve this problem.

Copy link
Member

Choose a reason for hiding this comment

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

it will not. there will always be a top level await in esm because the bindings path is programmatic so its not imported it looked up and then await import(bindingLocation)

Copy link
Member

Choose a reason for hiding this comment

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

i suppose it would be possible in bls to use hard imports and then null-loader from weback to not include the other version for web. But for blst if we make it esm it will always have the top-level await for the import

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's possible avoid top-level-import, but then would have to leave upto user of the library to init where appropriate.

Copy link
Member

Choose a reason for hiding this comment

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

it will not. there will always be a top level await in esm because the bindings path is programmatic so its not imported it looked up and then await import(bindingLocation)

I think there is a difference between using top-level await and a ESM module without it. Eg. Node 22 will ship a new feature to require ES modules with the exception of the ones that use top-level await (see nodejs/node#51977).

In any case, we might wanna avoid top-level await in packages that are used by others.

Copy link
Member

Choose a reason for hiding this comment

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

I made the top-level await go away in blst fyi. so might be able to also do something similar in bls...

https://github.com/ChainSafe/blst-ts/blob/37b13881307ee4eb8fb8a4a84f6cafccfde5293c/lib/index.mjs#L7-10

<script>
window.addEventListener("DOMContentLoaded", () => {
setTimeout(function () {
// here you can access the Lightclient
// window.lodestar.lightclient
}, 50);
});
</script>
```

## Contributors

Read our [contribution documentation](https://chainsafe.github.io/lodestar/contribution/getting-started), [submit an issue](https://github.com/ChainSafe/lodestar/issues/new/choose) or talk to us on our [discord](https://discord.gg/yjyvFRP)!
Expand Down
14 changes: 11 additions & 3 deletions packages/light-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,21 @@
"lib/**/*.js",
"lib/**/*.js.map",
"*.d.ts",
"*.js"
"*.js",
"dist/**/*.js",
"dist/**/*.mjs",
"dist/**/*.cjs",
"dist/**/*.map",
"dist/**/*.json",
"dist/**/*.d.ts"
],
"scripts": {
"clean": "rm -rf lib && rm -f *.tsbuildinfo",
"build": "tsc -p tsconfig.build.json",
"build": "yarn run build:lib && yarn run build:dist",
"build:lib": "tsc -p tsconfig.build.json",
"build:dist": "vite build",
"build:watch": "yarn run build --watch",
"build:release": "yarn clean && yarn run build",
"build:release": "yarn clean && yarn run build && yarn run build:dist",
"check-build": "node -e \"(async function() { await import('./lib/index.js') })()\"",
"check-types": "tsc",
"lint": "eslint --color --ext .ts src/ test/",
Expand Down
7 changes: 7 additions & 0 deletions packages/light-client/src/index.browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from "./index.js";

// To kep the consistent interface as npm package and browser
// Use consistent names as named exports
export * as utils from "./utils/index.js";
export * as validation from "./validation.js";
export * as transport from "./transport/index.js";
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions packages/light-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {isErrorAborted, sleep} from "@lodestar/utils";
import {getCurrentSlot, slotWithFutureTolerance, timeUntilNextEpoch} from "./utils/clock.js";
import {chunkifyInclusiveRange} from "./utils/chunkify.js";
import {LightclientEmitter, LightclientEvent} from "./events.js";
import {getLcLoggerConsole, ILcLogger} from "./utils/logger.js";
import {getConsoleLogger, ILcLogger} from "./utils/logger.js";
import {computeSyncPeriodAtEpoch, computeSyncPeriodAtSlot, computeEpochAtSlot} from "./utils/clock.js";
import {LightclientSpec} from "./spec/index.js";
import {validateLightClientBootstrap} from "./spec/validateLightClientBootstrap.js";
Expand Down Expand Up @@ -114,7 +114,7 @@ export class Lightclient {
: genesisData.genesisValidatorsRoot;

this.config = createBeaconConfig(config, this.genesisValidatorsRoot);
this.logger = logger ?? getLcLoggerConsole();
this.logger = logger ?? getConsoleLogger();
this.transport = transport;
this.runStatus = {code: RunStatusCode.uninitialized};

Expand Down
19 changes: 19 additions & 0 deletions packages/light-client/src/utils/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {getClient, Api} from "@lodestar/api";
import {ChainForkConfig, createChainForkConfig} from "@lodestar/config";
import {NetworkName, networksChainConfig} from "@lodestar/config/networks";

export function getApiFromUrl(url: string, network: NetworkName): Api {
if (!(network in networksChainConfig)) {
throw Error(`Invalid network name "${network}". Valid options are: ${Object.keys(networksChainConfig).join()}`);
}

return getClient({urls: [url]}, {config: createChainForkConfig(networksChainConfig[network])});
}

export function getChainForkConfigFromNetwork(network: NetworkName): ChainForkConfig {
if (!(network in networksChainConfig)) {
throw Error(`Invalid network name "${network}". Valid options are: ${Object.keys(networksChainConfig).join()}`);
}

return createChainForkConfig(networksChainConfig[network]);
}
1 change: 1 addition & 0 deletions packages/light-client/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./api.js";
export * from "./chunkify.js";
export * from "./clock.js";
export * from "./domain.js";
Expand Down
7 changes: 6 additions & 1 deletion packages/light-client/src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,16 @@ export type ILcLogger = {
/**
* With `console` module and ignoring debug logs
*/
export function getLcLoggerConsole(opts?: {logDebug?: boolean}): ILcLogger {
export function getConsoleLogger(opts?: {logDebug?: boolean}): ILcLogger {
return {
error: console.error,
warn: console.warn,
info: console.log,
debug: opts?.logDebug ? console.log : () => {},
};
}

/**
* @deprecated - Use `getConsoleLogger` instead.
*/
export const getLcLoggerConsole = getConsoleLogger;
63 changes: 63 additions & 0 deletions packages/light-client/test/unit/webEsmBundle.browser.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call */
import {expect, describe, it, beforeEach, vi} from "vitest";
import "../../dist/lightclient.min.mjs";

describe("web bundle for lightclient", () => {
vi.setConfig({testTimeout: 10_000});

let lightclient: any;

beforeEach(() => {
lightclient = (window as any)["lodestar"]["lightclient"];
});

it("should have a global interface", () => {
expect(lightclient).toBeDefined();
});

it("should have all relevant exports", () => {
expect(lightclient).toHaveProperty("Lightclient");
expect(lightclient).toHaveProperty("LightclientEvent");
expect(lightclient).toHaveProperty("RunStatusCode");
expect(lightclient).toHaveProperty("upgradeLightClientFinalityUpdate");
expect(lightclient).toHaveProperty("upgradeLightClientOptimisticUpdate");
expect(lightclient).toHaveProperty("utils");
expect(lightclient).toHaveProperty("transport");
expect(lightclient).toHaveProperty("validation");

expect(lightclient.Lightclient).toBeTypeOf("function");
});

it("should start the lightclient and sync", async () => {
const {Lightclient, LightclientEvent, transport, utils} = lightclient;

const logger = utils.getConsoleLogger({logDebug: true});
const config = utils.getChainForkConfigFromNetwork("mainnet");

// TODO: Decide to check which node to use in testing
// We have one node in CI, but that only starts with e2e tests
const api = utils.getApiFromUrl("https://lodestar-mainnet.chainsafe.io", "mainnet");

const lc = await Lightclient.initializeFromCheckpointRoot({
config,
logger,
transport: new transport.LightClientRestTransport(api),
genesisData: await utils.getGenesisData(api),
checkpointRoot: await utils.getFinalizedSyncCheckpoint(api),
opts: {
allowForcedUpdates: true,
updateHeadersOnForcedUpdate: true,
},
});

await expect(lc.start()).resolves.toBeUndefined();

await expect(
new Promise((resolve) => {
lc.emitter.on(LightclientEvent.lightClientOptimisticHeader, async (optimisticUpdate: unknown) => {
resolve(optimisticUpdate);
});
})
).resolves.toBeDefined();
});
});
4 changes: 2 additions & 2 deletions packages/light-client/test/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import {altair, phase0, Slot, ssz, SyncPeriod, allForks} from "@lodestar/types";
import {SyncCommitteeFast} from "../../src/types.js";
import {computeSigningRoot} from "../../src/utils/domain.js";
import {getLcLoggerConsole} from "../../src/utils/logger.js";
import {getConsoleLogger} from "../../src/utils/logger.js";

const CURRENT_SYNC_COMMITTEE_INDEX = 22;
const CURRENT_SYNC_COMMITTEE_DEPTH = 5;
Expand All @@ -25,7 +25,7 @@ const CURRENT_SYNC_COMMITTEE_DEPTH = 5;
* DEBUG=true vitest ...
* ```
*/
export const testLogger = getLcLoggerConsole({logDebug: Boolean(process.env.DEBUG)});
export const testLogger = getConsoleLogger({logDebug: Boolean(process.env.DEBUG)});

export const genesisValidatorsRoot = Buffer.alloc(32, 0xaa);
export const SOME_HASH = Buffer.alloc(32, 0xaa);
Expand Down
1 change: 1 addition & 0 deletions packages/light-client/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"extends": "../../tsconfig.json",
"exclude": ["src/index.browser.ts"],
"compilerOptions": {}
}
29 changes: 29 additions & 0 deletions packages/light-client/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {defineConfig, mergeConfig} from "vite";
import {getBaseViteConfig} from "../../vite.base.config.js";

import pkgJSON from "./package.json";

export default mergeConfig(
getBaseViteConfig(pkgJSON, {libName: "LightClient", entry: "src/index.browser.ts"}),
defineConfig({
build: {
rollupOptions: {
output: {
footer: `
globalThis.lodestar = globalThis.lodestar === undefined ? {} : globalThis.lodestar;
globalThis.lodestar.lightclient = {
Lightclient,
LightclientEvent,
RunStatusCode,
upgradeLightClientFinalityUpdate,
upgradeLightClientOptimisticUpdate,
utils: index$1,
transport: index,
nazarhussain marked this conversation as resolved.
Show resolved Hide resolved
validation
};
`,
},
},
},
})
);
Loading
Loading