diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7d8398e..12315f4e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,14 +1,47 @@ name: Test -on: [push, pull_request] + +on: + pull_request: + push: + branches: + - "**" + tags: + - "v*.*.*" + jobs: + build-swig: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Setup SWIG + run: | + git clone -b pr/new-node-fixes https://github.com/yegorich/swig; + cd swig; + ./autogen.sh; + ./configure --prefix=$HOME/swig; + make; + make install; + - name: Run.me + run: | + env PATH=~/swig/bin:$PATH SWIG_LIB=~/swig/share/swig/4.0.2 \ + ./blst/bindings/node.js/run.me + - name: Upload binding.node + uses: actions/upload-artifact@v2 + with: + name: blst_wrap.cpp + path: blst/bindings/node.js/blst_wrap.cpp + build: + needs: ["build-swig"] runs-on: ${{matrix.os}} strategy: fail-fast: false matrix: # windows-latest disabled by now, unable to build os: [ubuntu-latest, macos-latest] - node: [12, 14] + node: [10, 11, 12, 13, 14] steps: - uses: actions/checkout@v2 with: @@ -17,7 +50,56 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{matrix.node}} - - name: Install & Build - run: yarn install + + - name: Get SWIG pre-built + uses: actions/download-artifact@v2 + with: + name: blst_wrap.cpp + path: prebuild + + - name: Install && Build TS + bindings + run: yarn bootstrap - name: Test run: yarn test + + - name: Upload binding.node + uses: actions/upload-artifact@v2 + if: github.repository_owner == 'chainsafe' && github.event_name != 'pull_request' + with: + name: binding.node + path: build/ + + publish: + needs: ["build-swig", "build"] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + - name: Get SWIG pre-built + uses: actions/download-artifact@v2 + with: + name: blst_wrap.cpp + path: prebuild + - name: Get binding.node pre-builts + uses: actions/download-artifact@v2 + with: + name: binding.node + path: prebuild + + - name: Install && Build TS + bindings + run: yarn bootstrap + + - name: Create Github release with prebuilds + uses: softprops/action-gh-release@v1 + with: + files: prebuild/* + fail_on_unmatched_files: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # - name: Publish to NPM + # run: npm publish --access public + # env: + # NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 3ec7bd10..ad094d4b 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,4 @@ node_modules prebuilds npm-debug.log -yarn-error.log \ No newline at end of file +yarn-error.log diff --git a/build.node.sh b/build.node.sh index c7022995..db0c77aa 100755 --- a/build.node.sh +++ b/build.node.sh @@ -1,13 +1,13 @@ #!/bin/sh -e -BLST_WRAP_PREBUILD=blst_wrap.cpp -BLST_WRAP_OUTPUT=blst/bindings/node.js BLST_NODE_OUTPUT=blst/bindings/node.js/blst.node BLST_NODE_TARGET=dist/blst.node -cp $BLST_WRAP_PREBUILD $BLST_WRAP_OUTPUT +# Copy SWIG prebuilt +cp blst_wrap.cpp blst/bindings/node.js -(cd blst/bindings/node.js; ./run.me) +# Build node bindings +./blst/bindings/node.js/run.me mkdir -p `dirname $BLST_NODE_TARGET` cp $BLST_NODE_OUTPUT $BLST_NODE_TARGET diff --git a/package.json b/package.json index dea61c20..0cd18812 100644 --- a/package.json +++ b/package.json @@ -8,9 +8,10 @@ "dist" ], "scripts": { - "install": "./build.node.sh", + "install": "node dist/scripts/install.js", "test": "mocha test/**/*", - "build": "tsc" + "build": "tsc", + "bootstrap": "yarn install --ignore-scripts && yarn build && yarn install" }, "repository": { "type": "git", diff --git a/blst_wrap.cpp b/prebuild/blst_wrap.cpp similarity index 100% rename from blst_wrap.cpp rename to prebuild/blst_wrap.cpp diff --git a/src/bindings.ts b/src/bindings.ts index 1c18afc8..e56581b7 100644 --- a/src/bindings.ts +++ b/src/bindings.ts @@ -1,4 +1,5 @@ -export const blst: Blst = require("../build/blst"); +import { getBinaryPath } from "./scripts/paths"; +export const blst: Blst = require(getBinaryPath()); export interface Blst { SecretKey: SecretKeyConstructor; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..8cd5167d --- /dev/null +++ b/src/index.ts @@ -0,0 +1 @@ +export * from "./lib"; diff --git a/src/lib.ts b/src/lib.ts index c96a39c4..81577baa 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -162,25 +162,29 @@ export class AggregateSignature { } } -export function verify(msg: Uint8Array, pk: PublicKey, sig: Signature): void { - aggregateVerify([msg], [pk], sig); +export function verify( + msg: Uint8Array, + pk: PublicKey, + sig: Signature +): boolean { + return aggregateVerify([msg], [pk], sig); } export function fastAggregateVerify( msg: Uint8Array, pks: PublicKey[], sig: Signature -): void { +): boolean { const aggPk = AggregatePublicKey.fromPublicKeys(pks); const pk = aggPk.toPublicKey(); - aggregateVerify([msg], [pk], sig); + return aggregateVerify([msg], [pk], sig); } export function aggregateVerify( msgs: Uint8Array[], pks: PublicKey[], sig: Signature -): void { +): boolean { const n_elems = pks.length; if (msgs.length !== n_elems) { throw new ErrorBLST(BLST_ERROR.BLST_VERIFY_FAIL); @@ -198,9 +202,7 @@ export function aggregateVerify( // PT constructor calls `blst_aggregated` const gtsig = new blst.PT(sig.value); - if (!ctx.finalverify(gtsig)) { - throw new ErrorBLST(BLST_ERROR.BLST_VERIFY_FAIL); - } + return ctx.finalverify(gtsig); } // https://ethresear.ch/t/fast-verification-of-multiple-bls-signatures/5407 @@ -208,7 +210,7 @@ export function verifyMultipleAggregateSignatures( msgs: Uint8Array[], pks: PublicKey[], sigs: Signature[] -): void { +): boolean { const n_elems = pks.length; if (msgs.length !== n_elems || sigs.length !== n_elems) { throw new ErrorBLST(BLST_ERROR.BLST_VERIFY_FAIL); @@ -230,8 +232,5 @@ export function verifyMultipleAggregateSignatures( } ctx.commit(); - - if (!ctx.finalverify()) { - throw new ErrorBLST(BLST_ERROR.BLST_VERIFY_FAIL); - } + return ctx.finalverify(); } diff --git a/src/scripts/buildBindings.ts b/src/scripts/buildBindings.ts new file mode 100644 index 00000000..2098f41b --- /dev/null +++ b/src/scripts/buildBindings.ts @@ -0,0 +1,40 @@ +import fs from "fs"; +import { exec } from "child_process"; +import { testBindings } from "./testBindings"; +import { + bindingsDirSrc, + bindingsSrc, + ensureDirFromFilepath, + prebuiltSwigSrc, + prebuiltSwigTarget, +} from "./paths"; + +export async function buildBindings(binaryPath: string) { + // Copy SWIG prebuilt + fs.copyFileSync(prebuiltSwigSrc, prebuiltSwigTarget); + + // Use BLST run.me script to build libblst.a + blst.node + await new Promise((resolve, reject): void => { + const proc = exec( + "./run.me", + { + timeout: 3 * 60 * 1000, // ms + maxBuffer: 10e6, // bytes + cwd: bindingsDirSrc, + }, + (err, stdout, stderr) => { + if (err) reject(err); + else resolve(stdout.trim() || stderr); + } + ); + if (proc.stdout) proc.stdout.pipe(process.stdout); + if (proc.stderr) proc.stderr.pipe(process.stderr); + }); + + // Copy built .node file to expected path + ensureDirFromFilepath(binaryPath); + fs.copyFileSync(bindingsSrc, binaryPath); + + // Make sure downloaded bindings work + await testBindings(binaryPath); +} diff --git a/src/scripts/downloadBindings.ts b/src/scripts/downloadBindings.ts new file mode 100644 index 00000000..70032d87 --- /dev/null +++ b/src/scripts/downloadBindings.ts @@ -0,0 +1,20 @@ +import { testBindings } from "./testBindings"; +import { download } from "./downloadFile"; +import { ensureDirFromFilepath, getBinaryName, packageJsonPath } from "./paths"; + +const githubReleasesDownloadUrl = + "https://github.com/ChainSafe/blst-ts/releases/download"; + +export async function checkAndDownloadBinary(binaryPath: string) { + const packageJson = require(packageJsonPath); + const binaryName = getBinaryName(); + const version = packageJson.version; + + const binaryUrl = `${githubReleasesDownloadUrl}/v${version}/${binaryName}`; + + ensureDirFromFilepath(binaryPath); + await download(binaryUrl, binaryPath); + + // Make sure downloaded bindings work + await testBindings(binaryPath); +} diff --git a/src/scripts/downloadFile.ts b/src/scripts/downloadFile.ts new file mode 100644 index 00000000..0280474d --- /dev/null +++ b/src/scripts/downloadFile.ts @@ -0,0 +1,47 @@ +import fs from "fs"; +import https from "https"; + +export class HttpError extends Error { + statusCode?: number; + constructor(message: string, statusCode?: number) { + super(message); + this.statusCode = statusCode; + } +} + +/** + * Downloads file from remote HTTP[S] host and puts its contents to the + * specified location. + */ +export function download(url: string, filePath: string): Promise { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(filePath); + + const request = https.get(url, (response) => { + if (response.statusCode !== 200) { + reject( + new HttpError( + `Failed to get '${url}' (${response.statusCode})`, + response.statusCode + ) + ); + return; + } + + response.pipe(file); + }); + + // The destination stream is ended by the time it's called + file.on("finish", () => resolve()); + + request.on("error", (err) => { + fs.unlink(filePath, () => reject(err)); + }); + + file.on("error", (err) => { + fs.unlink(filePath, () => reject(err)); + }); + + request.end(); + }); +} diff --git a/src/scripts/install.ts b/src/scripts/install.ts new file mode 100644 index 00000000..fdca1318 --- /dev/null +++ b/src/scripts/install.ts @@ -0,0 +1,60 @@ +import fs from "fs"; +import { checkAndDownloadBinary } from "./downloadBindings"; +import { buildBindings } from "./buildBindings"; +import { getBinaryPath } from "./paths"; +import { testBindings } from "./testBindings"; + +const libName = "BLST native bindings"; + +// CLI runner +install().then( + () => process.exit(0), + (e) => { + console.log(e.stack); + process.exit(1); + } +); + +async function install() { + const binaryPath = getBinaryPath(); + + // Check if bindings already downloaded or built + if (fs.existsSync(binaryPath)) { + try { + await testBindings(binaryPath); + console.log(`Using existing ${libName} from ${binaryPath}`); + return; + } catch (e) { + console.log(`Cached ${libName} not OK`); + } + } + + // Fetch pre-built bindings from remote repo + try { + console.log(`Retrieving ${libName}...`); + await checkAndDownloadBinary(binaryPath); + await testBindings(binaryPath); + console.log(`Successfully retrieved ${libName}`); + return; + } catch (e) { + if (e.statusCode === 404) { + console.error(`${libName} not available: ${e.message}`); + } else { + console.error(`Error importing ${libName}: ${e.stack}`); + } + } + + // Build bindings locally from source + try { + console.log(`Building ${libName} from source...`); + await buildBindings(binaryPath); + await testBindings(binaryPath); + console.log(`Successfully built ${libName} from source`); + return; + } catch (e) { + console.error(`Error building ${libName}: ${e.stack}`); + } + + // Fallback? + throw Error(`Error downloading and building ${libName}. No fallback`); +} diff --git a/src/scripts/paths.ts b/src/scripts/paths.ts new file mode 100644 index 00000000..4ae4a00f --- /dev/null +++ b/src/scripts/paths.ts @@ -0,0 +1,42 @@ +import fs from "fs"; +import path from "path"; + +const rootDir = path.join(__dirname, "../.."); + +export const packageJsonPath = path.join(rootDir, "package.json"); + +export const bindingsDirSrc = path.join(rootDir, "blst/bindings/node.js"); +export const prebuiltSwigSrc = path.join(rootDir, "prebuild/blst_wrap.cpp"); +export const prebuiltSwigTarget = path.join(bindingsDirSrc, "blst_wrap.cpp"); +export const bindingsSrc = path.join(bindingsDirSrc, "blst.node"); + +export const defaultBinaryDir = path.join(rootDir, "build"); + +/** + * Get binary name. + * name: {platform}-{arch}-{v8 version}.node + */ +export function getBinaryName() { + const platform = process.platform; + const arch = process.arch; + const nodeV8CppApiVersion = process.versions.modules; + + return [platform, arch, nodeV8CppApiVersion, "binding.node"].join("-"); +} + +export function getBinaryPath() { + return path.join(defaultBinaryDir, getBinaryName()); +} + +export function mkdirBinary() { + if (!fs.existsSync(defaultBinaryDir)) { + fs.mkdirSync(defaultBinaryDir); + } +} + +export function ensureDirFromFilepath(filepath: string) { + const dirpath = path.dirname(filepath); + if (!fs.existsSync(dirpath)) { + fs.mkdirSync(dirpath, { recursive: true }); + } +} diff --git a/src/scripts/testBindings.ts b/src/scripts/testBindings.ts new file mode 100644 index 00000000..46c35042 --- /dev/null +++ b/src/scripts/testBindings.ts @@ -0,0 +1,3 @@ +export async function testBindings(binaryPath: string): Promise { + require(binaryPath); +}