diff --git a/.release-please-manifest.json b/.release-please-manifest.json index e0a9768aa1..39f303fd5f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -2,6 +2,7 @@ "packages/utils": "0.0.4", "packages/proto": "0.0.4", "packages/interfaces": "0.0.11", + "packages/message-hash": "0.1.0", "packages/enr": "0.0.10", "packages/peer-exchange": "0.0.9", "packages/core": "0.0.16", diff --git a/.size-limit.cjs b/.size-limit.cjs index c74ee498cd..8b277194ce 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -43,4 +43,9 @@ module.exports = [ path: "packages/core/bundle/index.js", import: "{ wakuStore }", }, + { + name: "Deterministic Message Hashing", + path: "packages/message-hash/bundle/index.js", + import: "{ messageHash }", + }, ]; diff --git a/package-lock.json b/package-lock.json index 50270ea3fb..e12185721f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "packages/utils", "packages/proto", "packages/interfaces", + "packages/message-hash", "packages/enr", "packages/core", "packages/peer-exchange", @@ -6107,6 +6108,10 @@ "resolved": "packages/message-encryption", "link": true }, + "node_modules/@waku/message-hash": { + "resolved": "packages/message-hash", + "link": true + }, "node_modules/@waku/peer-exchange": { "resolved": "packages/peer-exchange", "link": true @@ -29587,6 +29592,54 @@ "node": ">=16" } }, + "packages/message-hash": { + "name": "@waku/message-hash", + "version": "0.0.10", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@noble/hashes": "^1.2.0", + "@waku/utils": "*" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.1", + "@types/chai": "^4.3.4", + "@types/debug": "^4.1.7", + "@types/mocha": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^5.54.1", + "@typescript-eslint/parser": "^5.51.0", + "@waku/build-utils": "*", + "@waku/interfaces": "*", + "chai": "^4.3.7", + "cspell": "^6.28.0", + "eslint": "^8.35.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^5.0.4", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^4.2.1", + "fast-check": "^3.7.0", + "ignore-loader": "^0.1.2", + "isomorphic-fetch": "^3.0.0", + "karma": "^6.4.1", + "karma-chrome-launcher": "^3.1.1", + "karma-mocha": "^2.0.1", + "karma-webpack": "^5.0.0", + "mocha": "^10.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.8.4", + "process": "^0.11.10", + "puppeteer": "^19.7.2", + "rollup": "^3.15.0", + "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" + }, + "engines": { + "node": ">=16" + } + }, "packages/peer-exchange": { "name": "@waku/peer-exchange", "version": "0.0.9", @@ -34673,6 +34726,47 @@ "typescript": "^4.9.5" } }, + "@waku/message-hash": { + "version": "file:packages/message-hash", + "requires": { + "@noble/hashes": "^1.2.0", + "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.1", + "@types/chai": "^4.3.4", + "@types/debug": "^4.1.7", + "@types/mocha": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^5.54.1", + "@typescript-eslint/parser": "^5.51.0", + "@waku/build-utils": "*", + "@waku/interfaces": "*", + "@waku/utils": "*", + "chai": "^4.3.7", + "cspell": "^6.28.0", + "eslint": "^8.35.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^5.0.4", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^4.2.1", + "fast-check": "^3.7.0", + "ignore-loader": "^0.1.2", + "isomorphic-fetch": "^3.0.0", + "karma": "^6.4.1", + "karma-chrome-launcher": "^3.1.1", + "karma-mocha": "^2.0.1", + "karma-webpack": "^5.0.0", + "mocha": "^10.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.8.4", + "process": "^0.11.10", + "puppeteer": "^19.7.2", + "rollup": "^3.15.0", + "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" + } + }, "@waku/peer-exchange": { "version": "file:packages/peer-exchange", "requires": { diff --git a/package.json b/package.json index 77b7033cc4..23bd821123 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "packages/interfaces", "packages/enr", "packages/core", + "packages/message-hash", "packages/peer-exchange", "packages/dns-discovery", "packages/message-encryption", diff --git a/packages/message-hash/.eslintrc.cjs b/packages/message-hash/.eslintrc.cjs new file mode 100644 index 0000000000..324f1f526d --- /dev/null +++ b/packages/message-hash/.eslintrc.cjs @@ -0,0 +1,6 @@ +module.exports = { + parserOptions: { + tsconfigRootDir: __dirname, + project: "./tsconfig.dev.json", + }, +}; diff --git a/packages/message-hash/.mocharc.json b/packages/message-hash/.mocharc.json new file mode 100644 index 0000000000..8b0b97b620 --- /dev/null +++ b/packages/message-hash/.mocharc.json @@ -0,0 +1,11 @@ +{ + "extension": ["ts"], + "spec": "src/**/*.spec.ts", + "require": ["ts-node/register", "isomorphic-fetch"], + "loader": "ts-node/esm", + "node-option": [ + "experimental-specifier-resolution=node", + "loader=ts-node/esm" + ], + "exit": true +} diff --git a/packages/message-hash/.prettierignore b/packages/message-hash/.prettierignore new file mode 100644 index 0000000000..d8763f9fc7 --- /dev/null +++ b/packages/message-hash/.prettierignore @@ -0,0 +1,5 @@ +build +bundle +dist +node_modules +CHANGELOG.md diff --git a/packages/message-hash/README.md b/packages/message-hash/README.md new file mode 100644 index 0000000000..4d936787a5 --- /dev/null +++ b/packages/message-hash/README.md @@ -0,0 +1,26 @@ +[![NPM](https://nodei.co/npm/@waku/message-hash.png)](https://npmjs.org/package/@waku/message-hash) + +![GitHub Action](https://img.shields.io/github/workflow/status/waku-org/js-waku/CI) +[![Discord chat](https://img.shields.io/discord/864066763682218004.svg?logo=discord&colorB=7289DA)](https://discord.gg/Nrac59MfSX) + +# @waku/message-hash + +TypeScript implementation of the _Deterministic Message Hashing_ as specified in [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/). + +See [JS-Waku README](https://github.com/waku-org/js-waku) for more information. + +## Contributing + +See [CONTRIBUTING.md](https://github.com/waku-org/js-waku/blob/master/CONTRIBUTING.md). + +## License + +Licensed and distributed under either of + +- MIT license: [LICENSE-MIT](https://github.com/waku-org/js-waku/blob/master/LICENSE-MIT) or http://opensource.org/licenses/MIT + +or + +- Apache License, Version 2.0, ([LICENSE-APACHE-v2](https://github.com/waku-org/js-waku/blob/master/LICENSE-APACHE-v2) or http://www.apache.org/licenses/LICENSE-2.0) + +at your option. These files may not be copied, modified, or distributed except according to those terms. diff --git a/packages/message-hash/karma.conf.cjs b/packages/message-hash/karma.conf.cjs new file mode 100644 index 0000000000..c189f3b1e1 --- /dev/null +++ b/packages/message-hash/karma.conf.cjs @@ -0,0 +1,45 @@ +process.env.CHROME_BIN = require("puppeteer").executablePath(); +const webpack = require("webpack"); + +module.exports = function (config) { + config.set({ + frameworks: ["webpack", "mocha"], + files: ["src/**/!(node).spec.ts"], + preprocessors: { + "src/**/!(node).spec.ts": ["webpack"], + }, + envPreprocessor: ["CI"], + reporters: ["progress"], + browsers: ["ChromeHeadless"], + singleRun: true, + client: { + mocha: { + timeout: 6000, // Default is 2s + }, + }, + webpack: { + mode: "development", + module: { + rules: [{ test: /\.([cm]?ts|tsx)$/, loader: "ts-loader" }], + }, + plugins: [ + new webpack.DefinePlugin({ + "process.env.CI": process.env.CI || false, + }), + new webpack.ProvidePlugin({ + process: "process/browser.js", + }), + ], + resolve: { + extensions: [".ts", ".tsx", ".js"], + extensionAlias: { + ".js": [".js", ".ts"], + ".cjs": [".cjs", ".cts"], + ".mjs": [".mjs", ".mts"], + }, + }, + stats: { warnings: false }, + devtool: "inline-source-map", + }, + }); +}; diff --git a/packages/message-hash/package.json b/packages/message-hash/package.json new file mode 100644 index 0000000000..2ec2476796 --- /dev/null +++ b/packages/message-hash/package.json @@ -0,0 +1,108 @@ +{ + "name": "@waku/message-hash", + "version": "0.1.0", + "description": "TypeScript implementation of the Deterministic Message Hashing as specified in 14/WAKU2-MESSAGE", + "types": "./dist/index.d.ts", + "module": "./dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "type": "module", + "homepage": "https://github.com/waku-org/js-waku/tree/master/packages/message-hash#readme", + "repository": { + "type": "git", + "url": "https://github.com/waku-org/js-waku.git" + }, + "bugs": { + "url": "https://github.com/waku-org/js-waku/issues" + }, + "license": "MIT OR Apache-2.0", + "keywords": [ + "waku", + "decentralised", + "communication", + "web3", + "ethereum", + "dapps" + ], + "scripts": { + "build": "run-s build:**", + "build:esm": "tsc", + "build:bundle": "rollup --config rollup.config.js", + "fix": "run-s fix:*", + "fix:prettier": "prettier . --write", + "fix:lint": "eslint src *.js --fix", + "check": "run-s check:*", + "check:tsc": "tsc -p tsconfig.dev.json", + "check:lint": "eslint src *.js", + "check:prettier": "prettier . --list-different", + "check:spelling": "cspell \"{README.md,src/**/*.ts}\"", + "test": "run-s test:*", + "test:node": "TS_NODE_PROJECT=./tsconfig.dev.json mocha", + "test:browser": "karma start karma.conf.cjs", + "watch:build": "tsc -p tsconfig.json -w", + "watch:test": "mocha --watch", + "prepublish": "npm run build", + "reset-hard": "git clean -dfx -e .idea && git reset --hard && npm i && npm run build" + }, + "engines": { + "node": ">=16" + }, + "dependencies": { + "@noble/hashes": "^1.2.0", + "@waku/utils": "*" + }, + "devDependencies": { + "@rollup/plugin-commonjs": "^24.0.1", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.1", + "@types/chai": "^4.3.4", + "@types/debug": "^4.1.7", + "@types/mocha": "^10.0.1", + "@typescript-eslint/eslint-plugin": "^5.54.1", + "@typescript-eslint/parser": "^5.51.0", + "@waku/build-utils": "*", + "@waku/interfaces": "*", + "chai": "^4.3.7", + "cspell": "^6.28.0", + "eslint": "^8.35.0", + "eslint-config-prettier": "^8.6.0", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-functional": "^5.0.4", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^4.2.1", + "fast-check": "^3.7.0", + "ignore-loader": "^0.1.2", + "isomorphic-fetch": "^3.0.0", + "karma": "^6.4.1", + "karma-chrome-launcher": "^3.1.1", + "karma-mocha": "^2.0.1", + "karma-webpack": "^5.0.0", + "mocha": "^10.2.0", + "npm-run-all": "^4.1.5", + "prettier": "^2.8.4", + "process": "^0.11.10", + "puppeteer": "^19.7.2", + "rollup": "^3.15.0", + "ts-loader": "^9.4.2", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" + }, + "typedoc": { + "entryPoint": "./src/index.ts" + }, + "files": [ + "dist", + "bundle", + "src/*.ts", + "src/lib/**/*.ts", + "!**/*.spec.*", + "!**/*.json", + "CHANGELOG.md", + "LICENSE", + "README.md" + ] +} diff --git a/packages/message-hash/rollup.config.js b/packages/message-hash/rollup.config.js new file mode 100644 index 0000000000..8af2ba4bd1 --- /dev/null +++ b/packages/message-hash/rollup.config.js @@ -0,0 +1,24 @@ +import commonjs from "@rollup/plugin-commonjs"; +import json from "@rollup/plugin-json"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; +import { extractExports } from "@waku/build-utils"; + +import * as packageJson from "./package.json" assert { type: "json" }; + +const input = extractExports(packageJson); + +export default { + input, + output: { + dir: "bundle", + format: "esm", + }, + plugins: [ + commonjs(), + json(), + nodeResolve({ + browser: true, + preferBuiltins: false, + }), + ], +}; diff --git a/packages/message-hash/src/index.spec.ts b/packages/message-hash/src/index.spec.ts new file mode 100644 index 0000000000..d88a92af79 --- /dev/null +++ b/packages/message-hash/src/index.spec.ts @@ -0,0 +1,68 @@ +import type { IProtoMessage } from "@waku/interfaces"; +import { bytesToHex, hexToBytes } from "@waku/utils/bytes"; +import { expect } from "chai"; + +import { messageHash } from "./index.js"; + +// https://rfc.vac.dev/spec/14/#test-vectors +describe("RFC Test Vectors", () => { + it("Waku message hash computation", () => { + const expectedHash = + "4fdde1099c9f77f6dae8147b6b3179aba1fc8e14a7bf35203fc253ee479f135f"; + + const pubSubTopic = "/waku/2/default-waku/proto"; + const message: IProtoMessage = { + payload: hexToBytes("0x010203045445535405060708"), + contentTopic: "/waku/2/default-content/proto", + meta: hexToBytes("0x73757065722d736563726574"), + ephemeral: undefined, + rateLimitProof: undefined, + timestamp: undefined, + version: undefined, + }; + + const hash = messageHash(pubSubTopic, message); + + expect(bytesToHex(hash)).to.equal(expectedHash); + }); + + it("Waku message hash computation (meta attribute not present)", () => { + const expectedHash = + "87619d05e563521d9126749b45bd4cc2430df0607e77e23572d874ed9c1aaa62"; + + const pubSubTopic = "/waku/2/default-waku/proto"; + const message: IProtoMessage = { + payload: hexToBytes("0x010203045445535405060708"), + contentTopic: "/waku/2/default-content/proto", + meta: undefined, + ephemeral: undefined, + rateLimitProof: undefined, + timestamp: undefined, + version: undefined, + }; + + const hash = messageHash(pubSubTopic, message); + + expect(bytesToHex(hash)).to.equal(expectedHash); + }); + + it("Waku message hash computation (payload length 0)", () => { + const expectedHash = + "e1a9596237dbe2cc8aaf4b838c46a7052df6bc0d42ba214b998a8bfdbe8487d6"; + + const pubSubTopic = "/waku/2/default-waku/proto"; + const message: IProtoMessage = { + payload: new Uint8Array(), + contentTopic: "/waku/2/default-content/proto", + meta: hexToBytes("0x73757065722d736563726574"), + ephemeral: undefined, + rateLimitProof: undefined, + timestamp: undefined, + version: undefined, + }; + + const hash = messageHash(pubSubTopic, message); + + expect(bytesToHex(hash)).to.equal(expectedHash); + }); +}); diff --git a/packages/message-hash/src/index.ts b/packages/message-hash/src/index.ts new file mode 100644 index 0000000000..7c5a8220b8 --- /dev/null +++ b/packages/message-hash/src/index.ts @@ -0,0 +1,29 @@ +import { sha256 } from "@noble/hashes/sha256"; +import type { IProtoMessage } from "@waku/interfaces"; +import { concat, utf8ToBytes } from "@waku/utils/bytes"; + +/** + * Deterministic Message Hashing as defined in + * [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/#deterministic-message-hashing) + */ +export function messageHash( + pubsubTopic: string, + message: IProtoMessage +): Uint8Array { + const pubsubTopicBytes = utf8ToBytes(pubsubTopic); + const contentTopicBytes = utf8ToBytes(message.contentTopic); + + let bytes; + + if (message.meta) { + bytes = concat([ + pubsubTopicBytes, + message.payload, + contentTopicBytes, + message.meta, + ]); + } else { + bytes = concat([pubsubTopicBytes, message.payload, contentTopicBytes]); + } + return sha256(bytes); +} diff --git a/packages/message-hash/tsconfig.dev.json b/packages/message-hash/tsconfig.dev.json new file mode 100644 index 0000000000..4f7c34af3c --- /dev/null +++ b/packages/message-hash/tsconfig.dev.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.dev" +} diff --git a/packages/message-hash/tsconfig.json b/packages/message-hash/tsconfig.json new file mode 100644 index 0000000000..eebbc51585 --- /dev/null +++ b/packages/message-hash/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "outDir": "dist/", + "rootDir": "src", + "tsBuildInfoFile": "dist/.tsbuildinfo" + }, + "include": ["src"], + "exclude": ["src/**/*.spec.ts", "src/test_utils"] +} diff --git a/release-please-config.json b/release-please-config.json index edb95e5272..f9faf22ca7 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -9,6 +9,7 @@ "packages/utils": {}, "packages/proto": {}, "packages/interfaces": {}, + "packages/message-hash": {}, "packages/enr": {}, "packages/peer-exchange": {}, "packages/core": {},