diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..17ba5f79 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +node_modules +npm-debug.log* +.git +.gitignore +.github +.idea +.vscode +coverage +docs +migration +dockerfile +proto +stores +tests +whitelist +docker-compose.yml diff --git a/.gitignore b/.gitignore index 8a89d909..e56e8744 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ cert.pem migration/* !migration/.gitkeep .vscode/ + +# Environment files +.env diff --git a/README.md b/README.md index e2581903..7ff12ed3 100644 --- a/README.md +++ b/README.md @@ -2,77 +2,149 @@ A peer-to-peer crypto validator network to verify and append transactions. -Release 1 (R1) must be used alongside Trac Network R1 releases to maintain contract consistency. +Always follow the guidance in the [Security Policy](SECURITY.md) for release compatibility, upgrade steps, and required follow-up actions. -The MSB is utilizing the [Pear Runtime and Holepunch](https://pears.com/). +The MSB leverages the [Pear Runtime and Holepunch](https://pears.com/). + +## Prerequisites + +Node.js is required to run the application. Before installing Node.js, refer to the official [Node.js documentation](https://nodejs.org) for the latest recommended version and installation instructions. For this project, Node.js v24.11.0 (LTS) and npm 11.6.1 or newer are compatible. + +The Pear Runtime CLI is required to run the application. Before installing Pear, refer to the official [Pear documentation](https://docs.pears.com/guides/getting-started) for the latest recommended version and installation instructions. For this project, the latest Pear CLI is compatible. + +Install Pear globally: + +```sh +npm install -g pear +which pear +``` + +Docker is optional and only needed for running the containerized RPC node. Before installing Docker, refer to the official [Docker documentation](https://www.docker.com) for the latest recommended version and installation instructions. For running the containerized RPC node, the latest Docker is recommended. Tested with Docker version 28.3.2, build 578ccf6. ## Install ```shell -git clone -b msb-r1 --single-branch git@github.com:Trac-Systems/main_settlement_bus.git +git clone -b main --single-branch git@github.com:Trac-Systems/main_settlement_bus.git +cd main_settlement_bus +npm install ``` +## Post-install checklist + +- ✅ `npm run test:unit:all` – confirms the codebase builds and runs under both supported runtimes. +- 📋 `npm run test:acceptance` – optional but recommended before upgrades. This suite spins up in-process nodes and may take a few minutes. +- 🌐 RPC smoke test – start `MSB_STORE=smoke-store MSB_HOST=127.0.0.1 MSB_PORT=5000 npm run env-prod-rpc` in one terminal, then execute `curl -s http://127.0.0.1:5000/v1/fee` from another terminal to verify `/v1` routes respond. Stop the node with `Ctrl+C` once finished. + ## Usage -While the MSB supports native node-js, it is encouraged to use Pear: +Runtime entry points cover CLI-driven runs (`prod`, `prod-rpc`) and `.env`-aware runs (`env-prod`, `env-prod-rpc`). Each section below lists the accepted configuration inputs. + +### Interactive regular node + +#### Regular node with .env file + +This variant reads configuration from `.env`: + +``` +# .env +MSB_STORE= +``` +then +``` +npm run env-prod +``` + +The script sources `.env` before invoking program and falls back to `node-store` when `MSB_STORE` is not defined. + +#### Inline environment variables #### + +```sh +MSB_STORE= npm run env-prod +``` + +This run persists data under `./stores/${MSB_STORE}` (defaults to `node-store`) and is intended for inline or CLI-supplied configuration. + +#### CLI flags + +```sh +npm run prod --store= +``` + +### RPC-enabled node + +#### RPC with .env file +``` +# .env +MSB_STORE= +MSB_HOST=0.0.0.0 +MSB_PORT=5000 +``` -```js -cd main_settlement_bus -npm install -g pear -npm install ``` +npm run env-prod-rpc +``` + +This entry point sources `.env` automatically and defaults to `rpc-node-store`, `0.0.0.0`, and `5000` when variables are not present. -You can run the node in two modes: -1. Regular node (validator/indexer): -```js -pear run . store1 +#### Inline environment variables + +```sh +MSB_STORE= MSB_HOST= MSB_PORT= npm run prod-rpc ``` -2. Admin node (access to administrative commands): -```js -pear run . admin +Override any combination of `MSB_STORE`, `MSB_HOST`, or `MSB_PORT`. Data is persisted under `./stores/${MSB_STORE}` (default `rpc-node-store` for this script). + +#### CLI flags + +```sh +npm run prod-rpc --store= --host= --port= ``` -The admin mode provides access to additional commands such as `/add_admin`, `/add_whitelist`, `/balance_migration`, `/disable_initialization`, `/add_indexer`, `/remove_indexer`, and `/ban_writer`. These commands are only visible and available when running in admin mode. - -**Deploy Bootstrap (admin):** - -- Choose option 1) -- Copy and backup the seedphrase -- Copy the "MSB Writer" address -- With a text editor, open the file msb.mjs in document root -- Replace the bootstrap address with the copied writer address -- Choose a channel name (exactly 32 characters) -- Run again: pear run . store1 -- After the options appear, type /add_admin and hit enter -- Your instance is now the Bootstrap and admin peer, required to control validators -- Keep your bootstrap node running -- Strongly recommended: add a couple of nodes as writers - -**Running indexers (admin)** - -- Install on different machines than the Bootstrap's (ideally different data centers) -- Follow the "Running as validator" and then "Adding validators" procedures below -- Copy the MSB Writer address from your writer screen -- In your Bootstrap screen, add activate the new writers: -- /add_indexer -- You should see a success confirmation -- Usually 2 indexers on different locations are enough, we recommend 2 to max. 4 in addition to the Bootstrap - -**Running as validator (first run):** - -- Choose option 1) -- Copy and backup the seedphrase -- Copy the "MSB Address" after the screen fully loaded -- Hand your "MSB Address" over to the MSB admin for whitelisting -- Wait for the admin to announce the whitelist event -- In the screen type /add_writer -- After a few seconds you should see your validator being added as a writer - -**Adding validators (admin):** - -- Open the file /Whitelist/pubkeys.csv with a text editor -- Add as man Trac Network addresses as you wish -- In the MSB screen, enter /add_whitelist -- Wait for the listto be fully processed -- Inform your validator community being whitelisted \ No newline at end of file +## Docker usage + +You can run the RPC node in a containerized environment using the provided `docker-compose.yml` file. + +The provided `docker-compose.yml` uses the Pear-backed `npm run env-prod-rpc` entry point; the container image pre-installs the Pear CLI and bootstraps the runtime automatically when it first starts. + +### Running `msb-rpc` with Docker Compose + +The `msb-rpc` service uses the local `./stores` folder (mounted into `/app/stores`) and the environment variables `MSB_STORE`, `MSB_HOST`, and `MSB_PORT`. Any of the following launch methods can be applied: + +1. **Using a `.env` file** – populate `.env`, then start the service: + + ```sh + docker compose --env-file .env up -d msb-rpc + ``` + + Follow the logs with `docker compose logs -f msb-rpc` to ensure the node is healthy. + +2. **Passing variables inline** – use this method when environment variables should be provided directly in the command line, without modifying the `.env` file: + + ```sh + MSB_STORE= MSB_HOST= MSB_PORT= docker compose up -d msb-rpc + ``` + +3. **Reusing an existing store directory** – mount the path that already holds your store: + + ```sh + docker compose run -d --name msb-rpc \ + -e MSB_STORE= \ + -e MSB_HOST= \ + -e MSB_PORT= \ + -p : \ + -v /absolute/path/to/your/stores:/app/stores \ + msb-rpc + ``` + + Adjust `/absolute/path/to/your/stores` to the directory that already contains the persisted store. Once the container exists, bring it back with `docker compose start msb-rpc`. + +Stop the service with `docker compose stop msb-rpc`, remove the stack entirely with `docker compose down` when you are finished. + +> Note: The RPC instance must synchronize with the network after startup, so full readiness may take some time. + +## Troubleshooting + +- **Dependency install failures** – confirm you are on Node.js v24.11.0 (LTS) and npm ≥ 11.6.1. If packages still fail to build, clear artifacts (`rm -rf node_modules package-lock.json && npm install`) and rerun `npm run test:unit:all`. +- **Unit tests fail only in one runtime** – run the targeted commands (`npm run test:unit:node` or `npm run test:unit:bare`) to isolate regressions, then inspect `tests/unit/unit.test.js` for the failing cases. +- **RPC port already in use** – set `MSB_PORT` to a free value (for example `MSB_PORT=5050 npm run prod-rpc --port=5050`) or free the port with `lsof -i :` as needed. +- **Docker container exits immediately** – check `docker compose logs -f msb-rpc` for missing volume permissions or environment variables; the service requires the mounted `./stores` directory to be writable by the container user. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9d3e0db2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +services: + +# For now we won't support msb-node in docker-compose because docker stdin/out handling is problematic +# and it could make really bad user experience. +# msb-node: +# build: +# context: . +# dockerfile: dockerfile +# container_name: msb-node +# stdin_open: true +# tty: true +# restart: unless-stopped +# command: ["prod"] +# environment: +# MSB_STORE: ${MSB_STORE:-node-store} +# volumes: +# - ./stores:/app/stores + + msb-rpc: + build: + context: . + dockerfile: dockerfile + container_name: msb-rpc + restart: always + environment: + MSB_STORE: ${MSB_STORE:-rpc-node-store} + MSB_HOST: ${MSB_HOST:-0.0.0.0} + MSB_PORT: ${MSB_PORT:-5000} + volumes: + - ./stores:/app/stores + ports: + - "${MSB_PORT:-5000}:${MSB_PORT:-5000}" diff --git a/dockerfile b/dockerfile new file mode 100644 index 00000000..a94ef4d6 --- /dev/null +++ b/dockerfile @@ -0,0 +1,29 @@ +FROM node:22-bookworm + +RUN apt-get update \ + && apt-get install -y --no-install-recommends libatomic1 \ + && rm -rf /var/lib/apt/lists/* + +RUN npm install -g pear + +USER node + +WORKDIR /app + +RUN pear +ENV PATH="/home/node/.config/pear/bin:${PATH}" + +COPY package*.json ./ +RUN npm ci --omit=dev +COPY . . + +ENV MSB_STORE=node-store \ + MSB_HOST=0.0.0.0 \ + MSB_PORT=5000 + +VOLUME ["/app/stores"] +EXPOSE 5000 + +#ENTRYPOINT ["npm", "run"] +#CMD ["env-prod-rpc"] +CMD ["tail", "-f", "/dev/null"] diff --git a/msb.mjs b/msb.mjs index e7722c45..7c30d807 100644 --- a/msb.mjs +++ b/msb.mjs @@ -21,6 +21,8 @@ const rpc_opts = { ...opts, enable_tx_apply_logs: false, enable_error_apply_logs: false, + enable_wallet: false, + enable_interactive_mode: true, } @@ -44,4 +46,3 @@ msb.ready().then(async () => { msb.interactiveMode(); }); - diff --git a/package-lock.json b/package-lock.json index a5b7e8a2..a0cb5615 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "bare-http1": "4.1.5", "bare-readline": "1.0.7", "bare-tty": "5.0.2", + "bare-utils": "1.5.1", "bech32": "2.0.0", "compact-encoding": "2.18.0", "corestore": "7.5.0", @@ -1498,9 +1499,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz", - "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", + "version": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2045,9 +2046,9 @@ "license": "MIT" }, "node_modules/bare-addon-resolve": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.5.tgz", - "integrity": "sha512-XdqrG73zLK9LDfblOJwoAxmJ+7YdfRW4ex46+f4L+wPhk7H7LDrRMAbBw8s8jkxeEFpUenyB7QHnv0ErAWd3Yg==", + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.6.tgz", + "integrity": "sha512-hvOQY1zDK6u0rSr27T6QlULoVLwi8J2k8HHHJlxSfT7XQdQ/7bsS+AnjYkHtu/TkL+gm3aMXAKucJkJAbrDG/g==", "license": "Apache-2.0", "dependencies": { "bare-module-resolve": "^1.10.0", @@ -2127,7 +2128,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/bare-debug-log/-/bare-debug-log-2.0.0.tgz", "integrity": "sha512-Vi42PkMQsNV9PUpx2Gl1hikshx5O9FzMJ6o9Nnopseg7qLBBK7Nl31d0RHcfwLEAfmcPApytpc0ZFfq68u22FQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "bare-os": "^3.0.1" @@ -2146,7 +2146,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/bare-encoding/-/bare-encoding-1.0.0.tgz", "integrity": "sha512-9T5CSCaytaIWZpFWx9LQLJ6/z/m2Slnan9tQBKmOvoq/UtPBbOKT/B2fo29Xhi4X1FFtNx8DFdtrFgqm2yse/Q==", - "dev": true, "license": "Apache-2.0", "peerDependencies": { "bare-buffer": "*" @@ -2168,9 +2167,9 @@ } }, "node_modules/bare-events": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.1.tgz", - "integrity": "sha512-oxSAxTS1hRfnyit2CL5QpAOS5ixfBjj6ex3yTNvXyY/kE719jQ/IjuESJBK2w5v4wwQRAHGseVJXx9QBYOtFGQ==", + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" @@ -2185,7 +2184,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/bare-format/-/bare-format-1.0.1.tgz", "integrity": "sha512-1oS+LZrWK6tnYnvNSHDGljc2MPunRxwhpFriuCgzNF+oklrnwmBKD91tS0yt+jpl2j3UgcSDzBIMiVTvLs9A8w==", - "dev": true, "license": "Apache-2.0", "dependencies": { "bare-inspect": "^3.0.0" @@ -2268,7 +2266,6 @@ "version": "3.1.4", "resolved": "https://registry.npmjs.org/bare-inspect/-/bare-inspect-3.1.4.tgz", "integrity": "sha512-jfW5KRA84o3REpI6Vr4nbvMn+hqVAw8GU1mMdRwUsY5yJovQamxYeKGVKGqdzs+8ZbG4jRzGUXP/3Ji/DnqfPg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "bare-ansi-escapes": "^2.1.0", @@ -2304,9 +2301,9 @@ } }, "node_modules/bare-module-resolve": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.11.2.tgz", - "integrity": "sha512-HIBu9WacMejg3Dz4X1v6lJjp7ECnwpujvuLub+8I7JJLRwJaGxWMzGYvieOoS9R1n5iRByvTmLtIdPbwjfRgiQ==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.12.0.tgz", + "integrity": "sha512-JrzrqlC3Tds0iKRwQs8xIIJ+FRieKA9ll0jaqpotDLZtjJPVevzRoeuUYZ5GIo1t1z7/pIRdk85Q3i/2xQLfEQ==", "license": "Apache-2.0", "dependencies": { "bare-semver": "^1.0.0" @@ -2398,9 +2395,9 @@ "license": "Apache-2.0" }, "node_modules/bare-signals": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.1.0.tgz", - "integrity": "sha512-KTEy3ihxHvAt9O07uJ8RL82GBfrkUwjypLkXFpID+eYfGasGQ7WlRkcNjhRL6krmOBhDHUNLBkbV3JvDC2Yk5g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/bare-signals/-/bare-signals-4.2.0.tgz", + "integrity": "sha512-fNHMOdQIlYuTvMB3Oh9Apk99hLKn351+Ir8vz+khiPTcOqIyGG4uWWjdLTzxWdYGsA0eT+We3y0K74hjj2nq7A==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.5.3", @@ -2432,16 +2429,15 @@ } }, "node_modules/bare-subprocess": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.1.4.tgz", - "integrity": "sha512-CkZZr2R4mpRu2+WlTs2YZVlOKJ9t2D3SLIwhYBJxnRvtoygPTnMsdc92N6/E8YXY/uhZvbM6ZhEZTt1ubU6xlw==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/bare-subprocess/-/bare-subprocess-5.1.5.tgz", + "integrity": "sha512-hVOw9TJ5YnhnRD8fjaxNK7eTPfn4bR/uXHbD+4glmOvZzmmv+0twSUDSWFMYEBl5HBiurNVOUPZVuFrUyJvimg==", "dev": true, "license": "Apache-2.0", "dependencies": { "bare-env": "^3.0.0", "bare-events": "^2.5.4", "bare-os": "^3.0.1", - "bare-path": "^3.0.0", "bare-pipe": "^4.0.0", "bare-url": "^2.2.2" }, @@ -2503,7 +2499,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/bare-type/-/bare-type-1.1.0.tgz", "integrity": "sha512-LdtnnEEYldOc87Dr4GpsKnStStZk3zfgoEMXy8yvEZkXrcCv9RtYDrUYWFsBQHtaB0s1EUWmcvS6XmEZYIj3Bw==", - "dev": true, "license": "Apache-2.0", "engines": { "bare": ">=1.2.0" @@ -2523,7 +2518,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/bare-utils/-/bare-utils-1.5.1.tgz", "integrity": "sha512-mxCkFvmDU3mlD/mb+pT64kKXOsx2KMsWLQbngN1LB+NOXfhfnRnyvpy3VZc6m7gzQxe57Bsi+aTCBqA4/S3elQ==", - "dev": true, "license": "Apache-2.0", "dependencies": { "bare-debug-log": "^2.0.0", @@ -2577,9 +2571,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.8.25", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz", - "integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==", + "version": "2.8.28", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz", + "integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2688,9 +2682,9 @@ } }, "node_modules/brittle": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.0.tgz", - "integrity": "sha512-RUpK0HveDhvffC9i92fMKFef15D8mcS8OL3klFaRlcD+QLFINEHY2xhI4J9nnkLUinNOIRnFESXP/Yf/GAD38Q==", + "version": "3.19.1", + "resolved": "https://registry.npmjs.org/brittle/-/brittle-3.19.1.tgz", + "integrity": "sha512-4Ted1Mt9o9B6oIA6ImJJCtB/Fv++ZO3IDNQgRcHOXWSYGRUidgxchaGrUBaRn+4mlneXrgInPkCPzi0jO8qKbA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2718,9 +2712,9 @@ } }, "node_modules/browserslist": { - "version": "4.27.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", - "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", "dev": true, "funding": [ { @@ -2739,10 +2733,10 @@ "license": "MIT", "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.19", - "caniuse-lite": "^1.0.30001751", - "electron-to-chromium": "^1.5.238", - "node-releases": "^2.0.26", + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", "update-browserslist-db": "^1.1.4" }, "bin": { @@ -2821,9 +2815,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001753", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz", - "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==", + "version": "1.0.30001754", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz", + "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==", "dev": true, "funding": [ { @@ -2894,9 +2888,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", - "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.1.tgz", + "integrity": "sha512-+CmxIZ/L2vNcEfvNtLdU0ZQ6mbq3FZnwAP2PPTiKP+1QOoKwlKlPgb8UKV0Dds7QVaMnHm+FwSft2VB0s/SLjQ==", "dev": true, "license": "MIT" }, @@ -3227,13 +3221,15 @@ } }, "node_modules/device-file": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.0.1.tgz", - "integrity": "sha512-KEtMtJEA5/mHXIypfjSnzEZ062XciNupSVkYcf98KgrpeQRO3JMuGMqFXiKWSiT5pBoOt81RQuoUeMAeZxfNBg==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/device-file/-/device-file-2.1.3.tgz", + "integrity": "sha512-EKXOEa63atDkCLbUVZOTASG+6hXsg4ay9lcLZ53C1hkPLYjS2gIVIi0jtxz7ZXwCitcCRz5FI3NBZfQQifhf1g==", "license": "Apache-2.0", "dependencies": { "b4a": "^1.6.7", "bare-fs": "^4.0.1", + "bare-path": "^3.0.0", + "fd-lock": "^2.1.0", "fs-native-extensions": "^1.4.0", "ready-resource": "^1.2.0" } @@ -3310,9 +3306,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.245", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.245.tgz", - "integrity": "sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==", + "version": "1.5.250", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz", + "integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==", "dev": true, "license": "ISC" }, @@ -3589,6 +3585,27 @@ "bser": "2.1.1" } }, + "node_modules/fd-lock": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fd-lock/-/fd-lock-2.1.1.tgz", + "integrity": "sha512-H3VkcWFl39Rk0xBokDcUyBcVs6VrYTUo/DhDWMzJOF99wXWDAvmvq/GlLTS2NTehsznZhp3fXVyrtB2ldDXhwg==", + "license": "Apache-2.0", + "dependencies": { + "bare-fs": "^4.5.0", + "fs-native-extensions": "^1.4.4", + "ready-resource": "^1.2.0", + "resource-on-exit": "^1.0.0" + } + }, + "node_modules/fd-lock/node_modules/ready-resource": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/ready-resource/-/ready-resource-1.2.0.tgz", + "integrity": "sha512-nfcco/8iAFV0M+2PYnmIc+/xY0iRb35d42HFHQ7AfjulbGEAFa+XWpByfwSyeVeiBoMLLFVMv1HixxNCqzSQ1g==", + "license": "MIT", + "dependencies": { + "bare-events": "^2.2.0" + } + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3685,9 +3702,9 @@ } }, "node_modules/fs-native-extensions": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.4.tgz", - "integrity": "sha512-iLo3r2ei97thJNoj3DgSdzUF2hZ2yekZpXF98LlHc2eZGPOwiVblyEa6iS68zLu9ayXvlE8/c3CMaagNbHJB1Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/fs-native-extensions/-/fs-native-extensions-1.4.5.tgz", + "integrity": "sha512-ekV0T//iDm4AvhOcuPaHpxub4DI7HvY5ucLJVDvi7T2J+NZkQ9S6MuvgP0yeQvoqNUaAGyLjVYb1905BF9bpmg==", "license": "Apache-2.0", "dependencies": { "require-addon": "^1.1.0", @@ -4097,9 +4114,9 @@ } }, "node_modules/hyperschema": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.17.0.tgz", - "integrity": "sha512-eO0WPJMF5W5mVfMQRtHPekrbgIahsbDaQ9NtQS38Aqqsn+pR2LgaQNH+Gpr2nloATLT24zopphaPtdFSKMrrRA==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/hyperschema/-/hyperschema-1.17.1.tgz", + "integrity": "sha512-pxQ0+0B0wbK2OY64iUK7kGBDm22tAqowRemjqAxo38Q7Sc0wVWU6RhbNiAdBZVYRKOie0dzA+tBuRqO6YrKfyQ==", "license": "Apache-2.0", "dependencies": { "bare-fs": "^4.0.1", @@ -5437,9 +5454,9 @@ "license": "BlueOak-1.0.0" }, "node_modules/paparam": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.8.6.tgz", - "integrity": "sha512-M9YxYv8LkotNkXdFDd2GPwa9jspLkcTZn9yjBzAPkAF0ngq755KtmoQyVWb0+6eIZuW7WtTPWycelNDhkkgOog==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/paparam/-/paparam-1.9.0.tgz", + "integrity": "sha512-kAARhqtcvLbfQIUk9MwltpWU/3G7GfLig0KLU2TPQvo0fuOZsOL7j6qN9SxU7wNu/0ecHAGJJhoZtSKpqHpd2A==", "dev": true, "license": "Apache-2.0" }, @@ -5799,13 +5816,12 @@ "license": "Apache-2.0" }, "node_modules/require-addon": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.1.0.tgz", - "integrity": "sha512-KbXAD5q2+v1GJnkzd8zzbOxchTkStSyJZ9QwoCq3QwEXAaIlG3wDYRZGzVD357jmwaGY7hr5VaoEAL0BkF0Kvg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.2.0.tgz", + "integrity": "sha512-VNPDZlYgIYQwWp9jMTzljx+k0ZtatKlcvOhktZ/anNPI3dQ9NXk7cq2U4iJ1wd9IrytRnYhyEocFWbkdPb+MYA==", "license": "Apache-2.0", "dependencies": { - "bare-addon-resolve": "^1.3.0", - "bare-url": "^2.1.0" + "bare-addon-resolve": "^1.3.0" }, "engines": { "bare": ">=1.10.0" @@ -5850,10 +5866,16 @@ "integrity": "sha512-LWsTOA91AqzBTjSGgX79Tc130pwcBK6xjpJEO+qRT5IKZ6bGnHKcc8QL3upUBcWuU8OTIDzKK2VNSwmmlqvAVg==", "license": "MIT" }, + "node_modules/resource-on-exit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resource-on-exit/-/resource-on-exit-1.0.0.tgz", + "integrity": "sha512-ViJwJAknCkLRJRPR+9SISQQ7R5eRgtdIHLJsM2hHx1MweAJbJxJ5XnMjjq0Lc7ZGv44ufzAqds1nKxiVkdy4ag==", + "license": "Apache-2.0" + }, "node_modules/rocksdb-native": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.9.2.tgz", - "integrity": "sha512-xrOz5QnKiRstVuEiq4h1HVf4wrVI5gfLoKfChlNgel2HmOm21Wowbx1kObBUXHTGrbOoajAUkT+c4kPxCvSptw==", + "version": "3.11.4", + "resolved": "https://registry.npmjs.org/rocksdb-native/-/rocksdb-native-3.11.4.tgz", + "integrity": "sha512-vG6NIkmipcAYV9QHIN1tALBHfIAcAfRlV5YWSwfn7yMmsXW0AJel0oZyeW8lRpTD7HTWq88GwzIMI+MV4iPSOg==", "license": "Apache-2.0", "dependencies": { "compact-encoding": "^2.15.0", @@ -6074,9 +6096,9 @@ } }, "node_modules/simdle-native": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.7.tgz", - "integrity": "sha512-O3+kYD48jByt/IY/WEpdZshtRbAcotOmDMhs2PrW2MVpgRVoCuTGLtNJO5LjZNYzh7qLIhTScpa16FJWHaSScQ==", + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/simdle-native/-/simdle-native-1.3.9.tgz", + "integrity": "sha512-Isc8sP4OiiIU0mpslD4GHEnR0VQWvR/54WN7YtwEDkNdTJVWtpmvsSvsgRlw5BNGxdYXlVRegdnrSu10H/PhvA==", "license": "Apache-2.0", "optional": true, "dependencies": { @@ -6150,9 +6172,9 @@ } }, "node_modules/sodium-native": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.9.tgz", - "integrity": "sha512-6fpu3d6zdrRpLhuV3CDIBO5g90KkgaeR+c3xvDDz0ZnDkAlqbbPhFW7zhMJfsskfZ9SuC3SvBbqvxcECkXRyKw==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.10.tgz", + "integrity": "sha512-UIw+0AbpCQRuTJF88JWrZomP4O+PXhlWvdopiAJOsUivTyHTf3korMyStxkZuPngSbBEtEfDdc4ewEd8/T4/lA==", "license": "MIT", "dependencies": { "require-addon": "^1.1.0", diff --git a/package.json b/package.json index 431da8d7..8f7a1cd6 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,12 @@ "type": "module", "scripts": { "dev": "pear run -d .", - "prod": "NODE_OPTIONS='--max-old-space-size=4096' pear run . ${npm_config_store}", "dev-rpc": "pear run -d . ${npm_config_store} --rpc --port ${npm_config_port}", - "prod-rpc": "pear run . ${npm_config_store} --rpc --host ${npm_config_host} --port ${npm_config_port}", + "prod": "NODE_OPTIONS='--max-old-space-size=4096' pear run . ${npm_config_store}", + "prod-rpc": "NODE_OPTIONS='--max-old-space-size=4096' pear run . ${npm_config_store} --rpc --host ${npm_config_host} --port ${npm_config_port}", + "env-prod": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' pear run. ${MSB_STORE:-node-store}", + "env-prod-rpc": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' pear run . ${MSB_STORE:-rpc-node-store} --rpc --host ${MSB_HOST:-0.0.0.0} --port ${MSB_PORT:-5000}", + "env-prod-rpc-docker": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' node msb.mjs ${MSB_STORE:-rpc-node-store} --rpc --host ${MSB_HOST:-0.0.0.0} --port ${MSB_PORT:-5000}", "protobuf": "node scripts/generate-protobufs.js", "test:acceptance": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=200000 tests/acceptance/", "test:integration": "brittle-node -t 1200000 tests/integration/integration.test.js", @@ -30,6 +33,7 @@ "bare-http1": "4.1.5", "bare-readline": "1.0.7", "bare-tty": "5.0.2", + "bare-utils": "1.5.1", "bech32": "2.0.0", "compact-encoding": "2.18.0", "corestore": "7.5.0", diff --git a/rpc/handlers.mjs b/rpc/handlers.mjs index bbabdbb2..66fd3af5 100644 --- a/rpc/handlers.mjs +++ b/rpc/handlers.mjs @@ -249,4 +249,4 @@ export async function handleTransactionExtendedDetails({ msbInstance, respond, r respond(500, { error: 'An error occurred processing the request.' }); } } -} \ No newline at end of file +} diff --git a/src/core/network/Network.js b/src/core/network/Network.js index ac49c4a7..64f92749 100644 --- a/src/core/network/Network.js +++ b/src/core/network/Network.js @@ -17,7 +17,7 @@ import { DHT_BOOTSTRAPS } from '../../utils/constants.js'; import ConnectionManager from './services/ConnectionManager.js'; - +import IdentityProvider from './identity/IdentityProvider.js'; const wakeup = new w(); class Network extends ReadyResource { @@ -29,15 +29,18 @@ class Network extends ReadyResource { #transactionPoolService; #validatorObserverService; #validatorConnectionManager; + #options; + #identityProvider = null; constructor(state, channel, address = null, options = {}) { super(); + this.#options = options; this.#enable_wallet = options.enable_wallet !== false; this.#channel = channel; this.#transactionPoolService = new TransactionPoolService(state, address, options); - this.#validatorObserverService = new ValidatorObserverService(this, state, address, options) + this.#validatorObserverService = new ValidatorObserverService(this, state, address, options); this.#networkMessages = new NetworkMessages(this, options); - this.#validatorConnectionManager = new ConnectionManager({ maxValidators: options.max_validators }) + this.#validatorConnectionManager = new ConnectionManager({ maxValidators: options.max_validators }); this.admin_stream = null; this.admin = null; this.validator = null; @@ -90,6 +93,7 @@ class Network extends ReadyResource { ) { if (!this.#swarm) { const keyPair = await this.initializeNetworkingKeyPair(store, wallet); + const identityProvider = this.#resolveIdentityProvider(keyPair, wallet); this.#swarm = new Hyperswarm({ keyPair, bootstrap: this.#dht_bootstrap, @@ -100,7 +104,7 @@ class Network extends ReadyResource { }); console.log(`Channel: ${b4a.toString(this.#channel)}`); - this.#networkMessages.initializeMessageRouter(state, wallet); + this.#networkMessages.initializeMessageRouter(state, identityProvider); this.#swarm.on('connection', async (connection) => { const { message_channel, message } = await this.#networkMessages.setupProtomuxMessages(connection); @@ -120,7 +124,7 @@ class Network extends ReadyResource { this.custom_stream = null; this.custom_node = null; } - try{ message_channel.close() }catch(e){} + try { message_channel.close() } catch (e) { } }); @@ -129,7 +133,7 @@ class Network extends ReadyResource { error && error.message && ( error.message.includes('connection reset by peer') || error.message.includes('Duplicate connection') || - error.message.includes('connection timed out') ) + error.message.includes('connection timed out')) ) { // TODO: decide if we want to handle this error in a specific way. It generates a lot of logs. return; @@ -168,11 +172,11 @@ class Network extends ReadyResource { cnt += 1; } } - + if (this.#swarm.peers.has(publicKey)) { let stream; const peerInfo = this.#swarm.peers.get(publicKey) - stream = this.#swarm._allConnections.get(peerInfo.publicKey) + stream = this.#swarm._allConnections.get(peerInfo.publicKey) if (stream !== undefined && stream.messenger !== undefined) { if (type === 'validator') { this.#validatorConnectionManager.addValidator(b4a.from(publicKey, 'hex'), stream) @@ -183,8 +187,8 @@ class Network extends ReadyResource { } async isConnected(publicKey) { - return this.#swarm.peers.has(publicKey) && - this.#swarm.peers.get(publicKey).connectedTime != -1 + return this.#swarm.peers.has(publicKey) && + this.#swarm.peers.get(publicKey).connectedTime != -1 } async #sendRequestByType(stream, type) { @@ -239,6 +243,16 @@ class Network extends ReadyResource { console.log(e) } } + + #resolveIdentityProvider(keyPair, wallet) { + if (!this.#identityProvider) { + this.#identityProvider = this.#enable_wallet + ? IdentityProvider.fromWallet(wallet) + : IdentityProvider.fromNetworkKeyPair(keyPair, this.#options?.networkPrefix); + } + return this.#identityProvider; + } + } export default Network; diff --git a/src/core/network/identity/IdentityProvider.js b/src/core/network/identity/IdentityProvider.js new file mode 100644 index 00000000..619dfc2a --- /dev/null +++ b/src/core/network/identity/IdentityProvider.js @@ -0,0 +1,111 @@ +import PeerWallet from 'trac-wallet'; +import { TRAC_NETWORK_MSB_MAINNET_PREFIX } from 'trac-wallet/constants.js'; +import b4a from 'b4a'; + +class IdentityProvider { + #strategy; + constructor(strategy) { + this.#strategy = strategy; + } + + setStrategy(strategy) { + this.#strategy = strategy; + } + + get publicKey() { + return this.#strategy.publicKey; + } + + get address() { + return this.#strategy.address; + } + + sign(message) { + return this.#strategy.sign(message); + } + + verify(signature, message, publicKey) { + return this.#strategy.verify(signature, message, publicKey); + } + + static fromWallet(wallet) { + return new IdentityProvider(new PeerWalletStrategy(wallet)); + } + + static fromNetworkKeyPair(keyPair, networkPrefix) { + return new IdentityProvider(new NetworkWalletStrategy(keyPair, networkPrefix)); + } + +} + +class PeerWalletStrategy { + #wallet; + + constructor(wallet) { + this.#wallet = wallet; + } + + get publicKey() { + return this.#wallet.publicKey; + } + + get address() { + return this.#wallet.address; + } + + sign(message) { + return this.#wallet.sign(message); + } + + verify(signature, message, publicKey) { + return this.#wallet.verify(signature, message, publicKey); + } +} + +class NetworkWalletStrategy { + #publicKey; + #secretKey; + #address; + + constructor(keyPair, networkPrefix = TRAC_NETWORK_MSB_MAINNET_PREFIX) { + + if (!keyPair?.publicKey || !keyPair?.secretKey) { + throw new Error('NetworkIdentityProvider: keyPair with publicKey and secretKey is required'); + } + this.#assertBuffer(keyPair.publicKey, 'publicKey'); + this.#assertBuffer(keyPair.secretKey, 'secretKey'); + + const address = PeerWallet.encodeBech32mSafe(networkPrefix, keyPair.publicKey); + if (!address) { + throw new Error('NetworkIdentityProvider: failed to derive address from networking key pair'); + } + + this.#publicKey = keyPair.publicKey; + this.#secretKey = keyPair.secretKey; + this.#address = address; + } + + get publicKey() { + return this.#publicKey; + } + + get address() { + return this.#address; + } + + sign(message) { + return PeerWallet.sign(message, this.#secretKey); + } + + verify(signature, message, publicKey = this.#publicKey) { + return PeerWallet.verify(signature, message, publicKey); + } + + #assertBuffer(value) { + if (!b4a.isBuffer(value)) { + throw new Error(`NetworkIdentityProvider: value must be a Buffer`); + } + } +} + +export default IdentityProvider; diff --git a/src/core/network/services/ValidatorObserverService.js b/src/core/network/services/ValidatorObserverService.js index e1028643..d362dc53 100644 --- a/src/core/network/services/ValidatorObserverService.js +++ b/src/core/network/services/ValidatorObserverService.js @@ -10,7 +10,6 @@ const DELAY_INTERVAL = 250 class ValidatorObserverService { #enable_validator_observer; - #enable_wallet; #state; #network; #scheduler; @@ -18,7 +17,7 @@ class ValidatorObserverService { #isInterrupted constructor(network, state, address, options = {}) { - this.#enable_wallet = options.enable_wallet !== false; + this.#enable_validator_observer = options.enable_validator_observer !== false; this.#network = network; this.#state = state; this.#address = address; @@ -34,7 +33,7 @@ class ValidatorObserverService { // OS CALLS, ACCUMULATORS, MAYBE THIS IS POSSIBLE TO CHECK I/O QUEUE IF IT COINTAIN IT. FOR NOW WE ARE USING SLEEP. async start() { if (!this.#shouldRun()) { - console.info('ValidatorObserverService can not start. Wallet is not enabled'); + console.info('ValidatorObserverService can not start. Disabled by configuration.'); return; } if (this.#scheduler && this.#scheduler.isRunning) { @@ -111,7 +110,11 @@ class ValidatorObserverService { }; #shouldRun() { - return this.#enable_wallet && !this.#isInterrupted + if (!this.#enable_validator_observer || this.#isInterrupted) { + return false; + } + + return true; } async #lengthEntry() { diff --git a/tests/acceptance/v1/rpc.test.mjs b/tests/acceptance/v1/rpc.test.mjs index 468b5648..09f699dc 100644 --- a/tests/acceptance/v1/rpc.test.mjs +++ b/tests/acceptance/v1/rpc.test.mjs @@ -1,17 +1,19 @@ import request from "supertest" import { createServer } from "../../../rpc/create_server.mjs" import { initTemporaryDirectory } from '../../helpers/setupApplyTests.js' -import { testKeyPair1, testKeyPair2 } from '../../fixtures/apply.fixtures.js' -import { randomBytes, setupMsbAdmin, setupMsbWriter, fundPeer, removeTemporaryDirectory } from "../../helpers/setupApplyTests.js" +import { testKeyPair1, testKeyPair2, testKeyPair3 } from '../../fixtures/apply.fixtures.js' +import { randomBytes, setupMsbAdmin, setupMsbWriter, fundPeer, removeTemporaryDirectory, setupMsbPeer, tryToSyncWriters, waitForNodeState } from "../../helpers/setupApplyTests.js" import { $TNK } from "../../../src/core/state/utils/balance.js" import tracCrypto from 'trac-crypto-api'; import b4a from 'b4a' -let msb +let writerMsb +let rpcMsb let server let wallet let toClose let tmpDirectory +let additionalPeers = [] const setupNetwork = async () => { tmpDirectory = await initTemporaryDirectory() @@ -28,21 +30,36 @@ const setupNetwork = async () => { store_name: '/admin' } - const peer = await setupMsbAdmin(testKeyPair1, tmpDirectory, rpcOpts) - const writer = await setupMsbWriter(peer, 'writer', testKeyPair2, tmpDirectory, peer.options); - return { writer, peer } + const admin = await setupMsbAdmin(testKeyPair1, tmpDirectory, rpcOpts) + const writer = await setupMsbWriter(admin, 'writer', testKeyPair2, tmpDirectory, admin.options); + additionalPeers.push(writer) + const reader = await setupMsbPeer('peer-2', testKeyPair3, tmpDirectory, { ...admin.options, enable_wallet: false }); + additionalPeers.push(reader) + await tryToSyncWriters(admin, writer, reader) + await waitForNodeState(reader, writer.wallet.address, { + wk: writer.msb.state.writingKey, + isWhitelisted: true, + isWriter: true, + isIndexer: false, + }) + return { writer, admin, reader } } beforeAll(async () => { - const { peer, writer } = await setupNetwork() - msb = writer.msb - wallet = msb.wallet - server = createServer(msb) - toClose = peer.msb + const { admin, writer, reader } = await setupNetwork() + writerMsb = writer.msb + rpcMsb = reader.msb + wallet = writerMsb.wallet + server = createServer(rpcMsb) + toClose = admin.msb }) afterAll(async () => { - await Promise.all([msb?.close(), toClose?.close()]) + const peersToClose = [...new Set(additionalPeers.map(peer => peer?.msb).filter(Boolean))] + await Promise.all([ + toClose?.close(), + ...peersToClose.map(instance => instance.close()) + ]) await removeTemporaryDirectory(tmpDirectory) }) @@ -109,7 +126,7 @@ describe("API acceptance tests", () => { wallet.address, wallet.address, b4a.toString($TNK(1n), 'hex'), - b4a.toString(await msb.state.getIndexerSequenceState(), 'hex') + b4a.toString(await rpcMsb.state.getIndexerSequenceState(), 'hex') ); const payload = tracCrypto.transaction.build(txData, b4a.from(wallet.secretKey, 'hex')); @@ -130,7 +147,7 @@ describe("API acceptance tests", () => { }) it("POST /v1/tx-payloads-bulk", async () => { - const result = await msb.state.confirmedTransactionsBetween(0, 40) // This is just an arbitrary range that will most likely contain valid + const result = await rpcMsb.state.confirmedTransactionsBetween(0, 40) // This is just an arbitrary range that will most likely contain valid const hashes = result.map(({ hash }) => hash) const payload = { hashes } @@ -158,7 +175,7 @@ describe("API acceptance tests", () => { wallet.address, wallet.address, b4a.toString($TNK(1n), 'hex'), - b4a.toString(await msb.state.getIndexerSequenceState(), 'hex') + b4a.toString(await rpcMsb.state.getIndexerSequenceState(), 'hex') ); const payload = tracCrypto.transaction.build(txData, b4a.from(wallet.secretKey, 'hex')); @@ -194,13 +211,13 @@ describe("API acceptance tests", () => { wallet.address, wallet.address, b4a.toString($TNK(1n), 'hex'), - b4a.toString(await msb.state.getIndexerSequenceState(), 'hex') + b4a.toString(await rpcMsb.state.getIndexerSequenceState(), 'hex') ); const payload = tracCrypto.transaction.build(txData, b4a.from(wallet.secretKey, 'hex')); - - const originalGetConfirmedLength = msb.state.getTransactionConfirmedLength; - msb.state.getTransactionConfirmedLength = async () => null; + + const originalGetConfirmedLength = rpcMsb.state.getTransactionConfirmedLength; + rpcMsb.state.getTransactionConfirmedLength = async () => null; try { const broadcastRes = await request(server) @@ -219,7 +236,7 @@ describe("API acceptance tests", () => { fee: '0' }); } finally { - msb.state.getTransactionConfirmedLength = originalGetConfirmedLength; + rpcMsb.state.getTransactionConfirmedLength = originalGetConfirmedLength; } }); @@ -320,4 +337,4 @@ describe("API acceptance tests", () => { }); }); }) -}) \ No newline at end of file +}) diff --git a/tests/unit/network/IdentityProvider.test.js b/tests/unit/network/IdentityProvider.test.js new file mode 100644 index 00000000..2e94e355 --- /dev/null +++ b/tests/unit/network/IdentityProvider.test.js @@ -0,0 +1,77 @@ +import { test } from 'brittle'; +import sinon from 'sinon'; +import b4a from 'b4a'; + +import PeerWallet from 'trac-wallet'; +import { TRAC_NETWORK_MSB_MAINNET_PREFIX } from 'trac-wallet/constants.js'; + +import IdentityProvider from '../../../src/core/network/identity/IdentityProvider.js'; +import { errorMessageIncludes } from '../../helpers/regexHelper.js'; +import { testKeyPair1, testKeyPair2 } from '../../fixtures/apply.fixtures.js'; + +test('IdentityProvider.fromWallet proxies wallet behavior', async t => { + const publicKey = b4a.from(testKeyPair2.publicKey, 'hex'); + const address = PeerWallet.encodeBech32m(TRAC_NETWORK_MSB_MAINNET_PREFIX, publicKey); + const signResult = b4a.from('abcd', 'hex'); + const wallet = { + publicKey, + address, + sign: sinon.stub().returns(signResult), + verify: sinon.stub().returns(true) + }; + + const provider = IdentityProvider.fromWallet(wallet); + const message = b4a.from('00112233', 'hex'); + const signature = provider.sign(message); + + t.alike(provider.publicKey, publicKey); + t.is(provider.address, address); + t.alike(signature, signResult); + t.ok(wallet.sign.calledOnceWithExactly(message)); + + const verifyResult = provider.verify(signature, message); + t.ok(verifyResult); + t.ok(wallet.verify.calledOnceWithExactly(signature, message, undefined)); + + sinon.restore(); +}); + +test('IdentityProvider.fromNetworkKeyPair requires both public and secret keys', async t => { + const publicKey = b4a.from(testKeyPair1.publicKey, 'hex'); + await t.exception(() => IdentityProvider.fromNetworkKeyPair({ publicKey }), errorMessageIncludes('keyPair with publicKey and secretKey is required')); +}); + +test('IdentityProvider.fromNetworkKeyPair rejects non-buffer inputs', async t => { + const secretKey = b4a.from(testKeyPair1.secretKey, 'hex'); + await t.exception(() => IdentityProvider.fromNetworkKeyPair({ publicKey: 'not-a-buffer', secretKey }), errorMessageIncludes('value must be a Buffer') + ); +}); + +test('IdentityProvider.fromNetworkKeyPair derives address and signs payloads', async t => { + const keyPair = { + publicKey: b4a.from(testKeyPair1.publicKey, 'hex'), + secretKey: b4a.from(testKeyPair1.secretKey, 'hex') + }; + const provider = IdentityProvider.fromNetworkKeyPair(keyPair, TRAC_NETWORK_MSB_MAINNET_PREFIX); + const message = b4a.from('123455555', 'hex'); + const signature = provider.sign(message); + + t.is( + provider.address, + PeerWallet.encodeBech32m(TRAC_NETWORK_MSB_MAINNET_PREFIX, provider.publicKey) + ); + t.ok(PeerWallet.verify(signature, message, provider.publicKey)); + t.ok(provider.verify(signature, message)); +}); + +test('IdentityProvider surfaces address derivation failures', async t => { + const keyPair = { + publicKey: b4a.from(testKeyPair1.publicKey, 'hex'), + secretKey: b4a.from(testKeyPair1.secretKey, 'hex') + }; + + const stub = sinon.stub(PeerWallet, 'encodeBech32mSafe').returns(null); + await t.exception(() => IdentityProvider.fromNetworkKeyPair(keyPair, TRAC_NETWORK_MSB_MAINNET_PREFIX), errorMessageIncludes('failed to derive address')); + stub.restore(); + sinon.restore(); +});