diff --git a/.github/ISSUE_TEMPLATE/bump_dependencies.md b/.github/ISSUE_TEMPLATE/bump_dependencies.md index 0413cbfd2..59f46f08b 100644 --- a/.github/ISSUE_TEMPLATE/bump_dependencies.md +++ b/.github/ISSUE_TEMPLATE/bump_dependencies.md @@ -1,7 +1,7 @@ --- name: Bump dependencies -about: Bump vendor dependencies for release -title: 'Bump vendor dependencies for release 0.0.0' +about: Bump dependencies for release +title: 'Bump dependencies for release 0.X.0' labels: dependencies assignees: '' @@ -9,40 +9,10 @@ assignees: '' -Update `nwaku` "vendor" dependencies. +### Bumped items +- [ ] Update nimble dependencies + 1. Edit manually waku.nimble. For some dependencies, we want to bump versions manually and use a pinned version, f.e., nim-libp2p and all its dependencies. + 2. Run `nimble lock` (make sure `nimble --version` shows the Nimble version pinned in waku.nimble) + 3. Run `./tools/gen-nix-deps.sh nimble.lock nix/deps.nix` to update nix deps -### Items to bump -- [ ] dnsclient.nim ( update to the latest tag version ) -- [ ] nim-bearssl -- [ ] nimbus-build-system -- [ ] nim-chronicles -- [ ] nim-chronos -- [ ] nim-confutils -- [ ] nimcrypto -- [ ] nim-dnsdisc -- [ ] nim-eth -- [ ] nim-faststreams -- [ ] nim-http-utils -- [ ] nim-json-rpc -- [ ] nim-json-serialization -- [ ] nim-libbacktrace -- [ ] nim-libp2p ( update to the latest tag version ) -- [ ] nim-metrics -- [ ] nim-nat-traversal -- [ ] nim-presto -- [ ] nim-regex ( update to the latest tag version ) -- [ ] nim-results -- [ ] nim-secp256k1 -- [ ] nim-serialization -- [ ] nim-sqlite3-abi ( update to the latest tag version ) -- [ ] nim-stew -- [ ] nim-stint -- [ ] nim-taskpools ( update to the latest tag version ) -- [ ] nim-testutils ( update to the latest tag version ) -- [ ] nim-toml-serialization -- [ ] nim-unicodedb -- [ ] nim-unittest2 ( update to the latest tag version ) -- [ ] nim-web3 ( update to the latest tag version ) -- [ ] nim-websock ( update to the latest tag version ) -- [ ] nim-zlib -- [ ] zerokit ( this should be kept in version `v0.7.0` ) +- [ ] Update vendor/zerokit dependency. diff --git a/.github/ISSUE_TEMPLATE/deploy_release.md b/.github/ISSUE_TEMPLATE/deploy_release.md new file mode 100644 index 000000000..9b9a4f32c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/deploy_release.md @@ -0,0 +1,55 @@ +--- +name: Deploy Release +about: Execute tasks for deploying a new version in a fleet +title: 'Deploy release vX.X.X in waku.sandbox and/or status.prod fleet' +labels: deploy-release +assignees: '' + +--- + + + +### Link to the Release PR + + + +### Items to complete, in order + + + +- [ ] Receive sign-off from DST. + - [ ] Inform DST team about what are the expectations for this release. For example, if we expect higher, same or lower bandwidth consumption. Or a new protocol appears, etc. + - [ ] Ask DST to add a comment approving this deployment and add a link to the analysis report. + +- [ ] Deploy to waku.sandbox + - [ ] Coordinate with Infra Team about possible changes in CI behavior + - [ ] Update waku.sandbox with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). + +- [ ] Deploy to status.prod + - [ ] Coordinate with Infra Team about possible changes in CI behavior + - [ ] Ask Status admin to add a comment approving that this deployment to happen now. + - [ ] Update status.prod with [this deployment job](https://ci.infra.status.im/job/nim-waku/job/deploy-status-prod/). + +- [ ] Update infra config + - [ ] Submit PRs into infra repos to adjust deprecated or changed arguments (review CHANGELOG.md for that release). And confirm the fleet can run after that. This requires coordination with infra team. + +### Reference Links + +- [Release process](https://github.com/logos-messaging/logos-delivery/blob/master/docs/contributors/release-process.md) +- [Release notes](https://github.com/logos-messaging/logos-delivery/blob/master/CHANGELOG.md) +- [Infra-role-nim-waku](https://github.com/status-im/infra-role-nim-waku) +- [Infra-waku](https://github.com/status-im/infra-waku) +- [Infra-Status](https://github.com/status-im/infra-status) +- [Jenkins](https://ci.infra.status.im/job/nim-waku/) +- [Fleets](https://fleets.waku.org/) +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) +- [Kibana](https://kibana.infra.status.im/app/) diff --git a/.github/ISSUE_TEMPLATE/prepare_release.md b/.github/ISSUE_TEMPLATE/prepare_release.md index 9553d5685..3c2e2f729 100644 --- a/.github/ISSUE_TEMPLATE/prepare_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_release.md @@ -1,8 +1,8 @@ --- -name: Prepare release -about: Execute tasks for the creation and publishing of a new release +name: Prepare Release +about: Execute tasks for the creation and publishing of a new full release title: 'Prepare release 0.0.0' -labels: release +labels: full-release assignees: '' --- @@ -10,63 +10,70 @@ assignees: '' ### Items to complete All items below are to be completed by the owner of the given release. -- [ ] Create release branch -- [ ] Assign release candidate tag to the release branch HEAD. e.g. v0.30.0-rc.0 -- [ ] Generate and edit releases notes in CHANGELOG.md -- [ ] Review possible update of [config-options](https://github.com/waku-org/docs.waku.org/blob/develop/docs/guides/nwaku/config-options.md) -- [ ] _End user impact_: Summarize impact of changes on Status end users (can be a comment in this issue). -- [ ] **Validate release candidate** - - [ ] Bump nwaku dependency in [waku-rust-bindings](https://github.com/waku-org/waku-rust-bindings) and make sure all examples and tests work +- [ ] Create release branch with major and minor only ( e.g. release/v0.X ) if it doesn't exist. +- [ ] Update the `version` field in `waku.nimble` to match the release version (e.g. `version = "0.X.0"`) **and merge it before assigning any tag** - the `release-assets` workflow gates artifact build/upload. +- [ ] Assign release candidate tag to the release branch HEAD (e.g. `v0.X.0-rc.0`, `v0.X.0-rc.1`, ... `v0.X.0-rc.N`). +- [ ] Generate and edit release notes in CHANGELOG.md. -- [ ] Automated testing - - [ ] Ensures js-waku tests are green against release candidate - - [ ] Ask Vac-QA and Vac-DST to perform available tests against release candidate - - [ ] Vac-QA - - [ ] Vac-DST (we need additional report. see [this](https://www.notion.so/DST-Reports-1228f96fb65c80729cd1d98a7496fe6f)) +- [ ] **Validation of release candidate** - - [ ] **On Waku fleets** - - [ ] Lock `waku.test` fleet to release candidate version - - [ ] Continuously stress `waku.test` fleet for a week (e.g. from `wakudev`) - - [ ] Search _Kibana_ logs from the previous month (since last release was deployed), for possible crashes or errors in `waku.test` and `waku.sandbox`. - - Most relevant logs are `(fleet: "waku.test" OR fleet: "waku.sandbox") AND message: "SIGSEGV"` - - [ ] Run release candidate with `waku-simulator`, ensure that nodes connected to each other - - [ ] Unlock `waku.test` to resume auto-deployment of latest `master` commit + - [ ] **Automated testing** + - [ ] Ensure all the unit tests (specifically logos-messaging-js tests) are green against the release candidate. - - [ ] **On Status fleet** - - [ ] Deploy release candidate to `status.staging` + - [ ] **QA testing** + - [ ] Ask QA to run their available tests against the release candidate. + + - [ ] **Waku fleet testing** + - [ ] Deploy the release candidate to `waku.test` fleet. + - Start the [deployment job](https://ci.infra.status.im/job/nim-waku/) and wait for it to finish (Jenkins access required; ask the infra team if you don't have it). + - After completion, disable fleet so that daily CI does not override your release candidate. + - Verify at https://fleets.waku.org/ that the fleet is locked to the release candidate image. + - Confirm the container image exists on [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab). + - [ ] Search [Kibana logs](https://kibana.infra.status.im/app/discover) from the previous month (since the last release was deployed) for possible crashes or errors in `waku.test`. + - Set time range to "Last 30 days" (or since last release). + - Most relevant search query: `(fleet: "waku.test" AND message: "SIGSEGV")`, `(fleet: "waku.test" AND message: "exception")`, `(fleet: "waku.test" AND message: "error")`. + - Document any crashes or errors found. + - [ ] Ask QA to perform tests against `waku.test`, if any. Then, after that, review Kibana for possible issues or unexpected restart. + - [ ] Enable the `waku.test` fleet again to resume auto-deployment of the latest `master` commit. + + - [ ] **Status testing** + - [ ] Get QA approval to deploy a new version in `status.staging`. + - [ ] Deploy release candidate to `status.staging`. - [ ] Perform [sanity check](https://www.notion.so/How-to-test-Nwaku-on-Status-12c6e4b9bf06420ca868bd199129b425) and log results as comments in this issue. - - [ ] Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client. - - [ ] 1:1 Chats with each other - - [ ] Send and receive messages in a community - - [ ] Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store - - [ ] Perform checks based _end user impact_ - - [ ] Inform other (Waku and Status) CCs to point their instance to `status.staging` for a few days. Ping Status colleagues from their Discord server or [Status community](https://status.app/c/G3kAAMSQtb05kog3aGbr3kiaxN4tF5xy4BAGEkkLwILk2z3GcoYlm5hSJXGn7J3laft-tnTwDWmYJ18dP_3bgX96dqr_8E3qKAvxDf3NrrCMUBp4R9EYkQez9XSM4486mXoC3mIln2zc-TNdvjdfL9eHVZ-mGgs=#zQ3shZeEJqTC1xhGUjxuS4rtHSrhJ8vUYp64v6qWkLpvdy9L9) (not blocking point.) - - [ ] Ask Status-QA to perform sanity checks (as described above) + checks based on _end user impact_; do specify the version being tested - - [ ] Ask Status-QA or infra to run the automated Status e2e tests against `status.staging` - - [ ] Get other CCs sign-off: they comment on this PR "used app for a week, no problem", or problem reported, resolved and new RC - - [ ] **Get Status-QA sign-off**. Ensuring that `status.test` update will not disturb ongoing activities. + - [ ] Connect 2 instances to `status.staging` fleet, one in relay mode, the other one in light client mode. + - 1:1 Chats with each other. + - Send and receive messages in a community. + - Close one instance, send messages with second instance, reopen first instance and confirm messages sent while offline are retrieved from store. + - [ ] Perform checks based on _end user impact_ + - [ ] Inform other (Waku and Status) CCs to point their instances to `status.staging` for a few days. Ping Status colleagues on their Discord server or in the [Status community](https://status.app/c/G3kAAMSQtb05kog3aGbr3kiaxN4tF5xy4BAGEkkLwILk2z3GcoYlm5hSJXGn7J3laft-tnTwDWmYJ18dP_3bgX96dqr_8E3qKAvxDf3NrrCMUBp4R9EYkQez9XSM4486mXoC3mIln2zc-TNdvjdfL9eHVZ-mGgs=#zQ3shZeEJqTC1xhGUjxuS4rtHSrhJ8vUYp64v6qWkLpvdy9L9) (this is not a blocking point.) + - [ ] Ask QA to perform sanity checks (as described above) and checks based on _end user impact_; specify the version being tested + - [ ] Ask QA or infra to run the automated Status e2e tests against `status.staging` + - [ ] Get other CCs' sign-off: they should comment on this PR, e.g., "Used the app for a week, no problem." If problems are reported, resolve them and create a new RC. - [ ] **Proceed with release** - - [ ] Assign a release tag to the same commit that contains the validated release-candidate tag - - [ ] Create GitHub release - - [ ] Deploy the release to DockerHub - - [ ] Announce the release + - [ ] Assign a final release tag (`v0.X.0`) to the same commit that contains the validated release-candidate tag (e.g. `git tag -as v0.X.0 -m "final release."`). + - [ ] Update [logos-delivery-compose](https://github.com/logos-messaging/logos-delivery-compose) and [logos-delivery-simulator](https://github.com/logos-messaging/logos-delivery-simulator) according to the new release. + - [ ] Bump logos-delivery dependency in [logos-delivery-rust-bindings](https://github.com/logos-messaging/logos-delivery-rust-bindings) and make sure all examples and tests work. + - [ ] Bump logos-delivery dependency in [logos-delivery-go-bindings](https://github.com/logos-messaging/logos-delivery-go-bindings) and make sure all tests work. + - [ ] Create GitHub release (https://github.com/logos-messaging/logos-delivery/releases). + - [ ] Submit a PR to merge the release branch back to `master`. Make sure you use the option "Merge pull request (Create a merge commit)" to perform the merge. Ping repo admin if this option is not available. + - [ ] Create a deployment issue with the recently created release. -- [ ] **Promote release to fleets**. - - [ ] Update infra config with any deprecated arguments or changed options - - [ ] [Deploy final release to `waku.sandbox` fleet](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox) - - [ ] [Deploy final release to `status.staging` fleet](https://ci.infra.status.im/job/nim-waku/job/deploy-shards-staging/) - - [ ] [Deploy final release to `status.prod` fleet](https://ci.infra.status.im/job/nim-waku/job/deploy-shards-test/) +### Links -- [ ] **Post release** - - [ ] Submit a PR from the release branch to master. Important to commit the PR with "create a merge commit" option. - - [ ] Update waku-org/nwaku-compose with the new release version. - - [ ] Update version in js-waku repo. [update only this](https://github.com/waku-org/js-waku/blob/7c0ce7b2eca31cab837da0251e1e4255151be2f7/.github/workflows/ci.yml#L135) by submitting a PR. +- [Release process](https://github.com/logos-messaging/logos-delivery/blob/master/docs/contributors/release-process.md) +- [Release notes](https://github.com/logos-messaging/logos-delivery/blob/master/CHANGELOG.md) +- [Fleet ownership](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) +- [Infra-nim-waku](https://github.com/status-im/infra-nim-waku) +- [Jenkins](https://ci.infra.status.im/job/nim-waku/) +- [Fleets](https://fleets.waku.org/) +- [Harbor](https://harbor.status.im/harbor/projects/9/repositories/nwaku/artifacts-tab) +- [Kibana](https://kibana.infra.status.im/app/) diff --git a/.github/workflows/ci-daily.yml b/.github/workflows/ci-daily.yml new file mode 100644 index 000000000..a4cf39340 --- /dev/null +++ b/.github/workflows/ci-daily.yml @@ -0,0 +1,79 @@ +name: Daily logos-delivery CI + +on: + schedule: + - cron: '30 6 * * *' + +env: + NPROC: 2 + MAKEFLAGS: "-j${NPROC}" + NIMFLAGS: "--parallelBuild:${NPROC} --colors:off -d:chronicles_colors:none" + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, macos-15] + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + + name: build-${{ matrix.os }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Get submodules hash + id: submodules + run: | + echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + + - name: Cache submodules + uses: actions/cache@v3 + with: + path: | + vendor/ + .git/modules + key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + + - name: Make update + run: make update + + - name: Build binaries + run: make V=1 examples tools + + - name: Notify Discord + if: always() + env: + DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + run: | + STATUS="${{ job.status }}" + OS="${{ matrix.os }}" + REPO="${{ github.repository }}" + RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + + if [ "$STATUS" = "success" ]; then + COLOR=3066993 + TITLE="✅ CI Success" + else + COLOR=15158332 + TITLE="❌ CI Failed" + fi + + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{ + \"embeds\": [{ + \"title\": \"$TITLE\", + \"color\": $COLOR, + \"fields\": [ + {\"name\": \"Repository\", \"value\": \"$REPO\", \"inline\": true}, + {\"name\": \"OS\", \"value\": \"$OS\", \"inline\": true}, + {\"name\": \"Status\", \"value\": \"$STATUS\", \"inline\": true} + ], + \"url\": \"$RUN_URL\", + \"footer\": {\"text\": \"Daily logos-delivery CI\"} + }] + }" \ + "$DISCORD_WEBHOOK_URL" + diff --git a/.github/workflows/ci-nix.yml b/.github/workflows/ci-nix.yml new file mode 100644 index 000000000..7319f64aa --- /dev/null +++ b/.github/workflows/ci-nix.yml @@ -0,0 +1,39 @@ +name: ci / nix +permissions: + contents: read + pull-requests: read + checks: write +on: + pull_request: + branches: [master] + +jobs: + build: + strategy: + fail-fast: false + matrix: + system: + - aarch64-darwin + - x86_64-linux + nixpkg: + - liblogosdelivery + + include: + - system: aarch64-darwin + runs_on: [self-hosted, macOS, ARM64] + + - system: x86_64-linux + runs_on: [self-hosted, Linux, X64] + + name: '${{ matrix.system }} / ${{ matrix.nixpkg }}' + runs-on: ${{ matrix.runs_on }} + steps: + - uses: actions/checkout@v4 + + - name: 'Run Nix build for ${{ matrix.nixpkg }}' + shell: bash + run: nix build -L '.#${{ matrix.nixpkg }}' + + - name: 'Show result contents' + shell: bash + run: find result -type f diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cf64b66a..52d20157a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,8 @@ env: NPROC: 2 MAKEFLAGS: "-j${NPROC}" NIMFLAGS: "--parallelBuild:${NPROC} --colors:off -d:chronicles_colors:none" + NIM_VERSION: '2.2.4' + NIMBLE_VERSION: '0.22.3' jobs: changes: # changes detection @@ -30,10 +32,11 @@ jobs: filters: | common: - '.github/workflows/**' - - 'vendor/**' - - 'Makefile' + - 'nimble.lock' - 'waku.nimble' + - 'Makefile' - 'library/**' + - 'liblogosdelivery/**' v2: - 'waku/**' - 'apps/**' @@ -63,22 +66,37 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Get submodules hash - id: submodules - run: | - echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + - name: Install Nim ${{ env.NIM_VERSION }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Cache submodules + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Cache nimble deps + id: cache-nimbledeps uses: actions/cache@v3 with: path: | - vendor/ - .git/modules - key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: | + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps + make rebuild-bearssl-nimbledeps + touch nimbledeps/.nimble-setup - name: Build binaries - run: make V=1 QUICK_AND_DIRTY_COMPILER=1 all tools - + run: make V=1 all + build-windows: needs: changes if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' }} @@ -101,18 +119,33 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Get submodules hash - id: submodules - run: | - echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + - name: Install Nim ${{ env.NIM_VERSION }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Cache submodules + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Cache nimble deps + id: cache-nimbledeps uses: actions/cache@v3 with: path: | - vendor/ - .git/modules - key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: | + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps + make rebuild-bearssl-nimbledeps + touch nimbledeps/.nimble-setup - name: Run tests run: | @@ -121,23 +154,23 @@ jobs: sudo docker run --rm -d -e POSTGRES_PASSWORD=test123 -p 5432:5432 postgres:15.4-alpine3.18 postgres_enabled=1 fi - + export MAKEFLAGS="-j1" export NIMFLAGS="--colors:off -d:chronicles_colors:none" export USE_LIBBACKTRACE=0 - make V=1 LOG_LEVEL=DEBUG QUICK_AND_DIRTY_COMPILER=1 POSTGRES=$postgres_enabled test - make V=1 LOG_LEVEL=DEBUG QUICK_AND_DIRTY_COMPILER=1 POSTGRES=$postgres_enabled testwakunode2 + make V=1 POSTGRES=$postgres_enabled test + make V=1 POSTGRES=$postgres_enabled testwakunode2 build-docker-image: needs: changes if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' || needs.changes.outputs.docker == 'true' }} - uses: waku-org/nwaku/.github/workflows/container-image.yml@master + uses: ./.github/workflows/container-image.yml secrets: inherit nwaku-nwaku-interop-tests: needs: build-docker-image - uses: waku-org/waku-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_0.0.1 + uses: logos-messaging/logos-delivery-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_STABLE with: node_nwaku: ${{ needs.build-docker-image.outputs.image }} @@ -145,14 +178,14 @@ jobs: js-waku-node: needs: build-docker-image - uses: waku-org/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-delivery-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node js-waku-node-optional: needs: build-docker-image - uses: waku-org/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-delivery-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node-optional @@ -165,18 +198,33 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Get submodules hash - id: submodules - run: | - echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + - name: Install Nim ${{ env.NIM_VERSION }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Cache submodules + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Cache nimble deps + id: cache-nimbledeps uses: actions/cache@v3 with: path: | - vendor/ - .git/modules - key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: | + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps + make rebuild-bearssl-nimbledeps + touch nimbledeps/.nimble-setup - name: Build nph run: | diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index cfa66d20a..0ff427d87 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -15,6 +15,8 @@ env: NPROC: 2 MAKEFLAGS: "-j${NPROC}" NIMFLAGS: "--parallelBuild:${NPROC}" + NIM_VERSION: '2.2.4' + NIMBLE_VERSION: '0.22.3' # This workflow should not run for outside contributors # If org secrets are not available, we'll avoid building and publishing the docker image and we'll pass the workflow @@ -41,32 +43,47 @@ jobs: env: QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }} QUAY_USER: ${{ secrets.QUAY_USER }} - + - name: Checkout code if: ${{ steps.secrets.outcome == 'success' }} uses: actions/checkout@v4 - - name: Get submodules hash - id: submodules + - name: Install Nim ${{ env.NIM_VERSION }} + if: ${{ steps.secrets.outcome == 'success' }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Nimble ${{ env.NIMBLE_VERSION }} if: ${{ steps.secrets.outcome == 'success' }} run: | - echo "hash=$(git submodule status | awk '{print $1}' | sort | shasum -a 256 | sed 's/[ -]*//g')" >> $GITHUB_OUTPUT + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH - - name: Cache submodules + - name: Cache nimble deps if: ${{ steps.secrets.outcome == 'success' }} + id: cache-nimbledeps uses: actions/cache@v3 with: path: | - vendor/ - .git/modules - key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + nimbledeps/ + nimble.paths + key: ${{ runner.os }}-nimbledeps-nimble${{ env.NIMBLE_VERSION }}-${{ hashFiles('nimble.lock', 'BearSSL.mk', 'Nat.mk') }} + + - name: Install nimble deps + if: ${{ steps.secrets.outcome == 'success' && steps.cache-nimbledeps.outputs.cache-hit != 'true' }} + run: | + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps + make rebuild-bearssl-nimbledeps + touch nimbledeps/.nimble-setup - name: Build binaries id: build if: ${{ steps.secrets.outcome == 'success' }} run: | - - make -j${NPROC} V=1 QUICK_AND_DIRTY_COMPILER=1 NIMFLAGS="-d:disableMarchNative -d:postgres -d:chronicles_colors:none" wakunode2 + make -j${NPROC} V=1 POSTGRES=1 NIMFLAGS="-d:disableMarchNative -d:chronicles_colors:none" wakunode2 SHORT_REF=$(git rev-parse --short HEAD) diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index fe108e616..52a50adc8 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -47,7 +47,7 @@ jobs: - name: prep variables id: vars run: | - ARCH=${{matrix.arch}} + ARCH=${{matrix.arch}} echo "arch=${ARCH}" >> $GITHUB_OUTPUT @@ -63,11 +63,11 @@ jobs: run: | OS=$([[ "${{runner.os}}" == "macOS" ]] && echo "macosx" || echo "linux") - make QUICK_AND_DIRTY_COMPILER=1 V=1 CI=false NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" \ + make V=1 CI=false NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" \ update - make QUICK_AND_DIRTY_COMPILER=1 V=1 CI=false\ - NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" \ + make V=1 CI=false POSTGRES=1\ + NIMFLAGS="-d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" \ wakunode2\ chat2\ tools @@ -91,14 +91,14 @@ jobs: build-docker-image: needs: tag-name - uses: waku-org/nwaku/.github/workflows/container-image.yml@master + uses: logos-messaging/logos-delivery/.github/workflows/container-image.yml@master with: image_tag: ${{ needs.tag-name.outputs.tag }} secrets: inherit js-waku-node: needs: build-docker-image - uses: waku-org/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-delivery-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node @@ -106,7 +106,7 @@ jobs: js-waku-node-optional: needs: build-docker-image - uses: waku-org/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/logos-delivery-js/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node-optional @@ -150,7 +150,7 @@ jobs: -u $(id -u) \ docker.io/wakuorg/sv4git:latest \ release-notes ${RELEASE_NOTES_TAG} --previous $(git tag -l --sort -creatordate | grep -e "^v[0-9]*\.[0-9]*\.[0-9]*$") |\ - sed -E 's@#([0-9]+)@[#\1](https://github.com/waku-org/nwaku/issues/\1)@g' > release_notes.md + sed -E 's@#([0-9]+)@[#\1](https://github.com/logos-messaging/logos-delivery/issues/\1)@g' > release_notes.md sed -i "s/^## .*/Generated at $(date)/" release_notes.md diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml index c6cfbd680..77862d11b 100644 --- a/.github/workflows/release-assets.yml +++ b/.github/workflows/release-assets.yml @@ -4,14 +4,42 @@ on: push: tags: - 'v*' # "e.g. v0.4" - + workflow_dispatch: env: NPROC: 2 jobs: + # Release gate: the pushed tag MUST exactly match waku.nimble's version, + # so every published artifact reports the correct getNodeInfo Version. + # CI cannot reject/remove a tag, so we gate artifact build & upload on + # this instead: a mismatched tag yields no released artifacts. + verify-version: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - name: Assert pushed tag equals waku.nimble version + if: startsWith(github.ref, 'refs/tags/') + run: | + set -euo pipefail + NIMBLE_VERSION=$(grep -m1 '^version = ' waku.nimble | sed -E 's/version = "([^"]+)"/\1/') + # Strip leading v and any prerelease suffix (e.g. v0.38.0-rc.1 -> + # 0.38.0) so release-candidate tags build against the same + # waku.nimble version as the final tag. + TAG_VERSION="${GITHUB_REF_NAME#v}" + BASE_VERSION="${TAG_VERSION%%-*}" + echo "tag: ${GITHUB_REF_NAME} (base ${BASE_VERSION})" + echo "waku.nimble version: ${NIMBLE_VERSION}" + if [ "${BASE_VERSION}" != "${NIMBLE_VERSION}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} (base ${BASE_VERSION}) does not match" + echo "::error::waku.nimble version (${NIMBLE_VERSION}). Bump waku.nimble before tagging." + exit 1 + fi + echo "OK: tag base matches waku.nimble." + build-and-upload: + needs: verify-version strategy: matrix: os: [ubuntu-22.04, macos-15] @@ -41,25 +69,130 @@ jobs: .git/modules key: ${{ runner.os }}-${{matrix.arch}}-submodules-${{ steps.submodules.outputs.hash }} - - name: prep variables + - name: Get tag + id: version + run: | + # Use full tag, e.g., v0.37.0 + echo "version=${GITHUB_REF_NAME}" >> $GITHUB_OUTPUT + + - name: Prep variables id: vars run: | - NWAKU_ARTIFACT_NAME=$(echo "nwaku-${{matrix.arch}}-${{runner.os}}.tar.gz" | tr "[:upper:]" "[:lower:]") + VERSION=${{ steps.version.outputs.version }} - echo "nwaku=${NWAKU_ARTIFACT_NAME}" >> $GITHUB_OUTPUT + NWAKU_ARTIFACT_NAME=$(echo "waku-${{matrix.arch}}-${{runner.os}}.tar.gz" | tr "[:upper:]" "[:lower:]") + echo "waku=${NWAKU_ARTIFACT_NAME}" >> $GITHUB_OUTPUT - - name: Install dependencies + if [[ "${{ runner.os }}" == "Linux" ]]; then + LIBWAKU_ARTIFACT_NAME=$(echo "libwaku-${VERSION}-${{matrix.arch}}-${{runner.os}}-linux.deb" | tr "[:upper:]" "[:lower:]") + fi + + if [[ "${{ runner.os }}" == "macOS" ]]; then + LIBWAKU_ARTIFACT_NAME=$(echo "libwaku-${VERSION}-${{matrix.arch}}-macos.tar.gz" | tr "[:upper:]" "[:lower:]") + fi + + echo "libwaku=${LIBWAKU_ARTIFACT_NAME}" >> $GITHUB_OUTPUT + + if [[ "${{ runner.os }}" == "Linux" ]]; then + LIBLOGOSDELIVERY_ARTIFACT_NAME=$(echo "liblogosdelivery-${VERSION}-${{matrix.arch}}-${{runner.os}}-linux.deb" | tr "[:upper:]" "[:lower:]") + fi + + if [[ "${{ runner.os }}" == "macOS" ]]; then + LIBLOGOSDELIVERY_ARTIFACT_NAME=$(echo "liblogosdelivery-${VERSION}-${{matrix.arch}}-macos.tar.gz" | tr "[:upper:]" "[:lower:]") + fi + + echo "liblogosdelivery=${LIBLOGOSDELIVERY_ARTIFACT_NAME}" >> $GITHUB_OUTPUT + + - name: Install build dependencies + run: | + if [[ "${{ runner.os }}" == "Linux" ]]; then + sudo apt-get update && sudo apt-get install -y build-essential dpkg-dev + fi + + - name: Build Waku artifacts run: | OS=$([[ "${{runner.os}}" == "macOS" ]] && echo "macosx" || echo "linux") make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" V=1 update - make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" CI=false wakunode2 + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false wakunode2 make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" CI=false chat2 - tar -cvzf ${{steps.vars.outputs.nwaku}} ./build/ + tar -cvzf ${{steps.vars.outputs.waku}} ./build/ - - name: Upload asset + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false libwaku + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false STATIC=1 libwaku + + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false liblogosdelivery + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}}" POSTGRES=1 CI=false STATIC=1 liblogosdelivery + + - name: Create distributable libwaku package + run: | + VERSION=${{ steps.version.outputs.version }} + + if [[ "${{ runner.os }}" == "Linux" ]]; then + rm -rf pkg + mkdir -p pkg/DEBIAN pkg/usr/local/lib pkg/usr/local/include + cp build/libwaku.so pkg/usr/local/lib/ + cp build/libwaku.a pkg/usr/local/lib/ + cp library/libwaku.h pkg/usr/local/include/ + + echo "Package: waku" >> pkg/DEBIAN/control + echo "Version: ${VERSION}" >> pkg/DEBIAN/control + echo "Priority: optional" >> pkg/DEBIAN/control + echo "Section: libs" >> pkg/DEBIAN/control + echo "Architecture: ${{matrix.arch}}" >> pkg/DEBIAN/control + echo "Maintainer: Waku Team " >> pkg/DEBIAN/control + echo "Description: Waku library" >> pkg/DEBIAN/control + + dpkg-deb --build pkg ${{steps.vars.outputs.libwaku}} + fi + + if [[ "${{ runner.os }}" == "macOS" ]]; then + tar -cvzf ${{steps.vars.outputs.libwaku}} ./build/libwaku.dylib ./build/libwaku.a ./library/libwaku.h + fi + + - name: Create distributable liblogosdelivery package + run: | + VERSION=${{ steps.version.outputs.version }} + + if [[ "${{ runner.os }}" == "Linux" ]]; then + rm -rf pkg + mkdir -p pkg/DEBIAN pkg/usr/local/lib pkg/usr/local/include + cp build/liblogosdelivery.so pkg/usr/local/lib/ + cp build/liblogosdelivery.a pkg/usr/local/lib/ + cp liblogosdelivery/liblogosdelivery.h pkg/usr/local/include/ + + echo "Package: logosdelivery" >> pkg/DEBIAN/control + echo "Version: ${VERSION}" >> pkg/DEBIAN/control + echo "Priority: optional" >> pkg/DEBIAN/control + echo "Section: libs" >> pkg/DEBIAN/control + echo "Architecture: ${{matrix.arch}}" >> pkg/DEBIAN/control + echo "Maintainer: Logos Messaging Team" >> pkg/DEBIAN/control + echo "Description: Logos Delivery library" >> pkg/DEBIAN/control + + dpkg-deb --build pkg ${{steps.vars.outputs.liblogosdelivery}} + fi + + if [[ "${{ runner.os }}" == "macOS" ]]; then + tar -cvzf ${{steps.vars.outputs.liblogosdelivery}} ./build/liblogosdelivery.dylib ./build/liblogosdelivery.a ./liblogosdelivery/liblogosdelivery.h + fi + + - name: Upload waku artifact uses: actions/upload-artifact@v4.4.0 with: - name: ${{steps.vars.outputs.nwaku}} - path: ${{steps.vars.outputs.nwaku}} + name: waku-${{ steps.version.outputs.version }}-${{ matrix.arch }}-${{ runner.os }} + path: ${{ steps.vars.outputs.waku }} + if-no-files-found: error + + - name: Upload libwaku artifact + uses: actions/upload-artifact@v4.4.0 + with: + name: libwaku-${{ steps.version.outputs.version }}-${{ matrix.arch }}-${{ runner.os }} + path: ${{ steps.vars.outputs.libwaku }} + if-no-files-found: error + + - name: Upload liblogosdelivery artifact + uses: actions/upload-artifact@v4.4.0 + with: + name: liblogosdelivery-${{ steps.version.outputs.version }}-${{ matrix.arch }}-${{ runner.os }} + path: ${{ steps.vars.outputs.liblogosdelivery }} if-no-files-found: error diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 000000000..e3ad958ba --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,49 @@ +name: version check +permissions: + contents: read + +on: + pull_request: + branches: [master] + +jobs: + # PR check: waku.nimble version must be >= the nearest tag reachable from + # this branch (`git describe --tags --abbrev=0`, i.e. ancestor-aware). + # Because we check out the PR HEAD (not the simulated merge ref), a branch + # that predates a release tag does not see that tag in its history, so a + # newly pushed tag does NOT break in-flight PRs. Once the branch merges/ + # rebases past the tag, the bump is then enforced. This keeps waku.nimble + # fixed as early as possible, independent of whether a release is cut. + # The exact tag==nimble guarantee at release time lives in + # release-assets.yml, which gates artifact publishing on it. + nimble-not-behind-tag: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + - name: Compare waku.nimble version with nearest ancestor tag + run: | + set -euo pipefail + NIMBLE_VERSION=$(grep -m1 '^version = ' waku.nimble | sed -E 's/version = "([^"]+)"/\1/') + # Nearest tag reachable from HEAD; --abbrev=0 drops the --g + # suffix so we get the bare tag (e.g. v0.38.0). + BASE_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + BASE_TAG=${BASE_TAG#v} + # Compare on the base version, ignoring any -rc.N prerelease suffix. + BASE_TAG=${BASE_TAG%%-*} + echo "waku.nimble version: ${NIMBLE_VERSION}" + echo "ancestor git tag: ${BASE_TAG:-}" + if [ -z "${BASE_TAG}" ]; then + echo "No ancestor release tag; skipping." + exit 0 + fi + # lowest of the two by version sort must be the tag => nimble >= tag + LOWEST=$(printf '%s\n%s\n' "${NIMBLE_VERSION}" "${BASE_TAG}" | sort -V | head -1) + if [ "${LOWEST}" != "${BASE_TAG}" ] && [ "${NIMBLE_VERSION}" != "${BASE_TAG}" ]; then + echo "::error::waku.nimble version (${NIMBLE_VERSION}) is behind its" + echo "::error::ancestor git tag (v${BASE_TAG}). Bump 'version' in waku.nimble." + exit 1 + fi + echo "OK: waku.nimble is not behind its ancestor tag." diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index ed6d2cb17..50f1602cd 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -7,20 +7,25 @@ on: required: true type: string +env: + NPROC: 4 + NIM_VERSION: '2.2.4' + NIMBLE_VERSION: '0.22.3' + jobs: build: runs-on: windows-latest defaults: run: - shell: msys2 {0} + shell: msys2 {0} env: MSYSTEM: MINGW64 steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup MSYS2 uses: msys2/setup-msys2@v2 @@ -33,6 +38,7 @@ jobs: make cmake upx + unzip mingw-w64-x86_64-rust mingw-w64-x86_64-postgresql mingw-w64-x86_64-gcc @@ -44,50 +50,66 @@ jobs: mingw-w64-x86_64-cmake mingw-w64-x86_64-llvm mingw-w64-x86_64-clang - + mingw-w64-x86_64-nasm + + - name: Manually install nasm + run: | + bash scripts/install_nasm_in_windows.sh + source $HOME/.bashrc + - name: Add UPX to PATH run: | echo "/usr/bin:$PATH" >> $GITHUB_PATH echo "/mingw64/bin:$PATH" >> $GITHUB_PATH echo "/usr/lib:$PATH" >> $GITHUB_PATH - echo "/mingw64/lib:$PATH" >> $GITHUB_PATH + echo "/mingw64/lib:$PATH" >> $GITHUB_PATH - name: Verify dependencies run: | - which upx gcc g++ make cmake cargo rustc python + which upx gcc g++ make cmake cargo rustc python nasm - - name: Updating submodules - run: git submodule update --init --recursive + - name: Install Nim ${{ env.NIM_VERSION }} + uses: jiro4989/setup-nim-action@v2 + with: + nim-version: ${{ env.NIM_VERSION }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Nimble ${{ env.NIMBLE_VERSION }} + run: | + export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$PATH" + cd /tmp && nimble install "nimble@${{ env.NIMBLE_VERSION }}" -y + echo "$HOME/.nimble/bin" >> $GITHUB_PATH + + - name: Patch nimble.lock for Windows nim checksum + # nimble.exe uses Windows Git (core.autocrlf=true by default), which converts LF→CRLF + # on checkout. This changes the SHA1 of the nim package source tree relative to the + # Linux-computed checksum stored in nimble.lock. Patch the lock file with the + # Windows-computed checksum before nimble reads it. + run: | + sed -i 's/68bb85cbfb1832ce4db43943911b046c3af3caab/a092a045d3a427d127a5334a6e59c76faff54686/g' nimble.lock + + - name: Install nimble deps + if: steps.cache-nimbledeps.outputs.cache-hit != 'true' + run: | + export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH" + nimble setup --localdeps -y + make rebuild-nat-libs-nimbledeps CC=gcc + make rebuild-bearssl-nimbledeps CC=gcc + touch nimbledeps/.nimble-setup - name: Creating tmp directory run: mkdir -p tmp - - name: Building Nim - run: | - cd vendor/nimbus-build-system/vendor/Nim - ./build_all.bat - cd ../../../.. - - - name: Building miniupnpc - run: | - cd vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc - make -f Makefile.mingw CC=gcc CXX=g++ libminiupnpc.a V=1 - cd ../../../../.. - - - name: Building libnatpmp - run: | - cd ./vendor/nim-nat-traversal/vendor/libnatpmp-upstream - make CC="gcc -fPIC -D_WIN32_WINNT=0x0600 -DNATPMP_STATICLIB" libnatpmp.a V=1 - cd ../../../../ - - name: Building wakunode2.exe run: | - make wakunode2 LOG_LEVEL=DEBUG V=3 -j8 + export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH" + make wakunode2 V=3 -j${{ env.NPROC }} - name: Building libwaku.dll run: | - make libwaku STATIC=0 LOG_LEVEL=DEBUG V=1 -j - + export PATH="$GITHUB_WORKSPACE/.nim_runtime/bin:$HOME/.nimble/bin:$PATH" + make libwaku STATIC=0 V=1 -j + - name: Check Executable run: | if [ -f "./build/wakunode2.exe" ]; then @@ -101,4 +123,4 @@ jobs: else echo "Build failed: libwaku.dll not found" exit 1 - fi + fi \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7430c3e99..750d0a00b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,6 @@ # Executables shall be put in an ignored build/ directory /build -# Nimble packages -/vendor/.nimble - # Generated Files *.generated.nim @@ -45,9 +42,6 @@ node_modules/ rlnKeystore.json *.tar.gz -# Nimbus Build System -nimbus-build-system.paths - # sqlite db *.db *.db-shm @@ -59,6 +53,10 @@ nimbus-build-system.paths /examples/nodejs/build/ /examples/rust/target/ +# Xcode user data +xcuserdata/ +*.xcuserstate + # Coverage coverage_html_report/ @@ -79,3 +77,12 @@ waku_handler.moc.cpp # Nix build result result + +# llms +AGENTS.md +nimble.develop +nimble.paths +nimbledeps + +**/anvil_state/state-deployed-contracts-mint-and-approved.json +.gitnexus diff --git a/.gitmodules b/.gitmodules index b7e52550a..ac07235b8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,186 +1,10 @@ -[submodule "vendor/nim-eth"] - path = vendor/nim-eth - url = https://github.com/status-im/nim-eth.git - ignore = dirty - branch = master -[submodule "vendor/nim-secp256k1"] - path = vendor/nim-secp256k1 - url = https://github.com/status-im/nim-secp256k1.git - ignore = dirty - branch = master -[submodule "vendor/nim-libp2p"] - path = vendor/nim-libp2p - url = https://github.com/vacp2p/nim-libp2p.git - ignore = dirty - branch = master -[submodule "vendor/nim-stew"] - path = vendor/nim-stew - url = https://github.com/status-im/nim-stew.git - ignore = dirty - branch = master -[submodule "vendor/nimbus-build-system"] - path = vendor/nimbus-build-system - url = https://github.com/status-im/nimbus-build-system.git - ignore = dirty - branch = master -[submodule "vendor/nim-nat-traversal"] - path = vendor/nim-nat-traversal - url = https://github.com/status-im/nim-nat-traversal.git - ignore = dirty - branch = master -[submodule "vendor/nim-libbacktrace"] - path = vendor/nim-libbacktrace - url = https://github.com/status-im/nim-libbacktrace.git - ignore = dirty - branch = master -[submodule "vendor/nim-confutils"] - path = vendor/nim-confutils - url = https://github.com/status-im/nim-confutils.git - ignore = dirty - branch = master -[submodule "vendor/nim-chronicles"] - path = vendor/nim-chronicles - url = https://github.com/status-im/nim-chronicles.git - ignore = dirty - branch = master -[submodule "vendor/nim-faststreams"] - path = vendor/nim-faststreams - url = https://github.com/status-im/nim-faststreams.git - ignore = dirty - branch = master -[submodule "vendor/nim-chronos"] - path = vendor/nim-chronos - url = https://github.com/status-im/nim-chronos.git - ignore = dirty - branch = master -[submodule "vendor/nim-json-serialization"] - path = vendor/nim-json-serialization - url = https://github.com/status-im/nim-json-serialization.git - ignore = dirty - branch = master -[submodule "vendor/nim-serialization"] - path = vendor/nim-serialization - url = https://github.com/status-im/nim-serialization.git - ignore = dirty - branch = master -[submodule "vendor/nimcrypto"] - path = vendor/nimcrypto - url = https://github.com/cheatfate/nimcrypto.git - ignore = dirty - branch = master -[submodule "vendor/nim-metrics"] - path = vendor/nim-metrics - url = https://github.com/status-im/nim-metrics.git - ignore = dirty - branch = master -[submodule "vendor/nim-stint"] - path = vendor/nim-stint - url = https://github.com/status-im/nim-stint.git - ignore = dirty - branch = master -[submodule "vendor/nim-json-rpc"] - path = vendor/nim-json-rpc - url = https://github.com/status-im/nim-json-rpc.git - ignore = dirty - branch = master -[submodule "vendor/nim-http-utils"] - path = vendor/nim-http-utils - url = https://github.com/status-im/nim-http-utils.git - ignore = dirty - branch = master -[submodule "vendor/nim-bearssl"] - path = vendor/nim-bearssl - url = https://github.com/status-im/nim-bearssl.git - ignore = dirty - branch = master -[submodule "vendor/nim-sqlite3-abi"] - path = vendor/nim-sqlite3-abi - url = https://github.com/arnetheduck/nim-sqlite3-abi.git - ignore = dirty - branch = master -[submodule "vendor/nim-web3"] - path = vendor/nim-web3 - url = https://github.com/status-im/nim-web3.git -[submodule "vendor/nim-testutils"] - path = vendor/nim-testutils - url = https://github.com/status-im/nim-testutils.git - ignore = untracked - branch = master -[submodule "vendor/nim-unittest2"] - path = vendor/nim-unittest2 - url = https://github.com/status-im/nim-unittest2.git - ignore = untracked - branch = master -[submodule "vendor/nim-websock"] - path = vendor/nim-websock - url = https://github.com/status-im/nim-websock.git - ignore = untracked - branch = main -[submodule "vendor/nim-zlib"] - path = vendor/nim-zlib - url = https://github.com/status-im/nim-zlib.git - ignore = untracked - branch = master -[submodule "vendor/nim-dnsdisc"] - path = vendor/nim-dnsdisc - url = https://github.com/status-im/nim-dnsdisc.git - ignore = untracked - branch = main -[submodule "vendor/dnsclient.nim"] - path = vendor/dnsclient.nim - url = https://github.com/ba0f3/dnsclient.nim.git - ignore = untracked - branch = master -[submodule "vendor/nim-toml-serialization"] - path = vendor/nim-toml-serialization - url = https://github.com/status-im/nim-toml-serialization.git -[submodule "vendor/nim-presto"] - path = vendor/nim-presto - url = https://github.com/status-im/nim-presto.git - ignore = untracked - branch = master [submodule "vendor/zerokit"] path = vendor/zerokit url = https://github.com/vacp2p/zerokit.git ignore = dirty branch = v0.5.1 -[submodule "vendor/nim-regex"] - path = vendor/nim-regex - url = https://github.com/nitely/nim-regex.git - ignore = untracked - branch = master -[submodule "vendor/nim-unicodedb"] - path = vendor/nim-unicodedb - url = https://github.com/nitely/nim-unicodedb.git - ignore = untracked - branch = master -[submodule "vendor/nim-taskpools"] - path = vendor/nim-taskpools - url = https://github.com/status-im/nim-taskpools.git - ignore = untracked - branch = stable -[submodule "vendor/nim-results"] - ignore = untracked - branch = master - path = vendor/nim-results - url = https://github.com/arnetheduck/nim-results.git -[submodule "vendor/db_connector"] - path = vendor/db_connector - url = https://github.com/nim-lang/db_connector.git - ignore = untracked - branch = devel -[submodule "vendor/nph"] - ignore = untracked - branch = master - path = vendor/nph - url = https://github.com/arnetheduck/nph.git -[submodule "vendor/nim-minilru"] - path = vendor/nim-minilru - url = https://github.com/status-im/nim-minilru.git - ignore = untracked - branch = master [submodule "vendor/waku-rlnv2-contract"] path = vendor/waku-rlnv2-contract - url = https://github.com/waku-org/waku-rlnv2-contract.git + url = https://github.com/logos-messaging/waku-rlnv2-contract.git ignore = untracked branch = master diff --git a/.nph.toml b/.nph.toml new file mode 100644 index 000000000..f0398059c --- /dev/null +++ b/.nph.toml @@ -0,0 +1,4 @@ +extend-exclude = [ + "vendor", + "nimbledeps", +] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..28e455f47 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,551 @@ +# AGENTS.md - AI Coding Context + +This file provides essential context for LLMs assisting with Logos Messaging development. + +## Project Identity + +Logos Messaging is designed as a shared public network for generalized messaging, not application-specific infrastructure. + +This project is a Nim implementation of a libp2p protocol suite for private, censorship-resistant P2P messaging. It targets resource-restricted devices and privacy-preserving communication. + +Logos Messaging was formerly known as Waku. Waku-related terminology remains within the codebase for historical reasons. + +### Design Philosophy + +Key architectural decisions: + +Resource-restricted first: Protocols differentiate between full nodes (relay) and light clients (filter, lightpush, store). Light clients can participate without maintaining full message history or relay capabilities. This explains the client/server split in protocol implementations. + +Privacy through unlinkability: RLN (Rate Limiting Nullifier) provides DoS protection while preserving sender anonymity. Messages are routed through pubsub topics with automatic sharding across 8 shards. Code prioritizes metadata privacy alongside content encryption. + +Scalability via sharding: The network uses automatic content-topic-based sharding to distribute traffic. This is why you'll see sharding logic throughout the codebase and why pubsub topic selection is protocol-level, not application-level. + +See [documentation](https://docs.waku.org/learn/) for architectural details. + +### Core Protocols +- Relay: Pub/sub message routing using GossipSub +- Store: Historical message retrieval and persistence +- Filter: Lightweight message filtering for resource-restricted clients +- Lightpush: Lightweight message publishing for clients +- Peer Exchange: Peer discovery mechanism +- RLN Relay: Rate limiting nullifier for spam protection +- Metadata: Cluster and shard metadata exchange between peers +- Mix: Mixnet protocol for enhanced privacy through onion routing +- Rendezvous: Alternative peer discovery mechanism + +### Key Terminology +- ENR (Ethereum Node Record): Node identity and capability advertisement +- Multiaddr: libp2p addressing format (e.g., `/ip4/127.0.0.1/tcp/60000/p2p/16Uiu2...`) +- PubsubTopic: Gossipsub topic for message routing (e.g., `/waku/2/default-waku/proto`) +- ContentTopic: Application-level message categorization (e.g., `/my-app/1/chat/proto`) +- Sharding: Partitioning network traffic across topics (static or auto-sharding) +- RLN (Rate Limiting Nullifier): Zero-knowledge proof system for spam prevention + +### Specifications +All specs are at [rfc.vac.dev/waku](https://rfc.vac.dev/waku). RFCs use `WAKU2-XXX` format (not legacy `WAKU-XXX`). + +## Architecture + +### Protocol Module Pattern +Each protocol typically follows this structure: +``` +waku_/ +├── protocol.nim # Main protocol type and handler logic +├── client.nim # Client-side API +├── rpc.nim # RPC message types +├── rpc_codec.nim # Protobuf encoding/decoding +├── common.nim # Shared types and constants +└── protocol_metrics.nim # Prometheus metrics +``` + +### WakuNode Architecture +- WakuNode (`waku/node/waku_node.nim`) is the central orchestrator +- Protocols are "mounted" onto the node's switch (libp2p component) +- PeerManager handles peer selection and connection management +- Switch provides libp2p transport, security, and multiplexing + +Example protocol type definition: +```nim +type WakuFilter* = ref object of LPProtocol + subscriptions*: FilterSubscriptions + peerManager: PeerManager + messageCache: TimedCache[string] +``` + +## Development Essentials + +### Build Requirements +- Nim 2.x (check `waku.nimble` for minimum version) +- Rust toolchain (required for RLN dependencies) +- Build system: Make with nimbus-build-system + +### Build System +The project uses Makefile with nimbus-build-system (Status's Nim build framework): +```bash +# Initial build (updates submodules) +make wakunode2 + +# After git pull, update submodules +make update + +# Build with custom flags +make wakunode2 NIMFLAGS="-d:chronicles_log_level=DEBUG" +``` + +Note: The build system uses `--mm:refc` memory management (automatically enforced). Only relevant if compiling outside the standard build system. + +### Common Make Targets +```bash +make wakunode2 # Build main node binary +make test # Run all tests +make testcommon # Run common tests only +make libwakuStatic # Build static C library +make chat2 # Build chat example +make install-nph # Install git hook for auto-formatting +``` + +### Testing +```bash +# Run all tests +make test + +# Run specific test file +make test tests/test_waku_enr.nim + +# Run specific test case from file +make test tests/test_waku_enr.nim "check capabilities support" + +# Build and run test separately (for development iteration) +make test tests/test_waku_enr.nim +``` + +Test structure uses `testutils/unittests`: +```nim +import testutils/unittests + +suite "Waku ENR - Capabilities": + test "check capabilities support": + ## Given + let bitfield: CapabilitiesBitfield = 0b0000_1101u8 + + ## Then + check: + bitfield.supportsCapability(Capabilities.Relay) + not bitfield.supportsCapability(Capabilities.Store) +``` + +### Code Formatting +Mandatory: All code must be formatted with `nph` (vendored in `vendor/nph`) +```bash +# Format specific file +make nph/waku/waku_core.nim + +# Install git pre-commit hook (auto-formats on commit) +make install-nph +``` +The nph formatter handles all formatting details automatically, especially with the pre-commit hook installed. Focus on semantic correctness. + +### Logging +Uses `chronicles` library with compile-time configuration: +```nim +import chronicles + +logScope: + topics = "waku lightpush" + +info "handling request", peerId = peerId, topic = pubsubTopic +error "request failed", error = msg +``` + +Compile with log level: +```bash +nim c -d:chronicles_log_level=TRACE myfile.nim +``` + + +## Code Conventions + +Common pitfalls: +- Always handle Result types explicitly +- Avoid global mutable state: Pass state through parameters +- Keep functions focused: Under 50 lines when possible +- Prefer compile-time checks (`static assert`) over runtime checks + +### Naming +- Files/Directories: `snake_case` (e.g., `waku_lightpush`, `peer_manager`) +- Procedures: `camelCase` (e.g., `handleRequest`, `pushMessage`) +- Types: `PascalCase` (e.g., `WakuFilter`, `PubsubTopic`) +- Constants: `PascalCase` (e.g., `MaxContentTopicsPerRequest`) +- Constructors: `func init(T: type Xxx, params): T` +- For ref types: `func new(T: type Xxx, params): ref T` +- Exceptions: `XxxError` for CatchableError, `XxxDefect` for Defect +- ref object types: `XxxRef` suffix + +### Imports Organization +Group imports: stdlib, external libs, internal modules: +```nim +import + std/[options, sequtils], # stdlib + results, chronicles, chronos, # external + libp2p/peerid +import + ../node/peer_manager, # internal (separate import block) + ../waku_core, + ./common +``` + +### Async Programming +Uses chronos, not stdlib `asyncdispatch`: +```nim +proc handleRequest( + wl: WakuLightPush, peerId: PeerId +): Future[WakuLightPushResult] {.async.} = + let res = await wl.pushHandler(peerId, pubsubTopic, message) + return res +``` + +### Error Handling +The project uses both Result types and exceptions: + +Result types from nim-results are used for protocol and API-level errors: +```nim +proc subscribe( + wf: WakuFilter, peerId: PeerID +): Future[FilterSubscribeResult] {.async.} = + if contentTopics.len > MaxContentTopicsPerRequest: + return err(FilterSubscribeError.badRequest("exceeds maximum")) + + # Handle Result with isOkOr + (await wf.subscriptions.addSubscription(peerId, criteria)).isOkOr: + return err(FilterSubscribeError.serviceUnavailable(error)) + + ok() +``` + +Exceptions still used for: +- chronos async failures (CancelledError, etc.) +- Database/system errors +- Library interop + +Most files start with `{.push raises: [].}` to disable exception tracking, then use try/catch blocks where needed. + +### Pragma Usage +```nim +{.push raises: [].} # Disable default exception tracking (at file top) + +proc myProc(): Result[T, E] {.async.} = # Async proc +``` + +### Protocol Inheritance +Protocols inherit from libp2p's `LPProtocol`: +```nim +type WakuLightPush* = ref object of LPProtocol + rng*: ref rand.HmacDrbgContext + peerManager*: PeerManager + pushHandler*: PushMessageHandler +``` + +### Type Visibility +- Public exports use `*` suffix: `type WakuFilter* = ...` +- Fields without `*` are module-private + +## Style Guide Essentials + +This section summarizes key Nim style guidelines relevant to this project. Full guide: https://status-im.github.io/nim-style-guide/ + +### Language Features + +Import and Export +- Use explicit import paths with std/ prefix for stdlib +- Group imports: stdlib, external, internal (separate blocks) +- Export modules whose types appear in public API +- Avoid include + +Macros and Templates +- Avoid macros and templates - prefer simple constructs +- Avoid generating public API with macros +- Put logic in templates, use macros only for glue code + +Object Construction +- Prefer Type(field: value) syntax +- Use Type.init(params) convention for constructors +- Default zero-initialization should be valid state +- Avoid using result variable for construction + +ref object Types +- Avoid ref object unless needed for: + - Resource handles requiring reference semantics + - Shared ownership + - Reference-based data structures (trees, lists) + - Stable pointer for FFI +- Use explicit ref MyType where possible +- Name ref object types with Ref suffix: XxxRef + +Memory Management +- Prefer stack-based and statically sized types in core code +- Use heap allocation in glue layers +- Avoid alloca +- For FFI: use create/dealloc or createShared/deallocShared + +Variable Usage +- Use most restrictive of const, let, var (prefer const over let over var) +- Prefer expressions for initialization over var then assignment +- Avoid result variable - use explicit return or expression-based returns + +Functions +- Prefer func over proc +- Avoid public (*) symbols not part of intended API +- Prefer openArray over seq for function parameters + +Methods (runtime polymorphism) +- Avoid method keyword for dynamic dispatch +- Prefer manual vtable with proc closures for polymorphism +- Methods lack support for generics + +Miscellaneous +- Annotate callback proc types with {.raises: [], gcsafe.} +- Avoid explicit {.inline.} pragma +- Avoid converters +- Avoid finalizers + +Type Guidelines + +Binary Data +- Use byte for binary data +- Use seq[byte] for dynamic arrays +- Convert string to seq[byte] early if stdlib returns binary as string + +Integers +- Prefer signed (int, int64) for counting, lengths, indexing +- Use unsigned with explicit size (uint8, uint64) for binary data, bit ops +- Avoid Natural +- Check ranges before converting to int +- Avoid casting pointers to int +- Avoid range types + +Strings +- Use string for text +- Use seq[byte] for binary data instead of string + +### Error Handling + +Philosophy +- Prefer Result, Opt for explicit error handling +- Use Exceptions only for legacy code compatibility + +Result Types +- Use Result[T, E] for operations that can fail +- Use cstring for simple error messages: Result[T, cstring] +- Use enum for errors needing differentiation: Result[T, SomeErrorEnum] +- Use Opt[T] for simple optional values +- Annotate all modules: {.push raises: [].} at top + +Exceptions (when unavoidable) +- Inherit from CatchableError, name XxxError +- Use Defect for panics/logic errors, name XxxDefect +- Annotate functions explicitly: {.raises: [SpecificError].} +- Catch specific error types, avoid catching CatchableError +- Use expression-based try blocks +- Isolate legacy exception code with try/except, convert to Result + +Common Defect Sources +- Overflow in signed arithmetic +- Array/seq indexing with [] +- Implicit range type conversions + +Status Codes +- Avoid status code pattern +- Use Result instead + +### Library Usage + +Standard Library +- Use judiciously, prefer focused packages +- Prefer these replacements: + - async: chronos + - bitops: stew/bitops2 + - endians: stew/endians2 + - exceptions: results + - io: stew/io2 + +Results Library +- Use cstring errors for diagnostics without differentiation +- Use enum errors when caller needs to act on specific errors +- Use complex types when additional error context needed +- Use isOkOr pattern for chaining + +Wrappers (C/FFI) +- Prefer native Nim when available +- For C libraries: use {.compile.} to build from source +- Create xxx_abi.nim for raw ABI wrapper +- Avoid C++ libraries + +Miscellaneous +- Print hex output in lowercase, accept both cases + +### Common Pitfalls + +- Defects lack tracking by {.raises.} +- nil ref causes runtime crashes +- result variable disables branch checking +- Exception hierarchy unclear between Nim versions +- Range types have compiler bugs +- Finalizers infect all instances of type + +## Common Workflows + +### Adding a New Protocol +1. Create directory: `waku/waku_myprotocol/` +2. Define core files: + - `rpc.nim` - Message types + - `rpc_codec.nim` - Protobuf encoding + - `protocol.nim` - Protocol handler + - `client.nim` - Client API + - `common.nim` - Shared types +3. Define protocol type in `protocol.nim`: + ```nim + type WakuMyProtocol* = ref object of LPProtocol + peerManager: PeerManager + # ... fields + ``` +4. Implement request handler +5. Mount in WakuNode (`waku/node/waku_node.nim`) +6. Add tests in `tests/waku_myprotocol/` +7. Export module via `waku/waku_myprotocol.nim` + +### Adding a REST API Endpoint +1. Define handler in `waku/rest_api/endpoint/myprotocol/` +2. Implement endpoint following pattern: + ```nim + proc installMyProtocolApiHandlers*( + router: var RestRouter, node: WakuNode + ) = + router.api(MethodGet, "/waku/v2/myprotocol/endpoint") do () -> RestApiResponse: + # Implementation + return RestApiResponse.jsonResponse(data, status = Http200) + ``` +3. Register in `waku/rest_api/handlers.nim` + +### Adding Database Migration +For message_store (SQLite): +1. Create `migrations/message_store/NNNNN_description.up.sql` +2. Create corresponding `.down.sql` for rollback +3. Increment version number sequentially +4. Test migration locally before committing + +For PostgreSQL: add in `migrations/message_store_postgres/` + +### Running Single Test During Development +```bash +# Build test binary +make test tests/waku_filter_v2/test_waku_client.nim + +# Binary location +./build/tests/waku_filter_v2/test_waku_client.nim.bin + +# Or combine +make test tests/waku_filter_v2/test_waku_client.nim "specific test name" +``` + +### Debugging with Chronicles +Set log level and filter topics: +```bash +nim c -r \ + -d:chronicles_log_level=TRACE \ + -d:chronicles_disabled_topics="eth,dnsdisc" \ + tests/mytest.nim +``` + +## Key Constraints + +### Vendor Directory +- Never edit files directly in vendor - it is auto-generated from git submodules +- Always run `make update` after pulling changes +- Managed by `nimbus-build-system` + +### Chronicles Performance +- Log levels are configured at compile time for performance +- Runtime filtering is available but should be used sparingly: `-d:chronicles_runtime_filtering=on` +- Default sinks are optimized for production + +### Memory Management +- Uses `refc` (reference counting with cycle collection) +- Automatically enforced by the build system (hardcoded in `waku.nimble`) +- Do not override unless absolutely necessary, as it breaks compatibility + +### RLN Dependencies +- RLN code requires a Rust toolchain, which explains Rust imports in some modules +- Pre-built `librln` libraries are checked into the repository + +## Quick Reference + +Language: Nim 2.x | License: MIT or Apache 2.0 + +### Important Files +- `Makefile` - Primary build interface +- `waku.nimble` - Package definition and build tasks (called via nimbus-build-system) +- `vendor/nimbus-build-system/` - Status's build framework +- `waku/node/waku_node.nim` - Core node implementation +- `apps/wakunode2/wakunode2.nim` - Main CLI application +- `waku/factory/waku_conf.nim` - Configuration types +- `library/libwaku.nim` - C bindings entry point + +### Testing Entry Points +- `tests/all_tests_waku.nim` - All Waku protocol tests +- `tests/all_tests_wakunode2.nim` - Node application tests +- `tests/all_tests_common.nim` - Common utilities tests + +### Key Dependencies +- `chronos` - Async framework +- `nim-results` - Result type for error handling +- `chronicles` - Logging +- `libp2p` - P2P networking +- `confutils` - CLI argument parsing +- `presto` - REST server +- `nimcrypto` - Cryptographic primitives + +Note: For specific version requirements, check `waku.nimble`. + + +# GitNexus — Code Intelligence + +This project is indexed by GitNexus as **logos-delivery** (2076 symbols, 2564 relationships, 12 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/logos-delivery/context` | Codebase overview, check index freshness | +| `gitnexus://repo/logos-delivery/clusters` | All functional areas | +| `gitnexus://repo/logos-delivery/processes` | All execution flows | +| `gitnexus://repo/logos-delivery/process/{name}` | Step-by-step execution trace | + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | + + diff --git a/BearSSL.mk b/BearSSL.mk new file mode 100644 index 000000000..355e46563 --- /dev/null +++ b/BearSSL.mk @@ -0,0 +1,46 @@ +# Copyright (c) 2022 Status Research & Development GmbH. Licensed under +# either of: +# - Apache License, version 2.0 +# - MIT license +# at your option. This file may not be copied, modified, or distributed except +# according to those terms. + +########################### +## bearssl (nimbledeps) ## +########################### +# Rebuilds libbearssl.a from the package installed by nimble under +# nimbledeps/pkgs2/. Used by `make update` / $(NIMBLEDEPS_STAMP). +# +# BEARSSL_NIMBLEDEPS_DIR is evaluated at parse time, so targets that +# depend on it must be invoked via a recursive $(MAKE) call so the sub-make +# re-evaluates the variable after nimble setup has populated nimbledeps/. +# +# `ls -dt` (sort by modification time, newest first) is used to pick the +# latest installed version and is portable across Linux, macOS, and +# Windows (MSYS/MinGW). + +BEARSSL_NIMBLEDEPS_DIR := $(shell ls -dt $(CURDIR)/nimbledeps/pkgs2/bearssl-* 2>/dev/null | head -1) +BEARSSL_CSOURCES_DIR := $(BEARSSL_NIMBLEDEPS_DIR)/bearssl/csources + +BEARSSL_UNAME_M := $(shell uname -m) +ifeq ($(BEARSSL_UNAME_M),x86_64) + PORTABLE_BEARSSL_CFLAGS := -W -Wall -Os -fPIC -mssse3 +else + PORTABLE_BEARSSL_CFLAGS := -W -Wall -Os -fPIC +endif + +.PHONY: clean-bearssl-nimbledeps rebuild-bearssl-nimbledeps + +clean-bearssl-nimbledeps: +ifeq ($(BEARSSL_NIMBLEDEPS_DIR),) + $(error No bearssl package found under nimbledeps/pkgs2/ — run 'make update' first) +endif + + [ -e "$(BEARSSL_CSOURCES_DIR)/build" ] && \ + "$(MAKE)" -C "$(BEARSSL_CSOURCES_DIR)" clean || true + +rebuild-bearssl-nimbledeps: | clean-bearssl-nimbledeps +ifeq ($(BEARSSL_NIMBLEDEPS_DIR),) + $(error No bearssl package found under nimbledeps/pkgs2/ — run 'make update' first) +endif + @echo "Rebuilding bearssl from $(BEARSSL_CSOURCES_DIR)" + + "$(MAKE)" -C "$(BEARSSL_CSOURCES_DIR)" CFLAGS="$(PORTABLE_BEARSSL_CFLAGS)" lib \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dc073792c..2ba15fcd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,175 @@ +## v0.38.1 (2026-05-07) + +### Changes + +- Evict peer instead of abrupt disconnect and avoid sending unnecessary store requests ([#3857](https://github.com/logos-messaging/logos-delivery/pull/3857)) ([75dbeb1b](https://github.com/logos-messaging/logos-delivery/commit/75dbeb1be785df5e61c9ab0bcf8393349b9d0f5e) and [7e59b2c2](https://github.com/logos-messaging/logos-delivery/commit/7e59b2c2)) + +- RecvService now delivers store-recovered messages via MessageReceivedEvent and includes check for missed hashes before processing ([#3805](https://github.com/logos-messaging/logos-delivery/pull/3805)) ([494ea946](https://github.com/logos-messaging/logos-delivery/commit/494ea946)) + +## v0.38.0 (2026-03-16) + +### Notes + +- **liblogosdelivery**: Major new FFI API with debug API, health status events, message received events, stateful SubscriptionService, and improved resource management. +- Waku Kademlia discovery integrated with Mix protocol. +- Context-aware and event-driven broker architecture introduced. +- REST Store API now defaults to page size 20 with max 100. +- Lightpush no longer mounts without relay enabled. +- Repository renamed from `logos-messaging-nim` to `logos-delivery`. + +### Features + +- liblogosdelivery: FFI library of new API ([#3714](https://github.com/logos-messaging/logos-delivery/pull/3714)) ([3603b838](https://github.com/logos-messaging/logos-delivery/commit/3603b838)) +- liblogosdelivery: health status event support ([#3737](https://github.com/logos-messaging/logos-delivery/pull/3737)) ([ba85873f](https://github.com/logos-messaging/logos-delivery/commit/ba85873f)) +- liblogosdelivery: MessageReceivedEvent propagation over FFI ([#3747](https://github.com/logos-messaging/logos-delivery/pull/3747)) ([0ad55159](https://github.com/logos-messaging/logos-delivery/commit/0ad55159)) +- liblogosdelivery: add debug API ([#3742](https://github.com/logos-messaging/logos-delivery/pull/3742)) ([09618a26](https://github.com/logos-messaging/logos-delivery/commit/09618a26)) +- liblogosdelivery: implement stateful SubscriptionService for Core mode ([#3732](https://github.com/logos-messaging/logos-delivery/pull/3732)) ([51ec09c3](https://github.com/logos-messaging/logos-delivery/commit/51ec09c3)) +- Waku Kademlia integration and Mix protocol updates ([#3722](https://github.com/logos-messaging/logos-delivery/pull/3722)) ([335600eb](https://github.com/logos-messaging/logos-delivery/commit/335600eb)) +- Waku API: implement Health spec ([#3689](https://github.com/logos-messaging/logos-delivery/pull/3689)) ([1fb4d1ea](https://github.com/logos-messaging/logos-delivery/commit/1fb4d1ea)) +- Waku API: send ([#3669](https://github.com/logos-messaging/logos-delivery/pull/3669)) ([1fd25355](https://github.com/logos-messaging/logos-delivery/commit/1fd25355)) +- iOS compilation support (WIP) ([#3668](https://github.com/logos-messaging/logos-delivery/pull/3668)) ([96196ab8](https://github.com/logos-messaging/logos-delivery/commit/96196ab8)) +- Distribute libwaku binaries ([#3612](https://github.com/logos-messaging/logos-delivery/pull/3612)) ([9e2b3830](https://github.com/logos-messaging/logos-delivery/commit/9e2b3830)) +- Rendezvous: broadcast and discover WakuPeerRecords ([#3617](https://github.com/logos-messaging/logos-delivery/pull/3617)) ([b0cd75f4](https://github.com/logos-messaging/logos-delivery/commit/b0cd75f4)) +- New postgres metric to estimate payload stats ([#3596](https://github.com/logos-messaging/logos-delivery/pull/3596)) ([454b098a](https://github.com/logos-messaging/logos-delivery/commit/454b098a)) + +### Bug Fixes + +- Fix NodeHealthMonitor logspam ([#3743](https://github.com/logos-messaging/logos-delivery/pull/3743)) ([7e36e268](https://github.com/logos-messaging/logos-delivery/commit/7e36e268)) +- Fix peer selection by shard and rendezvous/metadata sharding initialization ([#3718](https://github.com/logos-messaging/logos-delivery/pull/3718)) ([84f79110](https://github.com/logos-messaging/logos-delivery/commit/84f79110)) +- Correct dynamic library extension on mac and update OS detection ([#3754](https://github.com/logos-messaging/logos-delivery/pull/3754)) ([1ace0154](https://github.com/logos-messaging/logos-delivery/commit/1ace0154)) +- Force FINALIZE partition detach after detecting shorter error ([#3728](https://github.com/logos-messaging/logos-delivery/pull/3728)) ([b38b5aae](https://github.com/logos-messaging/logos-delivery/commit/b38b5aae)) +- Fix store protocol issue in v0.37.0 ([#3657](https://github.com/logos-messaging/logos-delivery/pull/3657)) ([91b4c5f5](https://github.com/logos-messaging/logos-delivery/commit/91b4c5f5)) +- Fix hash inputs for external nullifier, remove length prefix for sha256 ([#3660](https://github.com/logos-messaging/logos-delivery/pull/3660)) ([2d40cb9d](https://github.com/logos-messaging/logos-delivery/commit/2d40cb9d)) +- Fix admin API peer shards field from metadata protocol ([#3594](https://github.com/logos-messaging/logos-delivery/pull/3594)) ([e54851d9](https://github.com/logos-messaging/logos-delivery/commit/e54851d9)) +- Wakucanary exits with error if ping fails ([#3595](https://github.com/logos-messaging/logos-delivery/pull/3595), [#3711](https://github.com/logos-messaging/logos-delivery/pull/3711)) +- Force epoll in chronos for Android ([#3705](https://github.com/logos-messaging/logos-delivery/pull/3705)) ([beb1dde1](https://github.com/logos-messaging/logos-delivery/commit/beb1dde1)) +- Fix build_rln.sh script ([#3704](https://github.com/logos-messaging/logos-delivery/pull/3704)) ([09034837](https://github.com/logos-messaging/logos-delivery/commit/09034837)) +- liblogosdelivery: move destroy API to node_api, add security checks and fix possible resource leak ([#3736](https://github.com/logos-messaging/logos-delivery/pull/3736)) ([db19da92](https://github.com/logos-messaging/logos-delivery/commit/db19da92)) + +### Changes + +- Context-aware brokers architecture ([#3674](https://github.com/logos-messaging/logos-delivery/pull/3674)) ([c27405b1](https://github.com/logos-messaging/logos-delivery/commit/c27405b1)) +- Introduce EventBroker, RequestBroker and MultiRequestBroker ([#3644](https://github.com/logos-messaging/logos-delivery/pull/3644)) ([ae74b901](https://github.com/logos-messaging/logos-delivery/commit/ae74b901)) +- Use chronos' TokenBucket ([#3670](https://github.com/logos-messaging/logos-delivery/pull/3670)) ([284a0816](https://github.com/logos-messaging/logos-delivery/commit/284a0816)) +- REST Store API constraints: default page size 20, max 100 ([#3602](https://github.com/logos-messaging/logos-delivery/pull/3602)) ([8c30a8e1](https://github.com/logos-messaging/logos-delivery/commit/8c30a8e1)) +- Do not mount lightpush without relay ([#3540](https://github.com/logos-messaging/logos-delivery/pull/3540)) ([7d1c6aba](https://github.com/logos-messaging/logos-delivery/commit/7d1c6aba)) +- Mix: use exit==dest approach ([#3642](https://github.com/logos-messaging/logos-delivery/pull/3642)) ([088e3108](https://github.com/logos-messaging/logos-delivery/commit/088e3108)) +- Mix: simple refactor to reduce duplicated logs ([#3752](https://github.com/logos-messaging/logos-delivery/pull/3752)) ([96f1c40a](https://github.com/logos-messaging/logos-delivery/commit/96f1c40a)) +- Simplify NodeHealthMonitor creation ([#3716](https://github.com/logos-messaging/logos-delivery/pull/3716)) ([a8bdbca9](https://github.com/logos-messaging/logos-delivery/commit/a8bdbca9)) +- Adapt CLI args for delivery API ([#3744](https://github.com/logos-messaging/logos-delivery/pull/3744)) ([1f9c4cb8](https://github.com/logos-messaging/logos-delivery/commit/1f9c4cb8)) +- Adapt debugapi to WakoNodeConf ([#3745](https://github.com/logos-messaging/logos-delivery/pull/3745)) ([4a6ad732](https://github.com/logos-messaging/logos-delivery/commit/4a6ad732)) +- Bump nim-ffi to v0.1.3 ([#3696](https://github.com/logos-messaging/logos-delivery/pull/3696)) ([a02aaab5](https://github.com/logos-messaging/logos-delivery/commit/a02aaab5)) +- Bump nim-metrics to v0.2.1 ([#3734](https://github.com/logos-messaging/logos-delivery/pull/3734)) ([c7e0cc0e](https://github.com/logos-messaging/logos-delivery/commit/c7e0cc0e)) +- Add gasprice overflow check ([#3636](https://github.com/logos-messaging/logos-delivery/pull/3636)) ([a8590a0a](https://github.com/logos-messaging/logos-delivery/commit/a8590a0a)) +- Pin RLN dependencies to specific version ([#3649](https://github.com/logos-messaging/logos-delivery/pull/3649)) ([834eea94](https://github.com/logos-messaging/logos-delivery/commit/834eea94)) +- Update CI/README references after repository rename to logos-delivery ([#3729](https://github.com/logos-messaging/logos-delivery/pull/3729)) ([895f3e2d](https://github.com/logos-messaging/logos-delivery/commit/895f3e2d)) +- Simplify on chain group manager error handling ([#3678](https://github.com/logos-messaging/logos-delivery/pull/3678)) ([bc9454db](https://github.com/logos-messaging/logos-delivery/commit/bc9454db)) +- Extend RequestBroker with support for native/external types and sync requests ([#3665](https://github.com/logos-messaging/logos-delivery/pull/3665)) ([33233255](https://github.com/logos-messaging/logos-delivery/commit/33233255)) + +### This release supports the following [libp2p protocols](https://docs.libp2p.io/concepts/protocols/): + +| Protocol | Spec status | Protocol id | +| ---: | :---: | :--- | +| [`11/WAKU2-RELAY`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/11/relay.md) | `stable` | `/vac/waku/relay/2.0.0` | +| [`12/WAKU2-FILTER`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/12/filter.md) | `draft` | `/vac/waku/filter/2.0.0-beta1`
`/vac/waku/filter-subscribe/2.0.0-beta1`
`/vac/waku/filter-push/2.0.0-beta1` | +| [`13/WAKU2-STORE`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/13/store.md) | `draft` | `/vac/waku/store/2.0.0-beta4` | +| [`19/WAKU2-LIGHTPUSH`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) | `draft` | `/vac/waku/lightpush/2.0.0-beta1` | +| [`WAKU2-LIGHTPUSH v3`](https://github.com/waku-org/specs/blob/master/standards/core/lightpush.md) | `draft` | `/vac/waku/lightpush/3.0.0` | +| [`66/WAKU2-METADATA`](https://github.com/waku-org/specs/blob/master/standards/core/metadata.md) | `raw` | `/vac/waku/metadata/1.0.0` | +| [`WAKU-SYNC`](https://github.com/waku-org/specs/blob/master/standards/core/sync.md) | `draft` | `/vac/waku/sync/1.0.0` | + +## v0.37.4 (2026-04-03) + +### Changes + +- Optimize release builds for speed ([#3735](https://github.com/logos-messaging/logos-delivery/pull/3735)) ([#3777](https://github.com/logos-messaging/logos-delivery/pull/3777)) + +### Bug Fixes + +- Properly add DEBUG flag into Dockerfile + +## v0.37.3 (2026-03-25) + +### Features + +- Allow override user-message-rate-limit ([#3778](https://github.com/logos-messaging/logos-delivery/pull/3778)) + +## v0.37.2 (2026-03-19) + +### Features + +- Allow union of several retention policies ([#3766](https://github.com/logos-messaging/logos-delivery/pull/3766)) + +### Bug Fixes + +- Bump nim-http-utils to v0.4.1 to allow accepting <:><(> as a valid header and tests to validate html rfc7230 ([#43](https://github.com/status-im/nim-http-utils/pull/43)) + +## v0.37.1 (2026-03-12) + +### Bug Fixes + +- Avoid IndexDefect if DB error message is short ([#3725](https://github.com/logos-messaging/logos-delivery/pull/3725)) +- Remove ENR cache from peer exchange ([#3652](https://github.com/logos-messaging/logos-messaging-nim/pull/3652)) ([7920368a](https://github.com/logos-messaging/logos-messaging-nim/commit/7920368a36687cd5f12afa52d59866792d8457ca)) + +## v0.37.0 (2025-10-01) + +### Notes + +- Deprecated parameters: + - `tree_path` and `rlnDB` (RLN-related storage paths) + - `--dns-discovery` (fully removed, including dns-discovery-name-server) + - `keepAlive` (deprecated, config updated accordingly) +- Legacy `store` protocol is no longer supported by default. +- Improved sharding configuration: now explicit and shard-specific metrics added. +- Mix nodes are limited to IPv4 addresses only. +- [lightpush legacy](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) is being deprecated. Use [lightpush v3](https://github.com/waku-org/specs/blob/master/standards/core/lightpush.md) instead. + +### Features + +- Waku API: create node via API ([#3580](https://github.com/waku-org/nwaku/pull/3580)) ([bc8acf76](https://github.com/waku-org/nwaku/commit/bc8acf76)) +- Waku Sync: full topic support ([#3275](https://github.com/waku-org/nwaku/pull/3275)) ([9327da5a](https://github.com/waku-org/nwaku/commit/9327da5a)) +- Mix PoC implementation ([#3284](https://github.com/waku-org/nwaku/pull/3284)) ([eb7a3d13](https://github.com/waku-org/nwaku/commit/eb7a3d13)) +- Rendezvous: add request interval option ([#3569](https://github.com/waku-org/nwaku/pull/3569)) ([cc7a6406](https://github.com/waku-org/nwaku/commit/cc7a6406)) +- Shard-specific metrics tracking ([#3520](https://github.com/waku-org/nwaku/pull/3520)) ([c3da29fd](https://github.com/waku-org/nwaku/commit/c3da29fd)) +- Libwaku: build Windows DLL for Status-go ([#3460](https://github.com/waku-org/nwaku/pull/3460)) ([5c38a53f](https://github.com/waku-org/nwaku/commit/5c38a53f)) +- RLN: add Stateless RLN support ([#3621](https://github.com/waku-org/nwaku/pull/3621)) +- LOG: Reduce log level of messages from debug to info for better visibility ([#3622](https://github.com/waku-org/nwaku/pull/3622)) + +### Bug Fixes + +- Prevent invalid pubsub topic subscription via Relay REST API ([#3559](https://github.com/waku-org/nwaku/pull/3559)) ([a36601ab](https://github.com/waku-org/nwaku/commit/a36601ab)) +- Fixed node crash when RLN is unregistered ([#3573](https://github.com/waku-org/nwaku/pull/3573)) ([3d0c6279](https://github.com/waku-org/nwaku/commit/3d0c6279)) +- REST: fixed sync protocol issues ([#3503](https://github.com/waku-org/nwaku/pull/3503)) ([393e3cce](https://github.com/waku-org/nwaku/commit/393e3cce)) +- Regex pattern fix for `username:password@` in URLs ([#3517](https://github.com/waku-org/nwaku/pull/3517)) ([89a3f735](https://github.com/waku-org/nwaku/commit/89a3f735)) +- Sharding: applied modulus fix ([#3530](https://github.com/waku-org/nwaku/pull/3530)) ([f68d7999](https://github.com/waku-org/nwaku/commit/f68d7999)) +- Metrics: switched to counter instead of gauge ([#3355](https://github.com/waku-org/nwaku/pull/3355)) ([a27eec90](https://github.com/waku-org/nwaku/commit/a27eec90)) +- Fixed lightpush metrics and diagnostics ([#3486](https://github.com/waku-org/nwaku/pull/3486)) ([0ed3fc80](https://github.com/waku-org/nwaku/commit/0ed3fc80)) +- Misc sync, dashboard, and CI fixes ([#3434](https://github.com/waku-org/nwaku/pull/3434), [#3508](https://github.com/waku-org/nwaku/pull/3508), [#3464](https://github.com/waku-org/nwaku/pull/3464)) +- Raise log level of numerous operational messages from debug to info for better visibility ([#3622](https://github.com/waku-org/nwaku/pull/3622)) + +### Changes + +- Enable peer-exchange by default ([#3557](https://github.com/waku-org/nwaku/pull/3557)) ([7df526f8](https://github.com/waku-org/nwaku/commit/7df526f8)) +- Refactor peer-exchange client and service implementations ([#3523](https://github.com/waku-org/nwaku/pull/3523)) ([4379f9ec](https://github.com/waku-org/nwaku/commit/4379f9ec)) +- Updated rendezvous to use callback-based shard/capability updates ([#3558](https://github.com/waku-org/nwaku/pull/3558)) ([028bf297](https://github.com/waku-org/nwaku/commit/028bf297)) +- Config updates and explicit sharding setup ([#3468](https://github.com/waku-org/nwaku/pull/3468)) ([994d485b](https://github.com/waku-org/nwaku/commit/994d485b)) +- Bumped libp2p to v1.13.0 ([#3574](https://github.com/waku-org/nwaku/pull/3574)) ([b1616e55](https://github.com/waku-org/nwaku/commit/b1616e55)) +- Removed legacy dependencies (e.g., libpcre in Docker builds) ([#3552](https://github.com/waku-org/nwaku/pull/3552)) ([4db4f830](https://github.com/waku-org/nwaku/commit/4db4f830)) +- Benchmarks for RLN proof generation & verification ([#3567](https://github.com/waku-org/nwaku/pull/3567)) ([794c3a85](https://github.com/waku-org/nwaku/commit/794c3a85)) +- Various CI/CD & infra updates ([#3515](https://github.com/waku-org/nwaku/pull/3515), [#3505](https://github.com/waku-org/nwaku/pull/3505)) + +### This release supports the following [libp2p protocols](https://docs.libp2p.io/concepts/protocols/): + +| Protocol | Spec status | Protocol id | +| ---: | :---: | :--- | +| [`11/WAKU2-RELAY`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/11/relay.md) | `stable` | `/vac/waku/relay/2.0.0` | +| [`12/WAKU2-FILTER`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/12/filter.md) | `draft` | `/vac/waku/filter/2.0.0-beta1`
`/vac/waku/filter-subscribe/2.0.0-beta1`
`/vac/waku/filter-push/2.0.0-beta1` | +| [`13/WAKU2-STORE`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/13/store.md) | `draft` | `/vac/waku/store/2.0.0-beta4` | +| [`19/WAKU2-LIGHTPUSH`](https://github.com/vacp2p/rfc-index/blob/main/waku/standards/core/19/lightpush.md) | `draft` | `/vac/waku/lightpush/2.0.0-beta1` | +| [`WAKU2-LIGHTPUSH v3`](https://github.com/waku-org/specs/blob/master/standards/core/lightpush.md) | `draft` | `/vac/waku/lightpush/3.0.0` | +| [`66/WAKU2-METADATA`](https://github.com/waku-org/specs/blob/master/standards/core/metadata.md) | `raw` | `/vac/waku/metadata/1.0.0` | +| [`WAKU-SYNC`](https://github.com/waku-org/specs/blob/master/standards/core/sync.md) | `draft` | `/vac/waku/sync/1.0.0` | + ## v0.36.0 (2025-06-20) ### Notes diff --git a/Dockerfile b/Dockerfile index 90fb0a9c9..05525774b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,11 +4,11 @@ FROM rustlang/rust:nightly-alpine3.19 AS nim-build ARG NIMFLAGS ARG MAKE_TARGET=wakunode2 ARG NIM_COMMIT -ARG LOG_LEVEL=TRACE ARG HEAPTRACK_BUILD=0 +ARG POSTGRES=0 # Get build tools and required header files -RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq +RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq libbsd-dev WORKDIR /app COPY . . @@ -27,7 +27,7 @@ RUN if [ "$HEAPTRACK_BUILD" = "1" ]; then \ RUN make -j$(nproc) deps QUICK_AND_DIRTY_COMPILER=1 ${NIM_COMMIT} # Build the final node binary -RUN make -j$(nproc) ${NIM_COMMIT} $MAKE_TARGET LOG_LEVEL=${LOG_LEVEL} NIMFLAGS="${NIMFLAGS}" +RUN make -j$(nproc) ${NIM_COMMIT} $MAKE_TARGET NIMFLAGS="${NIMFLAGS}" POSTGRES=${POSTGRES} # PRODUCTION IMAGE ------------------------------------------------------------- @@ -46,7 +46,7 @@ LABEL version="unknown" EXPOSE 30303 60000 8545 # Referenced in the binary -RUN apk add --no-cache libgcc libpq-dev bind-tools +RUN apk add --no-cache libgcc libpq-dev bind-tools libstdc++ # Copy to separate location to accomodate different MAKE_TARGET values COPY --from=nim-build /app/build/$MAKE_TARGET /usr/local/bin/ diff --git a/Dockerfile.lightpushWithMix.compile b/Dockerfile.lightpushWithMix.compile index 381ee60ef..82e076b41 100644 --- a/Dockerfile.lightpushWithMix.compile +++ b/Dockerfile.lightpushWithMix.compile @@ -1,5 +1,5 @@ # BUILD NIM APP ---------------------------------------------------------------- -FROM rust:1.81.0-alpine3.19 AS nim-build +FROM rustlang/rust:nightly-alpine3.19 AS nim-build ARG NIMFLAGS ARG MAKE_TARGET=lightpushwithmix @@ -7,7 +7,7 @@ ARG NIM_COMMIT ARG LOG_LEVEL=TRACE # Get build tools and required header files -RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq +RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq libbsd-dev WORKDIR /app COPY . . @@ -24,7 +24,6 @@ RUN make -j$(nproc) deps QUICK_AND_DIRTY_COMPILER=1 ${NIM_COMMIT} # Build the final node binary RUN make -j$(nproc) ${NIM_COMMIT} $MAKE_TARGET LOG_LEVEL=${LOG_LEVEL} NIMFLAGS="${NIMFLAGS}" - # REFERENCE IMAGE as BASE for specialized PRODUCTION IMAGES---------------------------------------- FROM alpine:3.18 AS base_lpt @@ -44,8 +43,8 @@ RUN apk add --no-cache libgcc libpq-dev \ wget \ iproute2 \ python3 \ - jq - + jq \ + libstdc++ COPY --from=nim-build /app/build/lightpush_publisher_mix /usr/bin/ RUN chmod +x /usr/bin/lightpush_publisher_mix diff --git a/LICENSE-APACHEv2 b/LICENSE-APACHE similarity index 98% rename from LICENSE-APACHEv2 rename to LICENSE-APACHE index 7b6a3cb27..d64569567 100644 --- a/LICENSE-APACHEv2 +++ b/LICENSE-APACHE @@ -1,6 +1,3 @@ -nim-waku is licensed under the Apache License version 2 -Copyright (c) 2018 Status Research & Development GmbH ------------------------------------------------------ Apache License Version 2.0, January 2004 @@ -190,7 +187,7 @@ Copyright (c) 2018 Status Research & Development GmbH same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2018 Status Research & Development GmbH + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/LICENSE-MIT b/LICENSE-MIT index aab8020f0..d4c697062 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,25 +1,21 @@ -nim-waku is licensed under the MIT License -Copyright (c) 2018 Status Research & Development GmbH ------------------------------------------------------ - The MIT License (MIT) -Copyright (c) 2018 Status Research & Development GmbH +Copyright © 2025-2026 Logos Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal +of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile index 37341792c..f147c5e7e 100644 --- a/Makefile +++ b/Makefile @@ -4,28 +4,13 @@ # - MIT license # at your option. This file may not be copied, modified, or distributed except # according to those terms. -export BUILD_SYSTEM_DIR := vendor/nimbus-build-system -export EXCLUDED_NIM_PACKAGES := vendor/nim-dnsdisc/vendor + +include Nat.mk +include BearSSL.mk + LINK_PCRE := 0 FORMAT_MSG := "\\x1B[95mFormatting:\\x1B[39m" -# we don't want an error here, so we can handle things later, in the ".DEFAULT" target --include $(BUILD_SYSTEM_DIR)/makefiles/variables.mk - - -ifeq ($(NIM_PARAMS),) -# "variables.mk" was not included, so we update the submodules. -GIT_SUBMODULE_UPDATE := git submodule update --init --recursive -.DEFAULT: - +@ echo -e "Git submodules not found. Running '$(GIT_SUBMODULE_UPDATE)'.\n"; \ - $(GIT_SUBMODULE_UPDATE); \ - echo -# Now that the included *.mk files appeared, and are newer than this file, Make will restart itself: -# https://www.gnu.org/software/make/manual/make.html#Remaking-Makefiles -# -# After restarting, it will execute its original goal, so we don't have to start a child Make here -# with "$(MAKE) $(MAKECMDGOALS)". Isn't hidden control flow great? - -else # "variables.mk" was included. Business as usual until the end of this file. +BUILD_MSG := "Building:" # Determine the OS detected_OS := $(shell uname -s) @@ -33,25 +18,36 @@ ifneq (,$(findstring MINGW,$(detected_OS))) detected_OS := Windows endif +# Ensure the nim/nimble installed by install-nim/install-nimble are found first +export PATH := $(HOME)/.nimble/bin:$(PATH) + +# NIM binary location +NIM_BINARY := $(shell which nim 2>/dev/null) +NPH := $(HOME)/.nimble/bin/nph +NIMBLEDEPS_STAMP := nimbledeps/.nimble-setup + +# Compilation parameters +NIM_PARAMS ?= + ifeq ($(detected_OS),Windows) - # Update MINGW_PATH to standard MinGW location MINGW_PATH = /mingw64 NIM_PARAMS += --passC:"-I$(MINGW_PATH)/include" NIM_PARAMS += --passL:"-L$(MINGW_PATH)/lib" - NIM_PARAMS += --passL:"-Lvendor/nim-nat-traversal/vendor/miniupnp/miniupnpc" - NIM_PARAMS += --passL:"-Lvendor/nim-nat-traversal/vendor/libnatpmp-upstream" - - LIBS = -lws2_32 -lbcrypt -liphlpapi -luserenv -lntdll -lminiupnpc -lnatpmp -lpq + LIBS = -lws2_32 -lbcrypt -liphlpapi -luserenv -lntdll -lpq NIM_PARAMS += $(foreach lib,$(LIBS),--passL:"$(lib)") + NIM_PARAMS += --passL:"-Wl,--allow-multiple-definition" + export PATH := /c/msys64/usr/bin:/c/msys64/mingw64/bin:/c/msys64/usr/lib:/c/msys64/mingw64/lib:$(PATH) endif ########## ## Main ## ########## -.PHONY: all test update clean +.PHONY: all test update clean examples deps nimble install-nim install-nimble -# default target, because it's the first one that doesn't start with '.' -all: | wakunode2 example2 chat2 chat2bridge libwaku +# default target +all: | wakunode2 libwaku liblogosdelivery + +examples: | example2 chat2 chat2bridge test_file := $(word 2,$(MAKECMDGOALS)) define test_name @@ -65,94 +61,85 @@ ifeq ($(strip $(test_file)),) else $(MAKE) compile-test TEST_FILE="$(test_file)" TEST_NAME="$(call test_name)" endif -# this prevents make from erroring on unknown targets like "Index" + +# this prevents make from erroring on unknown targets %: @true waku.nims: ln -s waku.nimble $@ -update: | update-common - rm -rf waku.nims && \ - $(MAKE) waku.nims $(HANDLE_OUTPUT) +$(NIMBLEDEPS_STAMP): nimble.lock | waku.nims + $(MAKE) install-nimble + nimble setup --localdeps $(MAKE) build-nph + $(MAKE) rebuild-bearssl-nimbledeps + $(MAKE) rebuild-nat-libs-nimbledeps + touch $@ + +update: + rm -f $(NIMBLEDEPS_STAMP) + $(MAKE) $(NIMBLEDEPS_STAMP) + nimble lock clean: - rm -rf build + rm -rf build 2> /dev/null || true + rm -rf nimbledeps 2> /dev/null || true + rm -fr nimcache 2> /dev/null || true + rm nimble.paths 2> /dev/null || true + nimble clean -# must be included after the default target --include $(BUILD_SYSTEM_DIR)/makefiles/targets.mk +REQUIRED_NIM_VERSION := $(shell grep -E '^const RequiredNimVersion\s*=' waku.nimble | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') +REQUIRED_NIMBLE_VERSION := $(shell grep -E '^const RequiredNimbleVersion\s*=' waku.nimble | grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' | tr -d '"') + +install-nim: + scripts/install_nim.sh $(REQUIRED_NIM_VERSION) + +install-nimble: install-nim + @nimble_ver=$$(nimble --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1); \ + if [ "$$nimble_ver" = "$(REQUIRED_NIMBLE_VERSION)" ]; then \ + echo "nimble $(REQUIRED_NIMBLE_VERSION) already installed, skipping."; \ + else \ + cd $$(mktemp -d) && nimble install "nimble@$(REQUIRED_NIMBLE_VERSION)" -y; \ + fi + +build: + mkdir -p build + +nimble: install-nimble ## Possible values: prod; debug TARGET ?= prod ## Git version GIT_VERSION ?= $(shell git describe --abbrev=6 --always --tags) -## Compilation parameters. If defined in the CLI the assignments won't be executed NIM_PARAMS := $(NIM_PARAMS) -d:git_version=\"$(GIT_VERSION)\" ## Heaptracker options HEAPTRACKER ?= 0 HEAPTRACKER_INJECT ?= 0 ifeq ($(HEAPTRACKER), 1) -# Assumes Nim's lib/system/alloc.nim is patched! TARGET := debug-with-heaptrack - ifeq ($(HEAPTRACKER_INJECT), 1) -# the Nim compiler will load 'libheaptrack_inject.so' HEAPTRACK_PARAMS := -d:heaptracker -d:heaptracker_inject NIM_PARAMS := $(NIM_PARAMS) -d:heaptracker -d:heaptracker_inject else -# the Nim compiler will load 'libheaptrack_preload.so' HEAPTRACK_PARAMS := -d:heaptracker NIM_PARAMS := $(NIM_PARAMS) -d:heaptracker endif - -endif -## end of Heaptracker options - -################## -## Dependencies ## -################## -.PHONY: deps libbacktrace - -rustup: -ifeq (, $(shell which cargo)) -# Install Rustup if it's not installed -# -y: Assume "yes" for all prompts -# --default-toolchain stable: Install the stable toolchain - curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable endif -rln-deps: rustup - ./scripts/install_rln_tests_dependencies.sh - -deps: | deps-common nat-libs waku.nims - - -### nim-libbacktrace - -# "-d:release" implies "--stacktrace:off" and it cannot be added to config.nims +# Debug/Release mode ifeq ($(DEBUG), 0) NIM_PARAMS := $(NIM_PARAMS) -d:release else NIM_PARAMS := $(NIM_PARAMS) -d:debug endif -ifeq ($(USE_LIBBACKTRACE), 0) NIM_PARAMS := $(NIM_PARAMS) -d:disable_libbacktrace -endif -libbacktrace: - + $(MAKE) -C vendor/nim-libbacktrace --no-print-directory BUILD_CXX_LIB=0 - -clean-libbacktrace: - + $(MAKE) -C vendor/nim-libbacktrace clean $(HANDLE_OUTPUT) - -# Extend deps and clean targets -ifneq ($(USE_LIBBACKTRACE), 0) -deps: | libbacktrace -endif +# enable experimental exit is dest feature in libp2p mix +NIM_PARAMS := $(NIM_PARAMS) -d:libp2p_mix_experimental_exit_is_dest ifeq ($(POSTGRES), 1) NIM_PARAMS := $(NIM_PARAMS) -d:postgres -d:nimDebugDlOpen @@ -162,14 +149,26 @@ ifeq ($(DEBUG_DISCV5), 1) NIM_PARAMS := $(NIM_PARAMS) -d:debugDiscv5 endif -clean: | clean-libbacktrace +# Export NIM_PARAMS so nimble can access it +export NIM_PARAMS -### Create nimble links (used when building with Nix) +################## +## Dependencies ## +################## +.PHONY: deps -nimbus-build-system-nimble-dir: - NIMBLE_DIR="$(CURDIR)/$(NIMBLE_DIR)" \ - PWD_CMD="$(PWD)" \ - $(CURDIR)/scripts/generate_nimble_links.sh +FOUNDRY_VERSION := 1.5.0 +PNPM_VERSION := 10.23.0 + +rustup: +ifeq (, $(shell which cargo)) + curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable +endif + +rln-deps: rustup + ./scripts/install_rln_tests_dependencies.sh $(FOUNDRY_VERSION) $(PNPM_VERSION) + +deps: | nimble ################## ## RLN ## @@ -177,17 +176,18 @@ nimbus-build-system-nimble-dir: .PHONY: librln LIBRLN_BUILDDIR := $(CURDIR)/vendor/zerokit -LIBRLN_VERSION := v0.9.0 +LIBRLN_VERSION := v2.0.2 ifeq ($(detected_OS),Windows) -LIBRLN_FILE := rln.lib +LIBRLN_FILE ?= rln.lib else -LIBRLN_FILE := librln_$(LIBRLN_VERSION).a +LIBRLN_FILE ?= librln_$(LIBRLN_VERSION).a endif $(LIBRLN_FILE): + git submodule update --init vendor/zerokit echo -e $(BUILD_MSG) "$@" && \ - ./scripts/build_rln.sh $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(LIBRLN_FILE) + bash scripts/build_rln.sh $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(LIBRLN_FILE) librln: | $(LIBRLN_FILE) $(eval NIM_PARAMS += --passL:$(LIBRLN_FILE) --passL:-lm) @@ -196,7 +196,6 @@ clean-librln: cargo clean --manifest-path vendor/zerokit/rln/Cargo.toml rm -f $(LIBRLN_FILE) -# Extend clean target clean: | clean-librln ################# @@ -204,70 +203,71 @@ clean: | clean-librln ################# .PHONY: testcommon -testcommon: | build deps +testcommon: | $(NIMBLEDEPS_STAMP) build echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim testcommon $(NIM_PARAMS) waku.nims - + nimble testcommon ########## ## Waku ## ########## .PHONY: testwaku wakunode2 testwakunode2 example2 chat2 chat2bridge liteprotocoltester -# install rln-deps only for the testwaku target -testwaku: | build deps rln-deps librln +testwaku: | $(NIMBLEDEPS_STAMP) build rln-deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim test -d:os=$(shell uname) $(NIM_PARAMS) waku.nims + nimble test -wakunode2: | build deps librln +wakunode2: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - \ - $(ENV_SCRIPT) nim wakunode2 $(NIM_PARAMS) waku.nims + nimble wakunode2 -benchmarks: | build deps librln +benchmarks: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim benchmarks $(NIM_PARAMS) waku.nims + nimble benchmarks -testwakunode2: | build deps librln +testwakunode2: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim testwakunode2 $(NIM_PARAMS) waku.nims + nimble testwakunode2 -example2: | build deps librln +example2: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim example2 $(NIM_PARAMS) waku.nims + nimble example2 -chat2: | build deps librln +chat2: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim chat2 $(NIM_PARAMS) waku.nims + nimble chat2 -chat2mix: | build deps librln +chat2mix: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim chat2mix $(NIM_PARAMS) waku.nims + nimble chat2mix -rln-db-inspector: | build deps librln +rln-db-inspector: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim rln_db_inspector $(NIM_PARAMS) waku.nims + nimble rln_db_inspector -chat2bridge: | build deps librln +chat2bridge: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim chat2bridge $(NIM_PARAMS) waku.nims + nimble chat2bridge -liteprotocoltester: | build deps librln +liteprotocoltester: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim liteprotocoltester $(NIM_PARAMS) waku.nims + nimble liteprotocoltester -lightpushwithmix: | build deps librln +lightpushwithmix: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim lightpushwithmix $(NIM_PARAMS) waku.nims + nimble lightpushwithmix -build/%: | build deps librln +api_example: | $(NIMBLEDEPS_STAMP) build deps librln + echo -e $(BUILD_MSG) "build/$@" && \ + $(ENV_SCRIPT) nim api_example $(NIM_PARAMS) waku.nims + +build/%: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$*" && \ - $(ENV_SCRIPT) nim buildone $(NIM_PARAMS) waku.nims $* + nimble buildone $* -compile-test: | build deps librln +compile-test: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "$(TEST_FILE)" "\"$(TEST_NAME)\"" && \ - $(ENV_SCRIPT) nim buildTest $(NIM_PARAMS) waku.nims $(TEST_FILE) && \ - $(ENV_SCRIPT) nim execTest $(NIM_PARAMS) waku.nims $(TEST_FILE) "\"$(TEST_NAME)\""; \ + nimble buildTest $(TEST_FILE) && \ + nimble execTest $(TEST_FILE) "\"$(TEST_NAME)\"" ################ ## Waku tools ## @@ -276,29 +276,30 @@ compile-test: | build deps librln tools: networkmonitor wakucanary -wakucanary: | build deps librln +wakucanary: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim wakucanary $(NIM_PARAMS) waku.nims + nimble wakucanary -networkmonitor: | build deps librln +networkmonitor: | $(NIMBLEDEPS_STAMP) build deps librln echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim networkmonitor $(NIM_PARAMS) waku.nims + nimble networkmonitor ############ ## Format ## ############ -.PHONY: build-nph install-nph clean-nph print-nph-path - -# Default location for nph binary shall be next to nim binary to make it available on the path. -NPH:=$(shell dirname $(NIM_BINARY))/nph +.PHONY: build-nph install-nph print-nph-path build-nph: | build deps -ifeq ("$(wildcard $(NPH))","") - $(ENV_SCRIPT) nim c --skipParentCfg:on vendor/nph/src/nph.nim && \ - mv vendor/nph/src/nph $(shell dirname $(NPH)) - echo "nph utility is available at " $(NPH) +ifneq ($(detected_OS),Windows) + if command -v nph > /dev/null 2>&1; then \ + echo "nph already installed, skipping"; \ + else \ + echo "Installing nph globally"; \ + (cd /tmp && nimble install nph@0.7.0 --accept -g); \ + fi + command -v nph else - echo "nph utility already exists at " $(NPH) + echo "Skipping nph build on Windows (nph is only used on Unix-like systems)" endif GIT_PRE_COMMIT_HOOK := .git/hooks/pre-commit @@ -315,39 +316,30 @@ nph/%: | build-nph echo -e $(FORMAT_MSG) "nph/$*" && \ $(NPH) $* -clean-nph: - rm -f $(NPH) - -# To avoid hardcoding nph binary location in several places print-nph-path: - echo "$(NPH)" + @echo "$(NPH)" -clean: | clean-nph +clean: ################### ## Documentation ## ################### .PHONY: docs coverage -# TODO: Remove unused target docs: | build deps echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) nim doc --run --index:on --project --out:.gh-pages waku/waku.nim waku.nims + nimble doc --run --index:on --project --out:.gh-pages waku/waku.nim waku.nims coverage: echo -e $(BUILD_MSG) "build/$@" && \ - $(ENV_SCRIPT) ./scripts/run_cov.sh -y - + ./scripts/run_cov.sh -y ##################### ## Container image ## ##################### -# -d:insecure - Necessary to enable Prometheus HTTP endpoint for metrics -# -d:chronicles_colors:none - Necessary to disable colors in logs for Docker DOCKER_IMAGE_NIMFLAGS ?= -d:chronicles_colors:none -d:insecure -d:postgres DOCKER_IMAGE_NIMFLAGS := $(DOCKER_IMAGE_NIMFLAGS) $(HEAPTRACK_PARAMS) -# build a docker image for the fleet docker-image: MAKE_TARGET ?= wakunode2 docker-image: DOCKER_IMAGE_TAG ?= $(MAKE_TARGET)-$(GIT_VERSION) docker-image: DOCKER_IMAGE_NAME ?= wakuorg/nwaku:$(DOCKER_IMAGE_TAG) @@ -355,8 +347,6 @@ docker-image: docker build \ --build-arg="MAKE_TARGET=$(MAKE_TARGET)" \ --build-arg="NIMFLAGS=$(DOCKER_IMAGE_NIMFLAGS)" \ - --build-arg="NIM_COMMIT=$(DOCKER_NIM_COMMIT)" \ - --build-arg="LOG_LEVEL=$(LOG_LEVEL)" \ --build-arg="HEAPTRACK_BUILD=$(HEAPTRACKER)" \ --label="commit=$(shell git rev-parse HEAD)" \ --label="version=$(GIT_VERSION)" \ @@ -367,7 +357,7 @@ docker-quick-image: MAKE_TARGET ?= wakunode2 docker-quick-image: DOCKER_IMAGE_TAG ?= $(MAKE_TARGET)-$(GIT_VERSION) docker-quick-image: DOCKER_IMAGE_NAME ?= wakuorg/nwaku:$(DOCKER_IMAGE_TAG) docker-quick-image: NIM_PARAMS := $(NIM_PARAMS) -d:chronicles_colors:none -d:insecure -d:postgres --passL:$(LIBRLN_FILE) --passL:-lm -docker-quick-image: | build deps librln wakunode2 +docker-quick-image: | build librln wakunode2 docker build \ --build-arg="MAKE_TARGET=$(MAKE_TARGET)" \ --tag $(DOCKER_IMAGE_NAME) \ @@ -381,20 +371,14 @@ docker-push: #################################### ## Container lite-protocol-tester ## #################################### -# -d:insecure - Necessary to enable Prometheus HTTP endpoint for metrics -# -d:chronicles_colors:none - Necessary to disable colors in logs for Docker DOCKER_LPT_NIMFLAGS ?= -d:chronicles_colors:none -d:insecure -# build a docker image for the fleet docker-liteprotocoltester: DOCKER_LPT_TAG ?= latest docker-liteprotocoltester: DOCKER_LPT_NAME ?= wakuorg/liteprotocoltester:$(DOCKER_LPT_TAG) -# --no-cache docker-liteprotocoltester: docker build \ --build-arg="MAKE_TARGET=liteprotocoltester" \ --build-arg="NIMFLAGS=$(DOCKER_LPT_NIMFLAGS)" \ - --build-arg="NIM_COMMIT=$(DOCKER_NIM_COMMIT)" \ - --build-arg="LOG_LEVEL=TRACE" \ --label="commit=$(shell git rev-parse HEAD)" \ --label="version=$(GIT_VERSION)" \ --target $(if $(filter deploy,$(DOCKER_LPT_TAG)),deployment_lpt,standalone_lpt) \ @@ -413,37 +397,96 @@ docker-quick-liteprotocoltester: | liteprotocoltester docker-liteprotocoltester-push: docker push $(DOCKER_LPT_NAME) - ################ ## C Bindings ## ################ -.PHONY: cbindings cwaku_example libwaku +.PHONY: cbindings cwaku_example libwaku liblogosdelivery liblogosdelivery_example -STATIC ?= 0 - - -libwaku: | build deps librln - rm -f build/libwaku* - -ifeq ($(STATIC), 1) - echo -e $(BUILD_MSG) "build/$@.a" && $(ENV_SCRIPT) nim libwakuStatic $(NIM_PARAMS) waku.nims -else ifeq ($(detected_OS),Windows) - echo -e $(BUILD_MSG) "build/$@.dll" && $(ENV_SCRIPT) nim libwakuDynamic $(NIM_PARAMS) waku.nims +detected_OS ?= Linux +ifeq ($(OS),Windows_NT) +detected_OS := Windows else - echo -e $(BUILD_MSG) "build/$@.so" && $(ENV_SCRIPT) nim libwakuDynamic $(NIM_PARAMS) waku.nims +detected_OS := $(shell uname -s) endif +BUILD_COMMAND ?= Dynamic +STATIC ?= 0 +ifeq ($(STATIC), 1) + BUILD_COMMAND = Static +endif + +ifeq ($(detected_OS),Windows) + BUILD_COMMAND := $(BUILD_COMMAND)Windows +else ifeq ($(detected_OS),Darwin) + BUILD_COMMAND := $(BUILD_COMMAND)Mac + export IOS_SDK_PATH := $(shell xcrun --sdk iphoneos --show-sdk-path) +else ifeq ($(detected_OS),Linux) + BUILD_COMMAND := $(BUILD_COMMAND)Linux +endif + +libwaku: | $(NIMBLEDEPS_STAMP) librln + nimble --verbose libwaku$(BUILD_COMMAND) waku.nimble + +liblogosdelivery: | $(NIMBLEDEPS_STAMP) librln + nimble --verbose liblogosdelivery$(BUILD_COMMAND) waku.nimble + +logosdelivery_example: | build liblogosdelivery + @echo -e $(BUILD_MSG) "build/$@" +ifeq ($(detected_OS),Darwin) + gcc -o build/$@ \ + liblogosdelivery/examples/logosdelivery_example.c \ + liblogosdelivery/examples/json_utils.c \ + -I./liblogosdelivery \ + -L./build \ + -llogosdelivery \ + -Wl,-rpath,./build +else ifeq ($(detected_OS),Linux) + gcc -o build/$@ \ + liblogosdelivery/examples/logosdelivery_example.c \ + liblogosdelivery/examples/json_utils.c \ + -I./liblogosdelivery \ + -L./build \ + -llogosdelivery \ + -Wl,-rpath,'$$ORIGIN' +else ifeq ($(detected_OS),Windows) + gcc -o build/$@.exe \ + liblogosdelivery/examples/logosdelivery_example.c \ + liblogosdelivery/examples/json_utils.c \ + -I./liblogosdelivery \ + -L./build \ + -llogosdelivery \ + -lws2_32 +endif + +cwaku_example: | build libwaku + echo -e $(BUILD_MSG) "build/$@" && \ + cc -o "build/$@" \ + ./examples/cbindings/waku_example.c \ + ./examples/cbindings/base64.c \ + -lwaku -Lbuild/ \ + -pthread -ldl -lm + +cppwaku_example: | build libwaku + echo -e $(BUILD_MSG) "build/$@" && \ + g++ -o "build/$@" \ + ./examples/cpp/waku.cpp \ + ./examples/cpp/base64.cpp \ + -lwaku -Lbuild/ \ + -pthread -ldl -lm + +nodejswaku: | build deps + echo -e $(BUILD_MSG) "build/$@" && \ + node-gyp build --directory=examples/nodejs/ + ##################### ## Mobile Bindings ## ##################### .PHONY: libwaku-android \ - libwaku-android-precheck \ - libwaku-android-arm64 \ - libwaku-android-amd64 \ - libwaku-android-x86 \ - libwaku-android-arm \ - rebuild-nat-libs \ - build-libwaku-for-android-arch + libwaku-android-precheck \ + libwaku-android-arm64 \ + libwaku-android-amd64 \ + libwaku-android-x86 \ + libwaku-android-arm ANDROID_TARGET ?= 30 ifeq ($(detected_OS),Darwin) @@ -452,17 +495,19 @@ else ANDROID_TOOLCHAIN_DIR := $(ANDROID_NDK_HOME)/toolchains/llvm/prebuilt/linux-x86_64 endif -rebuild-nat-libs: | clean-cross nat-libs - libwaku-android-precheck: ifndef ANDROID_NDK_HOME - $(error ANDROID_NDK_HOME is not set) + $(error ANDROID_NDK_HOME is not set) endif build-libwaku-for-android-arch: - $(MAKE) rebuild-nat-libs CC=$(ANDROID_TOOLCHAIN_DIR)/bin/$(ANDROID_COMPILER) && \ - ./scripts/build_rln_android.sh $(CURDIR)/build $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(CROSS_TARGET) $(ABIDIR) && \ - CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_ARCH=$(ANDROID_ARCH) ANDROID_COMPILER=$(ANDROID_COMPILER) ANDROID_TOOLCHAIN_DIR=$(ANDROID_TOOLCHAIN_DIR) $(ENV_SCRIPT) nim libWakuAndroid $(NIM_PARAMS) waku.nims +ifneq ($(findstring /nix/store,$(LIBRLN_FILE)),) + mkdir -p $(CURDIR)/build/android/$(ABIDIR)/ + CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_ARCH=$(ANDROID_ARCH) ANDROID_COMPILER=$(ANDROID_COMPILER) ANDROID_TOOLCHAIN_DIR=$(ANDROID_TOOLCHAIN_DIR) nimble libWakuAndroid +else + ./scripts/build_rln_android.sh $(CURDIR)/build $(LIBRLN_BUILDDIR) $(LIBRLN_VERSION) $(CROSS_TARGET) $(ABIDIR) +endif + $(MAKE) rebuild-nat-libs-nimbledeps CC=$(ANDROID_TOOLCHAIN_DIR)/bin/$(ANDROID_COMPILER) libwaku-android-arm64: ANDROID_ARCH=aarch64-linux-android libwaku-android-arm64: CPU=arm64 @@ -486,47 +531,52 @@ libwaku-android-arm: ANDROID_ARCH=armv7a-linux-androideabi libwaku-android-arm: CPU=arm libwaku-android-arm: ABIDIR=armeabi-v7a libwaku-android-arm: | libwaku-android-precheck build deps -# cross-rs target architecture name does not match the one used in android $(MAKE) build-libwaku-for-android-arch ANDROID_ARCH=$(ANDROID_ARCH) CROSS_TARGET=armv7-linux-androideabi CPU=$(CPU) ABIDIR=$(ABIDIR) ANDROID_COMPILER=$(ANDROID_ARCH)$(ANDROID_TARGET)-clang libwaku-android: $(MAKE) libwaku-android-amd64 $(MAKE) libwaku-android-arm64 $(MAKE) libwaku-android-x86 -# This target is disabled because on recent versions of cross-rs complain with the following error -# relocation R_ARM_THM_ALU_PREL_11_0 cannot be used against symbol 'stack_init_trampoline_return'; recompile with -fPIC -# It's likely this architecture is not used so we might just not support it. -# $(MAKE) libwaku-android-arm -cwaku_example: | build libwaku - echo -e $(BUILD_MSG) "build/$@" && \ - cc -o "build/$@" \ - ./examples/cbindings/waku_example.c \ - ./examples/cbindings/base64.c \ - -lwaku -Lbuild/ \ - -pthread -ldl -lm \ - -lminiupnpc -Lvendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/build/ \ - -lnatpmp -Lvendor/nim-nat-traversal/vendor/libnatpmp-upstream/ \ - vendor/nim-libbacktrace/libbacktrace_wrapper.o \ - vendor/nim-libbacktrace/install/usr/lib/libbacktrace.a +################# +## iOS Bindings # +################# +.PHONY: libwaku-ios-precheck \ + libwaku-ios-device \ + libwaku-ios-simulator \ + libwaku-ios -cppwaku_example: | build libwaku - echo -e $(BUILD_MSG) "build/$@" && \ - g++ -o "build/$@" \ - ./examples/cpp/waku.cpp \ - ./examples/cpp/base64.cpp \ - -lwaku -Lbuild/ \ - -pthread -ldl -lm \ - -lminiupnpc -Lvendor/nim-nat-traversal/vendor/miniupnp/miniupnpc/build/ \ - -lnatpmp -Lvendor/nim-nat-traversal/vendor/libnatpmp-upstream/ \ - vendor/nim-libbacktrace/libbacktrace_wrapper.o \ - vendor/nim-libbacktrace/install/usr/lib/libbacktrace.a +IOS_DEPLOYMENT_TARGET ?= 18.0 -nodejswaku: | build deps - echo -e $(BUILD_MSG) "build/$@" && \ - node-gyp build --directory=examples/nodejs/ +define get_ios_sdk_path +$(shell xcrun --sdk $(1) --show-sdk-path 2>/dev/null) +endef -endif # "variables.mk" was not included +libwaku-ios-precheck: +ifeq ($(detected_OS),Darwin) + @command -v xcrun >/dev/null 2>&1 || { echo "Error: Xcode command line tools not installed"; exit 1; } +else + $(error iOS builds are only supported on macOS) +endif + +build-libwaku-for-ios-arch: + IOS_SDK=$(IOS_SDK) IOS_ARCH=$(IOS_ARCH) IOS_SDK_PATH=$(IOS_SDK_PATH) nimble libWakuIOS + +libwaku-ios-device: IOS_ARCH=arm64 +libwaku-ios-device: IOS_SDK=iphoneos +libwaku-ios-device: IOS_SDK_PATH=$(call get_ios_sdk_path,iphoneos) +libwaku-ios-device: | libwaku-ios-precheck build deps + $(MAKE) build-libwaku-for-ios-arch IOS_ARCH=$(IOS_ARCH) IOS_SDK=$(IOS_SDK) IOS_SDK_PATH=$(IOS_SDK_PATH) + +libwaku-ios-simulator: IOS_ARCH=arm64 +libwaku-ios-simulator: IOS_SDK=iphonesimulator +libwaku-ios-simulator: IOS_SDK_PATH=$(call get_ios_sdk_path,iphonesimulator) +libwaku-ios-simulator: | libwaku-ios-precheck build deps + $(MAKE) build-libwaku-for-ios-arch IOS_ARCH=$(IOS_ARCH) IOS_SDK=$(IOS_SDK) IOS_SDK_PATH=$(IOS_SDK_PATH) + +libwaku-ios: + $(MAKE) libwaku-ios-device + $(MAKE) libwaku-ios-simulator ################### # Release Targets # @@ -540,7 +590,4 @@ release-notes: -u $(shell id -u) \ docker.io/wakuorg/sv4git:latest \ release-notes |\ - sed -E 's@#([0-9]+)@[#\1](https://github.com/waku-org/nwaku/issues/\1)@g' -# I could not get the tool to replace issue ids with links, so using sed for now, -# asked here: https://github.com/bvieira/sv4git/discussions/101 - + sed -E 's@#([0-9]+)@[#\1](https://github.com/waku-org/nwaku/issues/\1)@g' \ No newline at end of file diff --git a/Nat.mk b/Nat.mk new file mode 100644 index 000000000..90d0b2ead --- /dev/null +++ b/Nat.mk @@ -0,0 +1,61 @@ +# Copyright (c) 2022 Status Research & Development GmbH. Licensed under +# either of: +# - Apache License, version 2.0 +# - MIT license +# at your option. This file may not be copied, modified, or distributed except +# according to those terms. + +########################### +## nat-libs (nimbledeps) ## +########################### +# Builds miniupnpc and libnatpmp from the package installed by nimble under +# nimbledeps/pkgs2/. Used by `make update` / $(NIMBLEDEPS_STAMP). +# +# NAT_TRAVERSAL_NIMBLEDEPS_DIR is evaluated at parse time, so targets that +# depend on it must be invoked via a recursive $(MAKE) call so the sub-make +# re-evaluates the variable after nimble setup has populated nimbledeps/. +# +# `ls -dt` (sort by modification time, newest first) is used to pick the +# latest installed version and is portable across Linux, macOS, and +# Windows (MSYS/MinGW). + +NAT_TRAVERSAL_NIMBLEDEPS_DIR := $(shell ls -dt $(CURDIR)/nimbledeps/pkgs2/nat_traversal-* 2>/dev/null | head -1) + +NAT_UNAME_M := $(shell uname -m) +ifeq ($(NAT_UNAME_M),x86_64) + PORTABLE_NAT_MARCH := -mssse3 +else + PORTABLE_NAT_MARCH := +endif + +.PHONY: clean-cross-nimbledeps rebuild-nat-libs-nimbledeps + +clean-cross-nimbledeps: +ifeq ($(NAT_TRAVERSAL_NIMBLEDEPS_DIR),) + $(error No nat_traversal package found under nimbledeps/pkgs2/ — run 'make update' first) +endif + + [ -e "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" ] && \ + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" CC=$(CC) clean $(HANDLE_OUTPUT) || true + + [ -e "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" ] && \ + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" CC=$(CC) clean $(HANDLE_OUTPUT) || true + +rebuild-nat-libs-nimbledeps: | clean-cross-nimbledeps +ifeq ($(NAT_TRAVERSAL_NIMBLEDEPS_DIR),) + $(error No nat_traversal package found under nimbledeps/pkgs2/ — run 'make update' first) +endif + @echo "Rebuilding nat-libs from $(NAT_TRAVERSAL_NIMBLEDEPS_DIR)" +ifeq ($(OS), Windows_NT) + + [ -e "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc/libminiupnpc.a" ] || \ + PATH=".;$${PATH}" "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" \ + -f Makefile.mingw CC=$(CC) CFLAGS="-Os -fPIC" libminiupnpc.a $(HANDLE_OUTPUT) + + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" \ + OS=mingw CC=$(CC) \ + CFLAGS="-Wall -Wno-cpp -Os -fPIC -DWIN32 -DNATPMP_STATICLIB -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4 $(CFLAGS)" \ + libnatpmp.a $(HANDLE_OUTPUT) +else + + "$(MAKE)" -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/miniupnp/miniupnpc" \ + CC=$(CC) CFLAGS="-Os -fPIC $(PORTABLE_NAT_MARCH)" build/libminiupnpc.a $(HANDLE_OUTPUT) + + "$(MAKE)" CFLAGS="-Wall -Wno-cpp -Os -fPIC $(PORTABLE_NAT_MARCH) -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4 $(CFLAGS)" \ + -C "$(NAT_TRAVERSAL_NIMBLEDEPS_DIR)/vendor/libnatpmp-upstream" \ + CC=$(CC) libnatpmp.a $(HANDLE_OUTPUT) +endif diff --git a/README.md b/README.md index ce352d6f5..f227ea483 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,36 @@ -# Nwaku +# Logos Messaging Nim ## Introduction -The nwaku repository implements Waku, and provides tools related to it. +This repository implements a set of libp2p protocols aimed to bring +private communications. -- A Nim implementation of the [Waku (v2) protocol](https://specs.vac.dev/specs/waku/v2/waku-v2.html). -- CLI application `wakunode2` that allows you to run a Waku node. -- Examples of Waku usage. +- Nim implementation of [these specs](https://github.com/logos-co/logos-lips/tree/master/docs/messaging). +- C library that exposes the implemented protocols. +- CLI application that allows you to run a logos-delivery node. +- Examples. - Various tests of above. For more details see the [source code](waku/README.md) ## How to Build & Run ( Linux, MacOS & WSL ) -These instructions are generic. For more detailed instructions, see the Waku source code above. +These instructions are generic. For more detailed instructions, see the source code above. + +Recommended and tested toolchain versions (these are installed when you follow the build instructions below): +- Nim 2.2.4 +- Nimble 0.22.3 ### Prerequisites -The standard developer tools, including a C compiler, GNU Make, Bash, and Git. More information on these installations can be found [here](https://docs.waku.org/guides/nwaku/build-source#install-dependencies). +The standard developer tools, including a C compiler, GNU Make, Bash, and Git. > In some distributions (Fedora linux for example), you may need to install `which` utility separately. Nimbus build system is relying on it. You'll also need an installation of Rust and its toolchain (specifically `rustc` and `cargo`). The easiest way to install these, is using `rustup`: +Rust: ```bash curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ``` @@ -31,8 +38,7 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh ### Wakunode ```bash -# The first `make` invocation will update all Git submodules. -# You'll run `make update` after each `git pull` in the future to keep those submodules updated. +# The first `make` invocation will initialize the local dependency state. make wakunode2 # Build with custom compilation flags. Do not use NIM_PARAMS unless you know what you are doing. @@ -46,12 +52,12 @@ make wakunode2 NIMFLAGS="-d:chronicles_colors:none -d:disableMarchNative" ./build/wakunode2 --help ``` To join the network, you need to know the address of at least one bootstrap node. -Please refer to the [Waku README](https://github.com/waku-org/nwaku/blob/master/waku/README.md) for more information. +Please refer to the [Waku README](https://github.com/logos-messaging/logos-delivery/blob/master/waku/README.md) for more information. For more on how to run `wakunode2`, refer to: -- [Run using binaries](https://docs.waku.org/guides/nwaku/build-source) -- [Run using docker](https://docs.waku.org/guides/nwaku/run-docker) -- [Run using docker-compose](https://docs.waku.org/guides/nwaku/run-docker-compose) +- [Run using binaries](https://docs.waku.org/run-node/build-source) +- [Run using docker](https://docs.waku.org/run-node/run-docker) +- [Run using docker-compose](https://docs.waku.org/run-node/run-docker-compose) #### Issues ##### WSL @@ -102,13 +108,9 @@ If `wakunode2.exe` isn't generated: This repository is bundled with a Nim runtime that includes the necessary dependencies for the project. Before you can utilize the runtime you'll need to build the project, as detailed in a previous section. -This will generate a `vendor` directory containing various dependencies, including the `nimbus-build-system` which has the bundled nim runtime. +This will generate a `nimbledeps/pkgs2` directory containing various dependencies. -After successfully building the project, you may bring the bundled runtime into scope by running: -```bash -source env.sh -``` -If everything went well, you should see your prompt suffixed with `[Nimbus env]$`. Now you can run `nim` commands as usual. +If everything went well, you should see your prompt suffixed with `[SuccessX]`. Now you can run `nim` commands as usual. ### Test Suite @@ -142,7 +144,7 @@ make test/tests/common/test_enr_builder.nim ``` ### Testing against `js-waku` -Refer to [js-waku repo](https://github.com/waku-org/js-waku/tree/master/packages/tests) for instructions. +Refer to [logos-delivery-js repo](https://github.com/logos-messaging/logos-delivery-js/tree/master/packages/tests) for instructions. ## Formatting @@ -173,14 +175,14 @@ Different tools and their corresponding how-to guides can be found in the `tools ### Bugs, Questions & Features -For an inquiry, or if you would like to propose new features, feel free to [open a general issue](https://github.com/waku-org/nwaku/issues/new). +For an inquiry, or if you would like to propose new features, feel free to [open a general issue](https://github.com/logos-messaging/logos-delivery/issues/new). -For bug reports, please [tag your issue with the `bug` label](https://github.com/waku-org/nwaku/issues/new). +For bug reports, please [tag your issue with the `bug` label](https://github.com/logos-messaging/logos-delivery/issues/new). -If you believe the reported issue requires critical attention, please [use the `critical` label](https://github.com/waku-org/nwaku/issues/new?labels=critical,bug) to assist with triaging. +If you believe the reported issue requires critical attention, please [use the `critical` label](https://github.com/logos-messaging/logos-delivery/issues/new?labels=critical,bug) to assist with triaging. -To get help, or participate in the conversation, join the [Waku Discord](https://discord.waku.org/) server. +To get help, or participate in the conversation, join the [Logos Discord](https://discord.gg/logosnetwork) server. ### Docs -* [REST API Documentation](https://waku-org.github.io/waku-rest-api/) +* [REST API Documentation](https://logos-messaging.github.io/logos-delivery-rest-api/) \ No newline at end of file diff --git a/apps/chat2/chat2.nim b/apps/chat2/chat2.nim index 6b52bc919..282e17bfd 100644 --- a/apps/chat2/chat2.nim +++ b/apps/chat2/chat2.nim @@ -13,7 +13,8 @@ import chronos, eth/keys, bearssl, - stew/[byteutils, results], + stew/[byteutils], + results, metrics, metrics/chronos_httpserver import @@ -36,7 +37,6 @@ import waku_lightpush_legacy/rpc, waku_enr, discovery/waku_dnsdisc, - waku_store_legacy, waku_node, node/waku_metrics, node/peer_manager, @@ -50,8 +50,7 @@ import import libp2p/protocols/pubsub/rpc/messages, libp2p/protocols/pubsub/pubsub import ../../waku/waku_rln_relay -const Help = - """ +const Help = """ Commands: /[?|help|connect|nick|exit] help: Prints this help connect: dials a remote peer @@ -317,27 +316,19 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = if conf.logLevel != LogLevel.NONE: setLogLevel(conf.logLevel) - let natRes = setupNat( + let (extIp, extTcpPort, extUdpPort) = setupNat( conf.nat, clientId, Port(uint16(conf.tcpPort) + conf.portsShift), Port(uint16(conf.udpPort) + conf.portsShift), - ) - - if natRes.isErr(): - raise newException(ValueError, "setupNat error " & natRes.error) - - let (extIp, extTcpPort, extUdpPort) = natRes.get() + ).valueOr: + raise newException(ValueError, "setupNat error " & error) var enrBuilder = EnrBuilder.init(nodeKey) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) let node = block: var builder = WakuNodeBuilder.init() @@ -345,16 +336,16 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = builder.withRecord(record) builder - .withNetworkConfigurationDetails( - conf.listenAddress, - Port(uint16(conf.tcpPort) + conf.portsShift), - extIp, - extTcpPort, - wsBindPort = Port(uint16(conf.websocketPort) + conf.portsShift), - wsEnabled = conf.websocketSupport, - wssEnabled = conf.websocketSecureSupport, - ) - .tryGet() + .withNetworkConfigurationDetails( + conf.listenAddress, + Port(uint16(conf.tcpPort) + conf.portsShift), + extIp, + extTcpPort, + wsBindPort = Port(uint16(conf.websocketPort) + conf.portsShift), + wsEnabled = conf.websocketSupport, + wssEnabled = conf.websocketSecureSupport, + ) + .tryGet() builder.build().tryGet() await node.start() @@ -488,7 +479,9 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = if conf.lightpushnode != "": let peerInfo = parsePeerInfo(conf.lightpushnode) if peerInfo.isOk(): - await mountLegacyLightPush(node) + (await node.mountLegacyLightPush()).isOkOr: + error "failed to mount legacy lightpush", error = error + quit(QuitFailure) node.mountLegacyLightPushClient() node.peerManager.addServicePeer(peerInfo.value, WakuLightpushCodec) else: diff --git a/apps/chat2/config_chat2.nim b/apps/chat2/config_chat2.nim index fe7865c62..b0e38c6bc 100644 --- a/apps/chat2/config_chat2.nim +++ b/apps/chat2/config_chat2.nim @@ -140,7 +140,8 @@ type metricsServerAddress* {. desc: "Listening address of the metrics server.", - defaultValue: parseIpAddress("127.0.0.1"), + defaultValue: + IpAddress(family: IpAddressFamily.IPv4, address_v4: [127'u8, 0, 0, 1]), name: "metrics-server-address" .}: IpAddress @@ -173,7 +174,10 @@ type dnsAddrsNameServers* {. desc: "DNS name server IPs to query for DNS multiaddrs resolution. Argument may be repeated.", - defaultValue: @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")], + defaultValue: @[ + IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 1, 1, 1]), + IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 0, 0, 1]), + ], name: "dns-addrs-name-server" .}: seq[IpAddress] @@ -348,4 +352,4 @@ proc parseCmdArg*(T: type EthRpcUrl, s: string): T = func defaultListenAddress*(conf: Chat2Conf): IpAddress = # TODO: How should we select between IPv4 and IPv6 # Maybe there should be a config option for this. - (static parseIpAddress("0.0.0.0")) + (static IpAddress(family: IpAddressFamily.IPv4, address_v4: [0'u8, 0, 0, 0])) diff --git a/apps/chat2bridge/chat2bridge.nim b/apps/chat2bridge/chat2bridge.nim index 2d7b48cf8..53eb5d04b 100644 --- a/apps/chat2bridge/chat2bridge.nim +++ b/apps/chat2bridge/chat2bridge.nim @@ -126,23 +126,22 @@ proc toMatterbridge( assert chat2Msg.isOk - let postRes = cmb.mbClient.postMessage( - text = string.fromBytes(chat2Msg[].payload), username = chat2Msg[].nick - ) - - if postRes.isErr() or (postRes[] == false): + if not cmb.mbClient + .postMessage( + text = string.fromBytes(chat2Msg[].payload), username = chat2Msg[].nick + ) + .containsValue(true): chat2_mb_dropped.inc(labelValues = ["duplicate"]) error "Matterbridge host unreachable. Dropping message." proc pollMatterbridge(cmb: Chat2MatterBridge, handler: MbMessageHandler) {.async.} = while cmb.running: - if (let getRes = cmb.mbClient.getMessages(); getRes.isOk()): - for jsonNode in getRes[]: - await handler(jsonNode) - else: + let msg = cmb.mbClient.getMessages().valueOr: error "Matterbridge host unreachable. Sleeping before retrying." await sleepAsync(chronos.seconds(10)) - + continue + for jsonNode in msg: + await handler(jsonNode) await sleepAsync(cmb.pollPeriod) ############## @@ -178,10 +177,10 @@ proc new*( builder.withNodeKey(nodev2Key) builder - .withNetworkConfigurationDetails( - nodev2BindIp, nodev2BindPort, nodev2ExtIp, nodev2ExtPort - ) - .tryGet() + .withNetworkConfigurationDetails( + nodev2BindIp, nodev2BindPort, nodev2ExtIp, nodev2ExtPort + ) + .tryGet() builder.build().tryGet() return Chat2MatterBridge( @@ -243,7 +242,7 @@ proc stop*(cmb: Chat2MatterBridge) {.async: (raises: [Exception]).} = {.pop.} # @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError when isMainModule: - import waku/common/utils/nat, waku/waku_api/message_cache + import waku/common/utils/nat, waku/rest_api/message_cache let rng = newRng() @@ -252,25 +251,21 @@ when isMainModule: if conf.logLevel != LogLevel.NONE: setLogLevel(conf.logLevel) - let natRes = setupNat( + let (nodev2ExtIp, nodev2ExtPort, _) = setupNat( conf.nat, clientId, Port(uint16(conf.libp2pTcpPort) + conf.portsShift), Port(uint16(conf.udpPort) + conf.portsShift), - ) - if natRes.isErr(): - error "Error in setupNat", error = natRes.error + ).valueOr: + raise newException(ValueError, "setupNat error " & error) - # Load address configuration - let - (nodev2ExtIp, nodev2ExtPort, _) = natRes.get() - ## The following heuristic assumes that, in absence of manual - ## config, the external port is the same as the bind port. - extPort = - if nodev2ExtIp.isSome() and nodev2ExtPort.isNone(): - some(Port(uint16(conf.libp2pTcpPort) + conf.portsShift)) - else: - nodev2ExtPort + ## The following heuristic assumes that, in absence of manual + ## config, the external port is the same as the bind port. + let extPort = + if nodev2ExtIp.isSome() and nodev2ExtPort.isNone(): + some(Port(uint16(conf.libp2pTcpPort) + conf.portsShift)) + else: + nodev2ExtPort let bridge = Chat2Matterbridge.new( mbHostUri = "http://" & $initTAddress(conf.mbHostAddress, Port(conf.mbHostPort)), diff --git a/apps/chat2mix/chat2mix.nim b/apps/chat2mix/chat2mix.nim index 2b4e0a924..8b786d7b6 100644 --- a/apps/chat2mix/chat2mix.nim +++ b/apps/chat2mix/chat2mix.nim @@ -30,6 +30,7 @@ import protobuf/minprotobuf, # message serialisation/deserialisation from and to protobufs nameresolving/dnsresolver, protocols/mix/curve25519, + protocols/mix/mix_protocol, ] # define DNS resolution import waku/[ @@ -38,6 +39,7 @@ import waku_lightpush/rpc, waku_enr, discovery/waku_dnsdisc, + discovery/waku_kademlia, waku_node, node/waku_metrics, node/peer_manager, @@ -55,8 +57,7 @@ import ../../waku/waku_rln_relay logScope: topics = "chat2 mix" -const Help = - """ +const Help = """ Commands: /[?|help|connect|nick|exit] help: Prints this help connect: dials a remote peer @@ -82,6 +83,8 @@ type PrivateKey* = crypto.PrivateKey Topic* = waku_core.PubsubTopic +const MinMixNodePoolSize = 4 + ##################### ## chat2 protobufs ## ##################### @@ -124,7 +127,7 @@ proc encode*(message: Chat2Message): ProtoBuffer = return serialised -proc toString*(message: Chat2Message): string = +proc `$`*(message: Chat2Message): string = # Get message date and timestamp in local time let time = message.timestamp.fromUnix().local().format("'<'MMM' 'dd,' 'HH:mm'>'") @@ -175,18 +178,16 @@ proc startMetricsServer( ): Result[MetricsHttpServerRef, string] = info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $serverPort - let metricsServerRes = MetricsHttpServerRef.new($serverIp, serverPort) - if metricsServerRes.isErr(): - return err("metrics HTTP server start failed: " & $metricsServerRes.error) + let server = MetricsHttpServerRef.new($serverIp, serverPort).valueOr: + return err("metrics HTTP server start failed: " & $error) - let server = metricsServerRes.value try: waitFor server.start() except CatchableError: return err("metrics HTTP server start failed: " & getCurrentExceptionMsg()) info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $serverPort - ok(metricsServerRes.value) + ok(server) proc publish(c: Chat, line: string) {.async.} = # First create a Chat2Message protobuf with this line of text @@ -333,57 +334,57 @@ proc maintainSubscription( const maxFailedServiceNodeSwitches = 10 var noFailedSubscribes = 0 var noFailedServiceNodeSwitches = 0 + # Use chronos.Duration explicitly to avoid mismatch with std/times.Duration + let RetryWait = chronos.seconds(2) # Quick retry interval + let SubscriptionMaintenance = chronos.seconds(30) # Subscription maintenance interval while true: info "maintaining subscription at", peer = constructMultiaddrStr(actualFilterPeer) # First use filter-ping to check if we have an active subscription - let pingRes = await wakuNode.wakuFilterClient.ping(actualFilterPeer) - if pingRes.isErr(): - # No subscription found. Let's subscribe. - error "ping failed.", err = pingRes.error - trace "no subscription found. Sending subscribe request" + let pingErr = (await wakuNode.wakuFilterClient.ping(actualFilterPeer)).errorOr: + await sleepAsync(SubscriptionMaintenance) + info "subscription is live." + continue - let subscribeRes = await wakuNode.filterSubscribe( + # No subscription found. Let's subscribe. + error "ping failed.", error = pingErr + trace "no subscription found. Sending subscribe request" + + let subscribeErr = ( + await wakuNode.filterSubscribe( some(filterPubsubTopic), filterContentTopic, actualFilterPeer ) + ).errorOr: + await sleepAsync(SubscriptionMaintenance) + if noFailedSubscribes > 0: + noFailedSubscribes -= 1 + notice "subscribe request successful." + continue - if subscribeRes.isErr(): - noFailedSubscribes += 1 - error "Subscribe request failed.", - err = subscribeRes.error, - peer = actualFilterPeer, - failCount = noFailedSubscribes + noFailedSubscribes += 1 + error "Subscribe request failed.", + error = subscribeErr, peer = actualFilterPeer, failCount = noFailedSubscribes - # TODO: disconnet from failed actualFilterPeer - # asyncSpawn(wakuNode.peerManager.switch.disconnect(p)) - # wakunode.peerManager.peerStore.delete(actualFilterPeer) + # TODO: disconnet from failed actualFilterPeer + # asyncSpawn(wakuNode.peerManager.switch.disconnect(p)) + # wakunode.peerManager.peerStore.delete(actualFilterPeer) - if noFailedSubscribes < maxFailedSubscribes: - await sleepAsync(2000) # Wait a bit before retrying - continue - elif not preventPeerSwitch: - let peerOpt = selectRandomServicePeer( - wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec - ) - peerOpt.isOkOr: - error "Failed to find new service peer. Exiting." - noFailedServiceNodeSwitches += 1 - break + if noFailedSubscribes < maxFailedSubscribes: + await sleepAsync(RetryWait) # Wait a bit before retrying + elif not preventPeerSwitch: + # try again with new peer without delay + let actualFilterPeer = selectRandomServicePeer( + wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec + ).valueOr: + error "Failed to find new service peer. Exiting." + noFailedServiceNodeSwitches += 1 + break - actualFilterPeer = peerOpt.get() - info "Found new peer for codec", - codec = filterPubsubTopic, peer = constructMultiaddrStr(actualFilterPeer) + info "Found new peer for codec", + codec = filterPubsubTopic, peer = constructMultiaddrStr(actualFilterPeer) - noFailedSubscribes = 0 - continue # try again with new peer without delay - else: - if noFailedSubscribes > 0: - noFailedSubscribes -= 1 - - notice "subscribe request successful." + noFailedSubscribes = 0 else: - info "subscription is live." - - await sleepAsync(30000) # Subscription maintenance interval + await sleepAsync(SubscriptionMaintenance) {.pop.} # @TODO confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError @@ -401,17 +402,13 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = if conf.logLevel != LogLevel.NONE: setLogLevel(conf.logLevel) - let natRes = setupNat( + let (extIp, extTcpPort, extUdpPort) = setupNat( conf.nat, clientId, Port(uint16(conf.tcpPort) + conf.portsShift), Port(uint16(conf.udpPort) + conf.portsShift), - ) - - if natRes.isErr(): - raise newException(ValueError, "setupNat error " & natRes.error) - - let (extIp, extTcpPort, extUdpPort) = natRes.get() + ).valueOr: + raise newException(ValueError, "setupNat error " & error) var enrBuilder = EnrBuilder.init(nodeKey) @@ -421,13 +418,9 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = error "failed to add sharded topics to ENR", error = error quit(QuitFailure) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) let node = block: var builder = WakuNodeBuilder.init() @@ -435,16 +428,16 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = builder.withRecord(record) builder - .withNetworkConfigurationDetails( - conf.listenAddress, - Port(uint16(conf.tcpPort) + conf.portsShift), - extIp, - extTcpPort, - wsBindPort = Port(uint16(conf.websocketPort) + conf.portsShift), - wsEnabled = conf.websocketSupport, - wssEnabled = conf.websocketSecureSupport, - ) - .tryGet() + .withNetworkConfigurationDetails( + conf.listenAddress, + Port(uint16(conf.tcpPort) + conf.portsShift), + extIp, + extTcpPort, + wsBindPort = Port(uint16(conf.websocketPort) + conf.portsShift), + wsEnabled = conf.websocketSupport, + wssEnabled = conf.websocketSecureSupport, + ) + .tryGet() builder.build().tryGet() node.mountAutoSharding(conf.clusterId, conf.numShardsInNetwork).isOkOr: @@ -461,12 +454,48 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = (await node.mountMix(conf.clusterId, mixPrivKey, conf.mixnodes)).isOkOr: error "failed to mount waku mix protocol: ", error = $error quit(QuitFailure) + + # Setup extended kademlia discovery if bootstrap nodes are provided + if conf.kadBootstrapNodes.len > 0: + var kadBootstrapPeers: seq[(PeerId, seq[MultiAddress])] + for nodeStr in conf.kadBootstrapNodes: + let (peerId, ma) = parseFullAddress(nodeStr).valueOr: + error "Failed to parse kademlia bootstrap node", node = nodeStr, error = error + continue + kadBootstrapPeers.add((peerId, @[ma])) + + if kadBootstrapPeers.len > 0: + node.wakuKademlia = WakuKademlia.new( + node.switch, + ExtendedKademliaDiscoveryParams( + bootstrapNodes: kadBootstrapPeers, + mixPubKey: some(mixPubKey), + advertiseMix: false, + ), + node.peerManager, + getMixNodePoolSize = proc(): int {.gcsafe, raises: [].} = + if node.wakuMix.isNil(): + 0 + else: + node.getMixNodePoolSize(), + isNodeStarted = proc(): bool {.gcsafe, raises: [].} = + node.started, + ).valueOr: + error "failed to setup kademlia discovery", error = error + quit(QuitFailure) + + #await node.mountRendezvousClient(conf.clusterId) + await node.start() node.peerManager.start() + if not node.wakuKademlia.isNil(): + (await node.wakuKademlia.start(minMixPeers = MinMixNodePoolSize)).isOkOr: + error "failed to start kademlia discovery", error = error + quit(QuitFailure) await node.mountLibp2pPing() - await node.mountPeerExchangeClient() + #await node.mountPeerExchangeClient() let pubsubTopic = conf.getPubsubTopic(node, conf.contentTopic) echo "pubsub topic is: " & pubsubTopic let nick = await readNick(transp) @@ -598,22 +627,17 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = error "Couldn't find any service peer" quit(QuitFailure) - #await mountLegacyLightPush(node) node.peerManager.addServicePeer(servicePeerInfo, WakuLightpushCodec) node.peerManager.addServicePeer(servicePeerInfo, WakuPeerExchangeCodec) + #node.peerManager.addServicePeer(servicePeerInfo, WakuRendezVousCodec) # Start maintaining subscription asyncSpawn maintainSubscription( node, pubsubTopic, conf.contentTopic, servicePeerInfo, false ) echo "waiting for mix nodes to be discovered..." - while true: - if node.getMixNodePoolSize() >= 3: - break - discard await node.fetchPeerExchangePeers() - await sleepAsync(1000) - while node.getMixNodePoolSize() < 3: + while node.getMixNodePoolSize() < MinMixNodePoolSize: info "waiting for mix nodes to be discovered", currentpoolSize = node.getMixNodePoolSize() await sleepAsync(1000) diff --git a/apps/chat2mix/config_chat2mix.nim b/apps/chat2mix/config_chat2mix.nim index ddb7136cb..4e5a32e6d 100644 --- a/apps/chat2mix/config_chat2mix.nim +++ b/apps/chat2mix/config_chat2mix.nim @@ -113,17 +113,16 @@ type shards* {. desc: "Shards index to subscribe to [0..NUM_SHARDS_IN_NETWORK-1]. Argument may be repeated.", - defaultValue: - @[ - uint16(0), - uint16(1), - uint16(2), - uint16(3), - uint16(4), - uint16(5), - uint16(6), - uint16(7), - ], + defaultValue: @[ + uint16(0), + uint16(1), + uint16(2), + uint16(3), + uint16(4), + uint16(5), + uint16(6), + uint16(7), + ], name: "shard" .}: seq[uint16] @@ -203,13 +202,13 @@ type fleet* {. desc: "Select the fleet to connect to. This sets the DNS discovery URL to the selected fleet.", - defaultValue: Fleet.test, + defaultValue: Fleet.none, name: "fleet" .}: Fleet contentTopic* {. desc: "Content topic for chat messages.", - defaultValue: "/toy-chat-mix/2/huilong/proto", + defaultValue: "/toy-chat/2/baixa-chiado/proto", name: "content-topic" .}: string @@ -228,7 +227,14 @@ type desc: "WebSocket Secure Support.", defaultValue: false, name: "websocket-secure-support" - .}: bool ## rln-relay configuration + .}: bool + + ## Kademlia Discovery config + kadBootstrapNodes* {. + desc: + "Peer multiaddr for kademlia discovery bootstrap node (must include /p2p/). Argument may be repeated.", + name: "kad-bootstrap-node" + .}: seq[string] proc parseCmdArg*(T: type MixNodePubInfo, p: string): T = let elements = p.split(":") diff --git a/apps/liteprotocoltester/Dockerfile.liteprotocoltester.compile b/apps/liteprotocoltester/Dockerfile.liteprotocoltester.compile index 9e2432051..dd7018cc0 100644 --- a/apps/liteprotocoltester/Dockerfile.liteprotocoltester.compile +++ b/apps/liteprotocoltester/Dockerfile.liteprotocoltester.compile @@ -7,7 +7,7 @@ ARG NIM_COMMIT ARG LOG_LEVEL=TRACE # Get build tools and required header files -RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq +RUN apk add --no-cache bash git build-base openssl-dev linux-headers curl jq libbsd-dev WORKDIR /app COPY . . @@ -43,7 +43,8 @@ EXPOSE 30303 60000 8545 RUN apk add --no-cache libgcc libpq-dev \ wget \ iproute2 \ - python3 + python3 \ + libstdc++ COPY --from=nim-build /app/build/liteprotocoltester /usr/bin/ RUN chmod +x /usr/bin/liteprotocoltester diff --git a/apps/liteprotocoltester/diagnose_connections.nim b/apps/liteprotocoltester/diagnose_connections.nim index f595b4e03..d2cc75516 100644 --- a/apps/liteprotocoltester/diagnose_connections.nim +++ b/apps/liteprotocoltester/diagnose_connections.nim @@ -14,7 +14,7 @@ import libp2p/wire import - ../../tools/confutils/cli_args, + tools/confutils/cli_args, waku/[ node/peer_manager, waku_lightpush/common, @@ -59,7 +59,4 @@ proc logSelfPeers*(pm: PeerManager) = {allPeers(pm)} *------------------------------------------------------------------------------------------*""".fmt() - if printable.isErr(): - echo "Error while printing statistics: " & printable.error().msg - else: - echo printable.get() + echo printable.valueOr("Error while printing statistics: " & error.msg) diff --git a/apps/liteprotocoltester/liteprotocoltester.nim b/apps/liteprotocoltester/liteprotocoltester.nim index 90c355a25..46c85e910 100644 --- a/apps/liteprotocoltester/liteprotocoltester.nim +++ b/apps/liteprotocoltester/liteprotocoltester.nim @@ -11,7 +11,7 @@ import confutils import - ../../tools/confutils/cli_args, + tools/confutils/cli_args, waku/[ common/enr, common/logging, @@ -49,13 +49,10 @@ when isMainModule: const versionString = "version / git commit hash: " & waku_factory.git_version - let confRes = LiteProtocolTesterConf.load(version = versionString) - if confRes.isErr(): - error "failure while loading the configuration", error = confRes.error + let conf = LiteProtocolTesterConf.load(version = versionString).valueOr: + error "failure while loading the configuration", error = error quit(QuitFailure) - var conf = confRes.get() - ## Logging setup logging.setupLog(conf.logLevel, conf.logFormat) @@ -133,7 +130,8 @@ when isMainModule: info "Setting up shutdown hooks" proc asyncStopper(waku: Waku) {.async: (raises: [Exception]).} = - await waku.stop() + (await waku.stop()).isOkOr: + error "Waku shutdown failed", error = error quit(QuitSuccess) # Handle Ctrl-C SIGINT @@ -163,7 +161,8 @@ when isMainModule: # Not available in -d:release mode writeStackTrace() - waitFor waku.stop() + (waitFor waku.stop()).isOkOr: + error "Waku shutdown failed", error = error quit(QuitFailure) c_signal(ansi_c.SIGSEGV, handleSigsegv) @@ -187,7 +186,7 @@ when isMainModule: error "Service node not found in time via PX" quit(QuitFailure) - if futForServiceNode.read().isErr(): + futForServiceNode.read().isOkOr: error "Service node for test not found via PX" quit(QuitFailure) diff --git a/apps/liteprotocoltester/publisher.nim b/apps/liteprotocoltester/publisher.nim index 1debfdf56..0df3f3e3e 100644 --- a/apps/liteprotocoltester/publisher.nim +++ b/apps/liteprotocoltester/publisher.nim @@ -89,10 +89,7 @@ proc reportSentMessages() = |{numMessagesToSend+failedToSendCount:>11} |{messagesSent:>11} |{failedToSendCount:>11} | *----------------------------------------*""".fmt() - if report.isErr: - echo "Error while printing statistics" - else: - echo report.get() + echo report.valueOr("Error while printing statistics") echo "*--------------------------------------------------------------------------------------------------*" echo "| Failure cause | count |" diff --git a/apps/liteprotocoltester/receiver.nim b/apps/liteprotocoltester/receiver.nim index 0e6638c61..b62094ec6 100644 --- a/apps/liteprotocoltester/receiver.nim +++ b/apps/liteprotocoltester/receiver.nim @@ -54,64 +54,65 @@ proc maintainSubscription( var noFailedSubscribes = 0 var noFailedServiceNodeSwitches = 0 var isFirstPingOnNewPeer = true + const RetryWaitMs = 2.seconds # Quick retry interval + const SubscriptionMaintenanceMs = 30.seconds # Subscription maintenance interval while true: info "maintaining subscription at", peer = constructMultiaddrStr(actualFilterPeer) # First use filter-ping to check if we have an active subscription - let pingRes = await wakuNode.wakuFilterClient.ping(actualFilterPeer) - if pingRes.isErr(): - if isFirstPingOnNewPeer == false: - # Very first ping expected to fail as we have not yet subscribed at all - lpt_receiver_lost_subscription_count.inc() - isFirstPingOnNewPeer = false - # No subscription found. Let's subscribe. - error "ping failed.", err = pingRes.error - trace "no subscription found. Sending subscribe request" + let pingErr = (await wakuNode.wakuFilterClient.ping(actualFilterPeer)).errorOr: + await sleepAsync(SubscriptionMaintenanceMs) + info "subscription is live." + continue - let subscribeRes = await wakuNode.filterSubscribe( + if isFirstPingOnNewPeer == false: + # Very first ping expected to fail as we have not yet subscribed at all + lpt_receiver_lost_subscription_count.inc() + isFirstPingOnNewPeer = false + # No subscription found. Let's subscribe. + error "ping failed.", error = pingErr + trace "no subscription found. Sending subscribe request" + + let subscribeErr = ( + await wakuNode.filterSubscribe( some(filterPubsubTopic), filterContentTopic, actualFilterPeer ) + ).errorOr: + await sleepAsync(SubscriptionMaintenanceMs) + if noFailedSubscribes > 0: + noFailedSubscribes -= 1 + notice "subscribe request successful." + continue - if subscribeRes.isErr(): - noFailedSubscribes += 1 - lpt_service_peer_failure_count.inc( - labelValues = ["receiver", actualFilterPeer.getAgent()] - ) - error "Subscribe request failed.", - err = subscribeRes.error, - peer = actualFilterPeer, - failCount = noFailedSubscribes + noFailedSubscribes += 1 + lpt_service_peer_failure_count.inc( + labelValues = ["receiver", actualFilterPeer.getAgent()] + ) + error "Subscribe request failed.", + err = subscribeErr, peer = actualFilterPeer, failCount = noFailedSubscribes - # TODO: disconnet from failed actualFilterPeer - # asyncSpawn(wakuNode.peerManager.switch.disconnect(p)) - # wakunode.peerManager.peerStore.delete(actualFilterPeer) + # TODO: disconnet from failed actualFilterPeer + # asyncSpawn(wakuNode.peerManager.switch.disconnect(p)) + # wakunode.peerManager.peerStore.delete(actualFilterPeer) - if noFailedSubscribes < maxFailedSubscribes: - await sleepAsync(2.seconds) # Wait a bit before retrying - continue - elif not preventPeerSwitch: - actualFilterPeer = selectRandomServicePeer( - wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec - ).valueOr: - error "Failed to find new service peer. Exiting." - noFailedServiceNodeSwitches += 1 - break + if noFailedSubscribes < maxFailedSubscribes: + await sleepAsync(RetryWaitMs) # Wait a bit before retrying + elif not preventPeerSwitch: + # try again with new peer without delay + actualFilterPeer = selectRandomServicePeer( + wakuNode.peerManager, some(actualFilterPeer), WakuFilterSubscribeCodec + ).valueOr: + error "Failed to find new service peer. Exiting." + noFailedServiceNodeSwitches += 1 + break - info "Found new peer for codec", - codec = filterPubsubTopic, peer = constructMultiaddrStr(actualFilterPeer) + info "Found new peer for codec", + codec = filterPubsubTopic, peer = constructMultiaddrStr(actualFilterPeer) - noFailedSubscribes = 0 - lpt_change_service_peer_count.inc(labelValues = ["receiver"]) - isFirstPingOnNewPeer = true - continue # try again with new peer without delay - else: - if noFailedSubscribes > 0: - noFailedSubscribes -= 1 - - notice "subscribe request successful." + noFailedSubscribes = 0 + lpt_change_service_peer_count.inc(labelValues = ["receiver"]) + isFirstPingOnNewPeer = true else: - info "subscription is live." - - await sleepAsync(30.seconds) # Subscription maintenance interval + await sleepAsync(SubscriptionMaintenanceMs) proc setupAndListen*( wakuNode: WakuNode, conf: LiteProtocolTesterConf, servicePeer: RemotePeerInfo diff --git a/apps/liteprotocoltester/service_peer_management.nim b/apps/liteprotocoltester/service_peer_management.nim index 747ace86b..d5cfafef1 100644 --- a/apps/liteprotocoltester/service_peer_management.nim +++ b/apps/liteprotocoltester/service_peer_management.nim @@ -11,7 +11,7 @@ import libp2p/wire import - ../wakunode2/cli_args, + tools/confutils/cli_args, waku/[ common/enr, waku_node, @@ -181,7 +181,7 @@ proc pxLookupServiceNode*( if not await futPeers.withTimeout(30.seconds): notice "Cannot get peers from PX", round = 5 - trialCount else: - if futPeers.value().isErr(): + futPeers.value().isOkOr: info "PeerExchange reported error", error = futPeers.read().error return err() diff --git a/apps/liteprotocoltester/statistics.nim b/apps/liteprotocoltester/statistics.nim index 8322edd8f..7feebd4cf 100644 --- a/apps/liteprotocoltester/statistics.nim +++ b/apps/liteprotocoltester/statistics.nim @@ -8,6 +8,8 @@ import results, libp2p/peerid +from std/sugar import `=>` + import ./tester_message, ./lpt_metrics type @@ -114,12 +116,7 @@ proc addMessage*( if not self.contains(peerId): self[peerId] = Statistics.init() - let shortSenderId = block: - let senderPeer = PeerId.init(msg.sender) - if senderPeer.isErr(): - msg.sender - else: - senderPeer.get().shortLog() + let shortSenderId = PeerId.init(msg.sender).map(p => p.shortLog()).valueOr(msg.sender) discard catch: self[peerId].addMessage(shortSenderId, msg, msgHash) @@ -220,10 +217,7 @@ proc echoStat*(self: Statistics, peerId: string) = | {self.missingIndices()} | *------------------------------------------------------------------------------------------*""".fmt() - if printable.isErr(): - echo "Error while printing statistics: " & printable.error().msg - else: - echo printable.get() + echo printable.valueOr("Error while printing statistics: " & error.msg) proc jsonStat*(self: Statistics): string = let minL, maxL, avgL = self.calcLatency() @@ -243,20 +237,18 @@ proc jsonStat*(self: Statistics): string = }}, "lostIndices": {self.missingIndices()} }}""".fmt() - if json.isErr: - return "{\"result:\": \"" & json.error.msg & "\"}" - return json.get() + return json.valueOr("{\"result:\": \"" & error.msg & "\"}") proc echoStats*(self: var PerPeerStatistics) = for peerId, stats in self.pairs: let peerLine = catch: "Receiver statistics from peer {peerId}".fmt() - if peerLine.isErr: + peerLine.isOkOr: echo "Error while printing statistics" - else: - echo peerLine.get() - stats.echoStat(peerId) + continue + echo peerLine.get() + stats.echoStat(peerId) proc jsonStats*(self: PerPeerStatistics): string = try: diff --git a/apps/liteprotocoltester/tester_message.nim b/apps/liteprotocoltester/tester_message.nim index eeff7b531..38028e4a7 100644 --- a/apps/liteprotocoltester/tester_message.nim +++ b/apps/liteprotocoltester/tester_message.nim @@ -6,7 +6,7 @@ import json_serialization/std/options, json_serialization/lexer -import ../../waku/waku_api/rest/serdes +import waku/rest_api/endpoint/serdes type ProtocolTesterMessage* = object sender*: string diff --git a/apps/networkmonitor/networkmonitor.nim b/apps/networkmonitor/networkmonitor.nim index ad7732db2..23607b118 100644 --- a/apps/networkmonitor/networkmonitor.nim +++ b/apps/networkmonitor/networkmonitor.nim @@ -443,12 +443,8 @@ proc initAndStartApp( error "failed to add sharded topics to ENR", error = error return err("failed to add sharded topics to ENR: " & $error) - let recordRes = builder.build() - let record = - if recordRes.isErr(): - return err("cannot build record: " & $recordRes.error) - else: - recordRes.get() + let record = builder.build().valueOr: + return err("cannot build record: " & $error) var nodeBuilder = WakuNodeBuilder.init() @@ -461,21 +457,15 @@ proc initAndStartApp( relayServiceRatio = "13.33:86.67", shardAware = true, ) - let res = nodeBuilder.withNetworkConfigurationDetails(bindIp, nodeTcpPort) - if res.isErr(): - return err("node building error" & $res.error) + nodeBuilder.withNetworkConfigurationDetails(bindIp, nodeTcpPort).isOkOr: + return err("node building error" & $error) - let nodeRes = nodeBuilder.build() - let node = - if nodeRes.isErr(): - return err("node building error" & $res.error) - else: - nodeRes.get() + let node = nodeBuilder.build().valueOr: + return err("node building error" & $error) - var discv5BootstrapEnrsRes = await getBootstrapFromDiscDns(conf) - if discv5BootstrapEnrsRes.isErr(): + var discv5BootstrapEnrs = (await getBootstrapFromDiscDns(conf)).valueOr: error("failed discovering peers from DNS") - var discv5BootstrapEnrs = discv5BootstrapEnrsRes.get() + quit(QuitFailure) # parse enrURIs from the configuration and add the resulting ENRs to the discv5BootstrapEnrs seq for enrUri in conf.bootstrapNodes: @@ -553,12 +543,10 @@ proc subscribeAndHandleMessages( when isMainModule: # known issue: confutils.nim(775, 17) Error: can raise an unlisted exception: ref IOError {.pop.} - let confRes = NetworkMonitorConf.loadConfig() - if confRes.isErr(): - error "could not load cli variables", err = confRes.error - quit(1) + var conf = NetworkMonitorConf.loadConfig().valueOr: + error "could not load cli variables", error = error + quit(QuitFailure) - var conf = confRes.get() info "cli flags", conf = conf if conf.clusterId == 1: @@ -586,37 +574,30 @@ when isMainModule: # start metrics server if conf.metricsServer: - let res = - startMetricsServer(conf.metricsServerAddress, Port(conf.metricsServerPort)) - if res.isErr(): - error "could not start metrics server", err = res.error - quit(1) + startMetricsServer(conf.metricsServerAddress, Port(conf.metricsServerPort)).isOkOr: + error "could not start metrics server", error = error + quit(QuitFailure) # start rest server for custom metrics - let res = startRestApiServer(conf, allPeersInfo, msgPerContentTopic) - if res.isErr(): - error "could not start rest api server", err = res.error - quit(1) + startRestApiServer(conf, allPeersInfo, msgPerContentTopic).isOkOr: + error "could not start rest api server", error = error + quit(QuitFailure) # create a rest client - let clientRest = - RestClientRef.new(url = "http://ip-api.com", connectTimeout = ctime.seconds(2)) - if clientRest.isErr(): - error "could not start rest api client", err = res.error - quit(1) - let restClient = clientRest.get() + let restClient = RestClientRef.new( + url = "http://ip-api.com", connectTimeout = ctime.seconds(2) + ).valueOr: + error "could not start rest api client", error = error + quit(QuitFailure) # start waku node - let nodeRes = waitFor initAndStartApp(conf) - if nodeRes.isErr(): - error "could not start node" - quit 1 - - let (node, discv5) = nodeRes.get() + let (node, discv5) = (waitFor initAndStartApp(conf)).valueOr: + error "could not start node", error = error + quit(QuitFailure) (waitFor node.mountRelay()).isOkOr: - error "failed to mount waku relay protocol: ", err = error - quit 1 + error "failed to mount waku relay protocol: ", error = error + quit(QuitFailure) waitFor node.mountLibp2pPing() @@ -640,12 +621,12 @@ when isMainModule: try: waitFor node.mountRlnRelay(rlnConf) except CatchableError: - error "failed to setup RLN", err = getCurrentExceptionMsg() - quit 1 + error "failed to setup RLN", error = getCurrentExceptionMsg() + quit(QuitFailure) node.mountMetadata(conf.clusterId, conf.shards).isOkOr: - error "failed to mount waku metadata protocol: ", err = error - quit 1 + error "failed to mount waku metadata protocol: ", error = error + quit(QuitFailure) for shard in conf.shards: # Subscribe the node to the shards, to count messages diff --git a/apps/wakucanary/wakucanary.nim b/apps/wakucanary/wakucanary.nim index 337896d39..e7b1ff9aa 100644 --- a/apps/wakucanary/wakucanary.nim +++ b/apps/wakucanary/wakucanary.nim @@ -6,12 +6,15 @@ import os import libp2p/protocols/ping, + libp2p/protocols/protocol, libp2p/crypto/[crypto, secp], libp2p/nameresolving/dnsresolver, libp2p/multicodec import ./certsgenerator, - waku/[waku_enr, node/peer_manager, waku_core, waku_node, factory/builder] + waku/[waku_enr, node/peer_manager, waku_core, waku_node, factory/builder], + waku/waku_metadata/protocol, + waku/common/callbacks # protocols and their tag const ProtocolsTable = { @@ -45,7 +48,7 @@ type WakuCanaryConf* = object timeout* {. desc: "Timeout to consider that the connection failed", - defaultValue: chronos.seconds(10), + defaultValue: chronos.seconds(20), name: "timeout", abbr: "t" .}: chronos.Duration @@ -143,27 +146,28 @@ proc areProtocolsSupported( proc pingNode( node: WakuNode, peerInfo: RemotePeerInfo -): Future[void] {.async, gcsafe.} = +): Future[bool] {.async, gcsafe.} = try: let conn = await node.switch.dial(peerInfo.peerId, peerInfo.addrs, PingCodec) let pingDelay = await node.libp2pPing.ping(conn) info "Peer response time (ms)", peerId = peerInfo.peerId, ping = pingDelay.millis + return true except CatchableError: var msg = getCurrentExceptionMsg() if msg == "Future operation cancelled!": msg = "timedout" error "Failed to ping the peer", peer = peerInfo, err = msg + return false proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = let conf: WakuCanaryConf = WakuCanaryConf.load() # create dns resolver let - nameServers = - @[ - initTAddress(parseIpAddress("1.1.1.1"), Port(53)), - initTAddress(parseIpAddress("1.0.0.1"), Port(53)), - ] + nameServers = @[ + initTAddress(parseIpAddress("1.1.1.1"), Port(53)), + initTAddress(parseIpAddress("1.0.0.1"), Port(53)), + ] resolver: DnsResolver = DnsResolver.new(nameServers) if conf.logLevel != LogLevel.NONE: @@ -181,13 +185,10 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = protocols = conf.protocols, logLevel = conf.logLevel - let peerRes = parsePeerInfo(conf.address) - if peerRes.isErr(): - error "Couldn't parse 'conf.address'", error = peerRes.error + let peer = parsePeerInfo(conf.address).valueOr: + error "Couldn't parse 'conf.address'", error = error quit(QuitFailure) - let peer = peerRes.value - let nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[] bindIp = parseIpAddress("0.0.0.0") @@ -225,13 +226,9 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = error "could not initialize ENR with shards", error quit(QuitFailure) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) if isWss and (conf.websocketSecureKeyPath.len == 0 or conf.websocketSecureCertPath.len == 0): @@ -257,12 +254,26 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = error "failed to mount libp2p ping protocol: " & getCurrentExceptionMsg() quit(QuitFailure) - node.mountMetadata(conf.clusterId, conf.shards).isOkOr: - error "failed to mount metadata protocol", error + # Mount metadata with a custom getter that returns CLI shards directly, + # since the canary doesn't mount relay (which is what the default getter reads from). + # Without this fix, the canary always sends remoteShards=[] in metadata requests. + let cliShards = conf.shards + let shardsGetter: GetShards = proc(): seq[uint16] {.closure, gcsafe, raises: [].} = + return cliShards + + let metadata = WakuMetadata.new(conf.clusterId, shardsGetter) + node.wakuMetadata = metadata + node.peerManager.wakuMetadata = metadata + let mountRes = catch: + node.switch.mount(metadata, protocolMatcher(WakuMetadataCodec)) + mountRes.isOkOr: + error "failed to mount metadata protocol", error = error.msg quit(QuitFailure) await node.start() + debug "Connecting to peer", peer = peer, timeout = conf.timeout + var pingFut: Future[bool] if conf.ping: pingFut = pingNode(node, peer).withTimeout(conf.timeout) @@ -272,22 +283,42 @@ proc main(rng: ref HmacDrbgContext): Future[int] {.async.} = error "Timedout after", timeout = conf.timeout quit(QuitFailure) + # Clean disconnect with defer so the remote node doesn't see + # "Stream Underlying Connection Closed!" when we exit + defer: + debug "Cleanly disconnecting from peer", peerId = peer.peerId + await node.peerManager.disconnectNode(peer.peerId) + await node.stop() + + debug "Connected, checking connection status", peerId = peer.peerId + let lp2pPeerStore = node.switch.peerStore let conStatus = node.peerManager.switch.peerStore[ConnectionBook][peer.peerId] + debug "Connection status", peerId = peer.peerId, conStatus = conStatus + var pingSuccess = true if conf.ping: - discard await pingFut + try: + pingSuccess = await pingFut + except CatchableError as exc: + pingSuccess = false + error "Ping operation failed or timed out", error = exc.msg + + if not pingSuccess: + error "Ping to the node failed", peerId = peer.peerId, conStatus = $conStatus + quit(QuitFailure) if conStatus in [Connected, CanConnect]: let nodeProtocols = lp2pPeerStore[ProtoBook][peer.peerId] + debug "Peer protocols", peerId = peer.peerId, protocols = nodeProtocols if not areProtocolsSupported(conf.protocols, nodeProtocols): error "Not all protocols are supported", expected = conf.protocols, supported = nodeProtocols - quit(QuitFailure) + return 1 elif conStatus == CannotConnect: error "Could not connect", peerId = peer.peerId - quit(QuitFailure) + return 1 return 0 when isMainModule: diff --git a/apps/wakunode2/wakunode2.nim b/apps/wakunode2/wakunode2.nim index 86db3fbc4..484adf68f 100644 --- a/apps/wakunode2/wakunode2.nim +++ b/apps/wakunode2/wakunode2.nim @@ -5,7 +5,6 @@ import chronicles, chronos, metrics, - libbacktrace, system/ansi_c, libp2p/crypto/crypto import @@ -14,7 +13,7 @@ import common/logging, factory/waku, node/health_monitor, - waku_api/rest/builder as rest_server_builder, + rest_api/endpoint/builder as rest_server_builder, waku_core/message/default_values, ] @@ -62,7 +61,8 @@ when isMainModule: info "Setting up shutdown hooks" proc asyncStopper(waku: Waku) {.async: (raises: [Exception]).} = - await waku.stop() + (await waku.stop()).isOkOr: + error "Waku shutdown failed", error = error quit(QuitSuccess) # Handle Ctrl-C SIGINT @@ -87,12 +87,13 @@ when isMainModule: when defined(posix): proc handleSigsegv(signal: cint) {.noconv.} = # Require --debugger:native - fatal "Shutting down after receiving SIGSEGV", stacktrace = getBacktrace() + fatal "Shutting down after receiving SIGSEGV" # Not available in -d:release mode writeStackTrace() - waitFor waku.stop() + (waitFor waku.stop()).isOkOr: + error "Waku shutdown failed", error = error quit(QuitFailure) c_signal(ansi_c.SIGSEGV, handleSigsegv) diff --git a/ci/Jenkinsfile.release b/ci/Jenkinsfile.release index 570a37d5f..d8237f009 100644 --- a/ci/Jenkinsfile.release +++ b/ci/Jenkinsfile.release @@ -85,7 +85,8 @@ pipeline { "--label=commit='${git.commit()}' " + "--label=version='${git.describe('--tags')}' " + "--build-arg=MAKE_TARGET='${params.MAKE_TARGET}' " + - "--build-arg=NIMFLAGS='${params.NIMFLAGS} -d:postgres -d:heaptracker ' " + + "--build-arg=NIMFLAGS='${params.NIMFLAGS} -d:heaptracker ' " + + "--build-arg=POSTGRES='1' " + "--build-arg=LOG_LEVEL='${params.LOWEST_LOG_LEVEL_ALLOWED}' " + "--build-arg=DEBUG='${params.DEBUG ? "1" : "0"} ' " + "--build-arg=NIM_COMMIT='NIM_COMMIT=heaptrack_support_v2.0.12' " + @@ -98,7 +99,8 @@ pipeline { "--label=commit='${git.commit()}' " + "--label=version='${git.describe('--tags')}' " + "--build-arg=MAKE_TARGET='${params.MAKE_TARGET}' " + - "--build-arg=NIMFLAGS='${params.NIMFLAGS} -d:postgres ' " + + "--build-arg=NIMFLAGS='${params.NIMFLAGS}' " + + "--build-arg=POSTGRES='1' " + "--build-arg=LOG_LEVEL='${params.LOWEST_LOG_LEVEL_ALLOWED}' " + "--build-arg=DEBUG='${params.DEBUG ? "1" : "0"} ' " + "--target='prod' ." diff --git a/config.nims b/config.nims index f74fe183f..ebe501db8 100644 --- a/config.nims +++ b/config.nims @@ -9,12 +9,6 @@ if defined(windows): switch("passL", "rln.lib") switch("define", "postgres=false") - # Automatically add all vendor subdirectories - for dir in walkDir("./vendor"): - if dir.kind == pcDir: - switch("path", dir.path) - switch("path", dir.path / "src") - # disable timestamps in Windows PE headers - https://wiki.debian.org/ReproducibleBuilds/TimestampsInPEBinaries switch("passL", "-Wl,--no-insert-timestamp") # increase stack size @@ -26,10 +20,6 @@ if defined(windows): # set the IMAGE_FILE_LARGE_ADDRESS_AWARE flag so we can use PAE, if enabled, and access more than 2 GiB of RAM switch("passL", "-Wl,--large-address-aware") - # The dynamic Chronicles output currently prevents us from using colors on Windows - # because these require direct manipulations of the stdout File object. - switch("define", "chronicles_colors=off") - # https://github.com/status-im/nimbus-eth2/blob/stable/docs/cpu_features.md#ssse3-supplemental-sse3 # suggests that SHA256 hashing with SSSE3 is 20% faster than without SSSE3, so # given its near-ubiquity in the x86 installed base, it renders a distribution @@ -52,9 +42,10 @@ if defined(disableMarchNative): switch("passL", "-march=haswell -mtune=generic") else: if defined(marchOptimized): - # https://github.com/status-im/nimbus-eth2/blob/stable/docs/cpu_features.md#bmi2--adx - switch("passC", "-march=broadwell -mtune=generic") - switch("passL", "-march=broadwell -mtune=generic") + # -march=broadwell: https://github.com/status-im/nimbus-eth2/blob/stable/docs/cpu_features.md#bmi2--adx + # Changed to x86-64-v2 for broader support + switch("passC", "-march=x86-64-v2 -mtune=generic") + switch("passL", "-march=x86-64-v2 -mtune=generic") else: switch("passC", "-mssse3") switch("passL", "-mssse3") @@ -76,6 +67,7 @@ else: on --opt: speed + --excessiveStackTrace: on # enable metric collection @@ -85,16 +77,15 @@ else: --define: nimTypeNames -switch("define", "withoutPCRE") - # the default open files limit is too low on macOS (512), breaking the # "--debugger:native" build. It can be increased with `ulimit -n 1024`. if not defined(macosx) and not defined(android): # add debugging symbols and original files and line numbers --debugger: native - if not (defined(windows) and defined(i386)) and not defined(disable_libbacktrace): + when defined(enable_libbacktrace): # light-weight stack traces using libbacktrace and libunwind + # opt-in: pass -d:enable_libbacktrace (requires libbacktrace in project deps) --define: nimStackTraceOverride switch("import", "libbacktrace") @@ -125,3 +116,8 @@ if defined(android): switch("passC", "--sysroot=" & sysRoot) switch("passL", "--sysroot=" & sysRoot) switch("cincludes", sysRoot & "/usr/include/") +# begin Nimble config (version 2) +--noNimblePath +when withDir(thisDir(), system.fileExists("nimble.paths")): + include "nimble.paths" +# end Nimble config diff --git a/docs/api/rest-api.md b/docs/api/rest-api.md index eeb90abfb..cc8e51020 100644 --- a/docs/api/rest-api.md +++ b/docs/api/rest-api.md @@ -38,6 +38,9 @@ A particular OpenAPI spec can be easily imported into [Postman](https://www.post curl http://localhost:8645/debug/v1/info -s | jq ``` +### Store API + +The `page_size` flag in the Store API has a default value of 20 and a max value of 100. ### Node configuration Find details [here](https://github.com/waku-org/nwaku/tree/master/docs/operators/how-to/configure-rest-api.md) diff --git a/docs/contributors/release-process.md b/docs/contributors/release-process.md deleted file mode 100644 index c0fb12d1c..000000000 --- a/docs/contributors/release-process.md +++ /dev/null @@ -1,119 +0,0 @@ -# Release Process - -How to do releases. - -For more context, see https://trunkbaseddevelopment.com/branch-for-release/ - -## How to do releases - -### Before release - -Ensure all items in this list are ticked: -- [ ] All issues under the corresponding release [milestone](https://github.com/waku-org/nwaku/milestones) has been closed or, after consultation, deferred to a next release. -- [ ] All submodules are up to date. - > **IMPORTANT:** Updating submodules requires a PR (and very often several "fixes" to maintain compatibility with the changes in submodules). That PR process must be done and merged a couple of days before the release. - > In case the submodules update has a low effort and/or risk for the release, follow the ["Update submodules"](./git-submodules.md) instructions. - > If the effort or risk is too high, consider postponing the submodules upgrade for the subsequent release or delaying the current release until the submodules updates are included in the release candidate. -- [ ] The [js-waku CI tests](https://github.com/waku-org/js-waku/actions/workflows/ci.yml) pass against the release candidate (i.e. nwaku latest `master`). - > **NOTE:** This serves as a basic regression test against typical clients of nwaku. - > The specific job that needs to pass is named `node_with_nwaku_master`. - -### Performing the release - -1. Checkout a release branch from master - - ``` - git checkout -b release/v0.1.0 - ``` - -1. Update `CHANGELOG.md` and ensure it is up to date. Use the helper Make target to get PR based release-notes/changelog update. - - ``` - make release-notes - ``` - -1. Create a release-candidate tag with the same name as release and `-rc.N` suffix a few days before the official release and push it - - ``` - git tag -as v0.1.0-rc.0 -m "Initial release." - git push origin v0.1.0-rc.0 - ``` - - This will trigger a [workflow](../../.github/workflows/pre-release.yml) which will build RC artifacts and create and publish a Github release - -1. Open a PR from the release branch for others to review the included changes and the release-notes - -1. In case additional changes are needed, create a new RC tag - - Make sure the new tag is associated - with CHANGELOG update. - - ``` - # Make changes, rebase and create new tag - # Squash to one commit and make a nice commit message - git rebase -i origin/master - git tag -as v0.1.0-rc.1 -m "Initial release." - git push origin v0.1.0-rc.1 - ``` - -1. Validate the release. For the release validation process, please refer to the following [guide](https://www.notion.so/Release-Process-61234f335b904cd0943a5033ed8f42b4#47af557e7f9744c68fdbe5240bf93ca9) - -1. Once the release-candidate has been validated, create a final release tag and push it. -We also need to merge release branch back to master as a final step. - - ``` - git checkout release/v0.1.0 - git tag -as v0.1.0 -m "Initial release." - git push origin v0.1.0 - git switch master - git pull - git merge release/v0.1.0 - ``` - -1. Create a [Github release](https://github.com/waku-org/nwaku/releases) from the release tag. - - * Add binaries produced by the ["Upload Release Asset"](https://github.com/waku-org/nwaku/actions/workflows/release-assets.yml) workflow. Where possible, test the binaries before uploading to the release. - -### After the release - -1. Announce the release on Twitter, Discord and other channels. -2. Deploy the release image to [Dockerhub](https://hub.docker.com/r/wakuorg/nwaku) by triggering [the manual Jenkins deployment job](https://ci.infra.status.im/job/nim-waku/job/docker-manual/). - > Ensure the following build parameters are set: - > - `MAKE_TARGET`: `wakunode2` - > - `IMAGE_TAG`: the release tag (e.g. `v0.16.0`) - > - `IMAGE_NAME`: `wakuorg/nwaku` - > - `NIMFLAGS`: `--colors:off -d:disableMarchNative -d:chronicles_colors:none -d:postgres` - > - `GIT_REF` the release tag (e.g. `v0.16.0`) -3. Update the default nwaku image in [nwaku-compose](https://github.com/waku-org/nwaku-compose/blob/master/docker-compose.yml) -4. Deploy the release to appropriate fleets: - - Inform clients - > **NOTE:** known clients are currently using some version of js-waku, go-waku, nwaku or waku-rs. - > Clients are reachable via the corresponding channels on the Vac Discord server. - > It should be enough to inform clients on the `#nwaku` and `#announce` channels on Discord. - > Informal conversations with specific repo maintainers are often part of this process. - - Check if nwaku configuration parameters changed. If so [update fleet configuration](https://www.notion.so/Fleet-Ownership-7532aad8896d46599abac3c274189741?pvs=4#d2d2f0fe4b3c429fbd860a1d64f89a64) in [infra-nim-waku](https://github.com/status-im/infra-nim-waku) - - Deploy release to the `waku.sandbox` fleet from [Jenkins](https://ci.infra.status.im/job/nim-waku/job/deploy-waku-sandbox/). - - Ensure that nodes successfully start up and monitor health using [Grafana](https://grafana.infra.status.im/d/qrp_ZCTGz/nim-waku-v2?orgId=1) and [Kibana](https://kibana.infra.status.im/goto/a7728e70-eb26-11ec-81d1-210eb3022c76). - - If necessary, revert by deploying the previous release. Download logs and open a bug report issue. -5. Submit a PR to merge the release branch back to `master`. Make sure you use the option `Merge pull request (Create a merge commit)` to perform such merge. - -### Performing a patch release - -1. Cherry-pick the relevant commits from master to the release branch - - ``` - git cherry-pick - ``` - -2. Create a release-candidate tag with the same name as release and `-rc.N` suffix - -3. Update `CHANGELOG.md`. From the release branch, use the helper Make target after having cherry-picked the commits. - - ``` - make release-notes - ``` - Create a new branch and raise a PR with the changelog updates to master. - -4. Once the release-candidate has been validated and changelog PR got merged, cherry-pick the changelog update from master to the release branch. Create a final release tag and push it. - -5. Create a [Github release](https://github.com/waku-org/nwaku/releases) from the release tag and follow the same post-release process as usual. diff --git a/docs/operators/how-to/configure-rest-api.md b/docs/operators/how-to/configure-rest-api.md index 3fe070aab..7a58a798c 100644 --- a/docs/operators/how-to/configure-rest-api.md +++ b/docs/operators/how-to/configure-rest-api.md @@ -1,4 +1,3 @@ - # Configure a REST API node A subset of the node configuration can be used to modify the behaviour of the HTTP REST API. @@ -21,3 +20,5 @@ Example: ```shell wakunode2 --rest=true ``` + +The `page_size` flag in the Store API has a default value of 20 and a max value of 100. diff --git a/env.sh b/env.sh deleted file mode 100755 index f90ba9a74..000000000 --- a/env.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -# We use ${BASH_SOURCE[0]} instead of $0 to allow sourcing this file -# and we fall back to a Zsh-specific special var to also support Zsh. -REL_PATH="$(dirname ${BASH_SOURCE[0]:-${(%):-%x}})" -ABS_PATH="$(cd ${REL_PATH}; pwd)" -source ${ABS_PATH}/vendor/nimbus-build-system/scripts/env.sh - diff --git a/examples/api_example/api_example.nim b/examples/api_example/api_example.nim new file mode 100644 index 000000000..2093a81c0 --- /dev/null +++ b/examples/api_example/api_example.nim @@ -0,0 +1,94 @@ +import std/options +import chronos, results, confutils, confutils/defs +import waku + +type CliArgs = object + ethRpcEndpoint* {. + defaultValue: "", desc: "ETH RPC Endpoint, if passed, RLN is enabled" + .}: string + +proc periodicSender(w: Waku): Future[void] {.async.} = + let sentListener = MessageSentEvent.listen( + proc(event: MessageSentEvent) {.async: (raises: []).} = + echo "Message sent with request ID: ", + event.requestId, " hash: ", event.messageHash + ).valueOr: + echo "Failed to listen to message sent event: ", error + return + + let errorListener = MessageErrorEvent.listen( + proc(event: MessageErrorEvent) {.async: (raises: []).} = + echo "Message failed to send with request ID: ", + event.requestId, " error: ", event.error + ).valueOr: + echo "Failed to listen to message error event: ", error + return + + let propagatedListener = MessagePropagatedEvent.listen( + proc(event: MessagePropagatedEvent) {.async: (raises: []).} = + echo "Message propagated with request ID: ", + event.requestId, " hash: ", event.messageHash + ).valueOr: + echo "Failed to listen to message propagated event: ", error + return + + defer: + await MessageSentEvent.dropListener(sentListener) + await MessageErrorEvent.dropListener(errorListener) + await MessagePropagatedEvent.dropListener(propagatedListener) + + ## Periodically sends a Waku message every 30 seconds + var counter = 0 + while true: + let envelope = MessageEnvelope.init( + contentTopic = "example/content/topic", + payload = "Hello Waku! Message number: " & $counter, + ) + + let sendRequestId = (await w.send(envelope)).valueOr: + echo "Failed to send message: ", error + quit(QuitFailure) + + echo "Sending message with request ID: ", sendRequestId, " counter: ", counter + + counter += 1 + await sleepAsync(30.seconds) + +when isMainModule: + let args = CliArgs.load() + + echo "Starting Waku node..." + + # Use WakuNodeConf (the CLI configuration type) for node setup + var conf = defaultWakuNodeConf().valueOr: + echo "Failed to create default config: ", error + quit(QuitFailure) + + if args.ethRpcEndpoint == "": + # Create a basic configuration for the Waku node + # No RLN as we don't have an ETH RPC Endpoint + conf.mode = Core + conf.preset = "logos.dev" + else: + # Connect to TWN, use ETH RPC Endpoint for RLN + conf.mode = Core + conf.preset = "twn" + conf.ethClientUrls = @[EthRpcUrl(args.ethRpcEndpoint)] + + # Create the node using the library API's createNode function + let node = (waitFor createNode(conf)).valueOr: + echo "Failed to create node: ", error + quit(QuitFailure) + + echo("Waku node created successfully!") + + # Start the node + (waitFor startWaku(addr node)).isOkOr: + echo "Failed to start node: ", error + quit(QuitFailure) + + echo "Node started successfully!" + + asyncSpawn periodicSender(node) + + runForever() diff --git a/examples/cbindings/waku_example.c b/examples/cbindings/waku_example.c index 35ac8a2e2..f337203ae 100644 --- a/examples/cbindings/waku_example.c +++ b/examples/cbindings/waku_example.c @@ -19,283 +19,309 @@ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int callback_executed = 0; -void waitForCallback() { - pthread_mutex_lock(&mutex); - while (!callback_executed) { - pthread_cond_wait(&cond, &mutex); - } - callback_executed = 0; - pthread_mutex_unlock(&mutex); +void waitForCallback() +{ + pthread_mutex_lock(&mutex); + while (!callback_executed) + { + pthread_cond_wait(&cond, &mutex); + } + callback_executed = 0; + pthread_mutex_unlock(&mutex); } -#define WAKU_CALL(call) \ -do { \ - int ret = call; \ - if (ret != 0) { \ - printf("Failed the call to: %s. Returned code: %d\n", #call, ret); \ - exit(1); \ - } \ - waitForCallback(); \ -} while (0) +#define WAKU_CALL(call) \ + do \ + { \ + int ret = call; \ + if (ret != 0) \ + { \ + printf("Failed the call to: %s. Returned code: %d\n", #call, ret); \ + exit(1); \ + } \ + waitForCallback(); \ + } while (0) -struct ConfigNode { - char host[128]; - int port; - char key[128]; - int relay; - char peers[2048]; - int store; - char storeNode[2048]; - char storeRetentionPolicy[64]; - char storeDbUrl[256]; - int storeVacuum; - int storeDbMigration; - int storeMaxNumDbConnections; +struct ConfigNode +{ + char host[128]; + int port; + char key[128]; + int relay; + char peers[2048]; + int store; + char storeNode[2048]; + char storeRetentionPolicy[64]; + char storeDbUrl[256]; + int storeVacuum; + int storeDbMigration; + int storeMaxNumDbConnections; }; // libwaku Context -void* ctx; +void *ctx; // For the case of C language we don't need to store a particular userData -void* userData = NULL; +void *userData = NULL; // Arguments parsing static char doc[] = "\nC example that shows how to use the waku library."; static char args_doc[] = ""; static struct argp_option options[] = { - { "host", 'h', "HOST", 0, "IP to listen for for LibP2P traffic. (default: \"0.0.0.0\")"}, - { "port", 'p', "PORT", 0, "TCP listening port. (default: \"60000\")"}, - { "key", 'k', "KEY", 0, "P2P node private key as 64 char hex string."}, - { "relay", 'r', "RELAY", 0, "Enable relay protocol: 1 or 0. (default: 1)"}, - { "peers", 'a', "PEERS", 0, "Comma-separated list of peer-multiaddress to connect\ + {"host", 'h', "HOST", 0, "IP to listen for for LibP2P traffic. (default: \"0.0.0.0\")"}, + {"port", 'p', "PORT", 0, "TCP listening port. (default: \"60000\")"}, + {"key", 'k', "KEY", 0, "P2P node private key as 64 char hex string."}, + {"relay", 'r', "RELAY", 0, "Enable relay protocol: 1 or 0. (default: 1)"}, + {"peers", 'a', "PEERS", 0, "Comma-separated list of peer-multiaddress to connect\ to. (default: \"\") e.g. \"/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\""}, - { 0 } -}; + {0}}; -static error_t parse_opt(int key, char *arg, struct argp_state *state) { +static error_t parse_opt(int key, char *arg, struct argp_state *state) +{ - struct ConfigNode *cfgNode = state->input; - switch (key) { - case 'h': - snprintf(cfgNode->host, 128, "%s", arg); - break; - case 'p': - cfgNode->port = atoi(arg); - break; - case 'k': - snprintf(cfgNode->key, 128, "%s", arg); - break; - case 'r': - cfgNode->relay = atoi(arg); - break; - case 'a': - snprintf(cfgNode->peers, 2048, "%s", arg); - break; - case ARGP_KEY_ARG: - if (state->arg_num >= 1) /* Too many arguments. */ - argp_usage(state); - break; - case ARGP_KEY_END: - break; - default: - return ARGP_ERR_UNKNOWN; - } + struct ConfigNode *cfgNode = state->input; + switch (key) + { + case 'h': + snprintf(cfgNode->host, 128, "%s", arg); + break; + case 'p': + cfgNode->port = atoi(arg); + break; + case 'k': + snprintf(cfgNode->key, 128, "%s", arg); + break; + case 'r': + cfgNode->relay = atoi(arg); + break; + case 'a': + snprintf(cfgNode->peers, 2048, "%s", arg); + break; + case ARGP_KEY_ARG: + if (state->arg_num >= 1) /* Too many arguments. */ + argp_usage(state); + break; + case ARGP_KEY_END: + break; + default: + return ARGP_ERR_UNKNOWN; + } - return 0; + return 0; } -void signal_cond() { - pthread_mutex_lock(&mutex); - callback_executed = 1; - pthread_cond_signal(&cond); - pthread_mutex_unlock(&mutex); +void signal_cond() +{ + pthread_mutex_lock(&mutex); + callback_executed = 1; + pthread_cond_signal(&cond); + pthread_mutex_unlock(&mutex); } -static struct argp argp = { options, parse_opt, args_doc, doc, 0, 0, 0 }; +static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; -void event_handler(int callerRet, const char* msg, size_t len, void* userData) { - if (callerRet == RET_ERR) { - printf("Error: %s\n", msg); - exit(1); - } - else if (callerRet == RET_OK) { - printf("Receiving event: %s\n", msg); - } +void event_handler(int callerRet, const char *msg, size_t len, void *userData) +{ + if (callerRet == RET_ERR) + { + printf("Error: %s\n", msg); + exit(1); + } + else if (callerRet == RET_OK) + { + printf("Receiving event: %s\n", msg); + } - signal_cond(); + signal_cond(); } -void on_event_received(int callerRet, const char* msg, size_t len, void* userData) { - if (callerRet == RET_ERR) { - printf("Error: %s\n", msg); - exit(1); - } - else if (callerRet == RET_OK) { - printf("Receiving event: %s\n", msg); - } +void on_event_received(int callerRet, const char *msg, size_t len, void *userData) +{ + if (callerRet == RET_ERR) + { + printf("Error: %s\n", msg); + exit(1); + } + else if (callerRet == RET_OK) + { + printf("Receiving event: %s\n", msg); + } } -char* contentTopic = NULL; -void handle_content_topic(int callerRet, const char* msg, size_t len, void* userData) { - if (contentTopic != NULL) { - free(contentTopic); - } +char *contentTopic = NULL; +void handle_content_topic(int callerRet, const char *msg, size_t len, void *userData) +{ + if (contentTopic != NULL) + { + free(contentTopic); + } - contentTopic = malloc(len * sizeof(char) + 1); - strcpy(contentTopic, msg); - signal_cond(); + contentTopic = malloc(len * sizeof(char) + 1); + strcpy(contentTopic, msg); + signal_cond(); } -char* publishResponse = NULL; -void handle_publish_ok(int callerRet, const char* msg, size_t len, void* userData) { - printf("Publish Ok: %s %lu\n", msg, len); +char *publishResponse = NULL; +void handle_publish_ok(int callerRet, const char *msg, size_t len, void *userData) +{ + printf("Publish Ok: %s %lu\n", msg, len); - if (publishResponse != NULL) { - free(publishResponse); - } + if (publishResponse != NULL) + { + free(publishResponse); + } - publishResponse = malloc(len * sizeof(char) + 1); - strcpy(publishResponse, msg); + publishResponse = malloc(len * sizeof(char) + 1); + strcpy(publishResponse, msg); } #define MAX_MSG_SIZE 65535 -void publish_message(const char* msg) { - char jsonWakuMsg[MAX_MSG_SIZE]; - char *msgPayload = b64_encode(msg, strlen(msg)); +void publish_message(const char *msg) +{ + char jsonWakuMsg[MAX_MSG_SIZE]; + char *msgPayload = b64_encode(msg, strlen(msg)); - WAKU_CALL( waku_content_topic(ctx, - "appName", - 1, - "contentTopicName", - "encoding", - handle_content_topic, - userData) ); - snprintf(jsonWakuMsg, - MAX_MSG_SIZE, - "{\"payload\":\"%s\",\"contentTopic\":\"%s\"}", - msgPayload, contentTopic); + WAKU_CALL(waku_content_topic(ctx, + handle_content_topic, + userData, + "appName", + 1, + "contentTopicName", + "encoding")); + snprintf(jsonWakuMsg, + MAX_MSG_SIZE, + "{\"payload\":\"%s\",\"contentTopic\":\"%s\"}", + msgPayload, contentTopic); - free(msgPayload); + free(msgPayload); - WAKU_CALL( waku_relay_publish(ctx, - "/waku/2/rs/16/32", - jsonWakuMsg, - 10000 /*timeout ms*/, - event_handler, - userData) ); + WAKU_CALL(waku_relay_publish(ctx, + event_handler, + userData, + "/waku/2/rs/16/32", + jsonWakuMsg, + 10000 /*timeout ms*/)); } -void show_help_and_exit() { - printf("Wrong parameters\n"); - exit(1); +void show_help_and_exit() +{ + printf("Wrong parameters\n"); + exit(1); } -void print_default_pubsub_topic(int callerRet, const char* msg, size_t len, void* userData) { - printf("Default pubsub topic: %s\n", msg); - signal_cond(); +void print_default_pubsub_topic(int callerRet, const char *msg, size_t len, void *userData) +{ + printf("Default pubsub topic: %s\n", msg); + signal_cond(); } -void print_waku_version(int callerRet, const char* msg, size_t len, void* userData) { - printf("Git Version: %s\n", msg); - signal_cond(); +void print_waku_version(int callerRet, const char *msg, size_t len, void *userData) +{ + printf("Git Version: %s\n", msg); + signal_cond(); } // Beginning of UI program logic -enum PROGRAM_STATE { - MAIN_MENU, - SUBSCRIBE_TOPIC_MENU, - CONNECT_TO_OTHER_NODE_MENU, - PUBLISH_MESSAGE_MENU +enum PROGRAM_STATE +{ + MAIN_MENU, + SUBSCRIBE_TOPIC_MENU, + CONNECT_TO_OTHER_NODE_MENU, + PUBLISH_MESSAGE_MENU }; enum PROGRAM_STATE current_state = MAIN_MENU; -void show_main_menu() { - printf("\nPlease, select an option:\n"); - printf("\t1.) Subscribe to topic\n"); - printf("\t2.) Connect to other node\n"); - printf("\t3.) Publish a message\n"); +void show_main_menu() +{ + printf("\nPlease, select an option:\n"); + printf("\t1.) Subscribe to topic\n"); + printf("\t2.) Connect to other node\n"); + printf("\t3.) Publish a message\n"); } -void handle_user_input() { - char cmd[1024]; - memset(cmd, 0, 1024); - int numRead = read(0, cmd, 1024); - if (numRead <= 0) { - return; - } +void handle_user_input() +{ + char cmd[1024]; + memset(cmd, 0, 1024); + int numRead = read(0, cmd, 1024); + if (numRead <= 0) + { + return; + } - switch (atoi(cmd)) - { - case SUBSCRIBE_TOPIC_MENU: - { - printf("Indicate the Pubsubtopic to subscribe:\n"); - char pubsubTopic[128]; - scanf("%127s", pubsubTopic); + switch (atoi(cmd)) + { + case SUBSCRIBE_TOPIC_MENU: + { + printf("Indicate the Pubsubtopic to subscribe:\n"); + char pubsubTopic[128]; + scanf("%127s", pubsubTopic); - WAKU_CALL( waku_relay_subscribe(ctx, - pubsubTopic, - event_handler, - userData) ); - printf("The subscription went well\n"); + WAKU_CALL(waku_relay_subscribe(ctx, + event_handler, + userData, + pubsubTopic)); + printf("The subscription went well\n"); - show_main_menu(); - } + show_main_menu(); + } + break; + + case CONNECT_TO_OTHER_NODE_MENU: + // printf("Connecting to a node. Please indicate the peer Multiaddress:\n"); + // printf("e.g.: /ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\n"); + // char peerAddr[512]; + // scanf("%511s", peerAddr); + // WAKU_CALL(waku_connect(ctx, peerAddr, 10000 /* timeoutMs */, event_handler, userData)); + show_main_menu(); break; - case CONNECT_TO_OTHER_NODE_MENU: - printf("Connecting to a node. Please indicate the peer Multiaddress:\n"); - printf("e.g.: /ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\n"); - char peerAddr[512]; - scanf("%511s", peerAddr); - WAKU_CALL(waku_connect(ctx, peerAddr, 10000 /* timeoutMs */, event_handler, userData)); - show_main_menu(); + case PUBLISH_MESSAGE_MENU: + { + printf("Type the message to publish:\n"); + char msg[1024]; + scanf("%1023s", msg); + + publish_message(msg); + + show_main_menu(); + } + break; + + case MAIN_MENU: break; - - case PUBLISH_MESSAGE_MENU: - { - printf("Type the message to publish:\n"); - char msg[1024]; - scanf("%1023s", msg); - - publish_message(msg); - - show_main_menu(); - } - break; - - case MAIN_MENU: - break; - } + } } // End of UI program logic -int main(int argc, char** argv) { - struct ConfigNode cfgNode; - // default values - snprintf(cfgNode.host, 128, "0.0.0.0"); - cfgNode.port = 60000; - cfgNode.relay = 1; +int main(int argc, char **argv) +{ + struct ConfigNode cfgNode; + // default values + snprintf(cfgNode.host, 128, "0.0.0.0"); + cfgNode.port = 60000; + cfgNode.relay = 1; - cfgNode.store = 0; - snprintf(cfgNode.storeNode, 2048, ""); - snprintf(cfgNode.storeRetentionPolicy, 64, "time:6000000"); - snprintf(cfgNode.storeDbUrl, 256, "postgres://postgres:test123@localhost:5432/postgres"); - cfgNode.storeVacuum = 0; - cfgNode.storeDbMigration = 0; - cfgNode.storeMaxNumDbConnections = 30; + cfgNode.store = 0; + snprintf(cfgNode.storeNode, 2048, ""); + snprintf(cfgNode.storeRetentionPolicy, 64, "time:6000000"); + snprintf(cfgNode.storeDbUrl, 256, "postgres://postgres:test123@localhost:5432/postgres"); + cfgNode.storeVacuum = 0; + cfgNode.storeDbMigration = 0; + cfgNode.storeMaxNumDbConnections = 30; - if (argp_parse(&argp, argc, argv, 0, 0, &cfgNode) - == ARGP_ERR_UNKNOWN) { - show_help_and_exit(); - } + if (argp_parse(&argp, argc, argv, 0, 0, &cfgNode) == ARGP_ERR_UNKNOWN) + { + show_help_and_exit(); + } - char jsonConfig[5000]; - snprintf(jsonConfig, 5000, "{ \ + char jsonConfig[5000]; + snprintf(jsonConfig, 5000, "{ \ \"clusterId\": 16, \ \"shards\": [ 1, 32, 64, 128, 256 ], \ \"numShardsInNetwork\": 257, \ @@ -313,54 +339,56 @@ int main(int argc, char** argv) { \"discv5UdpPort\": 9999, \ \"dnsDiscoveryUrl\": \"enrtree://AMOJVZX4V6EXP7NTJPMAYJYST2QP6AJXYW76IU6VGJS7UVSNDYZG4@boot.prod.status.nodes.status.im\", \ \"dnsDiscoveryNameServers\": [\"8.8.8.8\", \"1.0.0.1\"] \ - }", cfgNode.host, - cfgNode.port, - cfgNode.relay ? "true":"false", - cfgNode.store ? "true":"false", - cfgNode.storeDbUrl, - cfgNode.storeRetentionPolicy, - cfgNode.storeMaxNumDbConnections); + }", + cfgNode.host, + cfgNode.port, + cfgNode.relay ? "true" : "false", + cfgNode.store ? "true" : "false", + cfgNode.storeDbUrl, + cfgNode.storeRetentionPolicy, + cfgNode.storeMaxNumDbConnections); - ctx = waku_new(jsonConfig, event_handler, userData); - waitForCallback(); + ctx = waku_new(jsonConfig, event_handler, userData); + waitForCallback(); - WAKU_CALL( waku_default_pubsub_topic(ctx, print_default_pubsub_topic, userData) ); - WAKU_CALL( waku_version(ctx, print_waku_version, userData) ); + WAKU_CALL(waku_default_pubsub_topic(ctx, print_default_pubsub_topic, userData)); + WAKU_CALL(waku_version(ctx, print_waku_version, userData)); - printf("Bind addr: %s:%u\n", cfgNode.host, cfgNode.port); - printf("Waku Relay enabled: %s\n", cfgNode.relay == 1 ? "YES": "NO"); + printf("Bind addr: %s:%u\n", cfgNode.host, cfgNode.port); + printf("Waku Relay enabled: %s\n", cfgNode.relay == 1 ? "YES" : "NO"); - waku_set_event_callback(ctx, on_event_received, userData); + set_event_callback(ctx, on_event_received, userData); - waku_start(ctx, event_handler, userData); - waitForCallback(); + waku_start(ctx, event_handler, userData); + waitForCallback(); - WAKU_CALL( waku_listen_addresses(ctx, event_handler, userData) ); + WAKU_CALL(waku_listen_addresses(ctx, event_handler, userData)); - WAKU_CALL( waku_relay_subscribe(ctx, - "/waku/2/rs/0/0", - event_handler, - userData) ); + WAKU_CALL(waku_relay_subscribe(ctx, + event_handler, + userData, + "/waku/2/rs/16/32")); - WAKU_CALL( waku_discv5_update_bootnodes(ctx, - "[\"enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw\",\"enr:-QEkuEB3WHNS-xA3RDpfu9A2Qycr3bN3u7VoArMEiDIFZJ66F1EB3d4wxZN1hcdcOX-RfuXB-MQauhJGQbpz3qUofOtLAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPK35Nnz0cWUtSAhBp7zvHEhyU_AqeQUlqzLiLxfP2L4oN0Y3CCdl-DdWRwgiMohXdha3UyDw\"]", - event_handler, - userData) ); + WAKU_CALL(waku_discv5_update_bootnodes(ctx, + event_handler, + userData, + "[\"enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw\",\"enr:-QEkuEB3WHNS-xA3RDpfu9A2Qycr3bN3u7VoArMEiDIFZJ66F1EB3d4wxZN1hcdcOX-RfuXB-MQauhJGQbpz3qUofOtLAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPK35Nnz0cWUtSAhBp7zvHEhyU_AqeQUlqzLiLxfP2L4oN0Y3CCdl-DdWRwgiMohXdha3UyDw\"]")); - WAKU_CALL( waku_get_peerids_from_peerstore(ctx, - event_handler, - userData) ); + WAKU_CALL(waku_get_peerids_from_peerstore(ctx, + event_handler, + userData)); - show_main_menu(); - while(1) { - handle_user_input(); + show_main_menu(); + while (1) + { + handle_user_input(); - // Uncomment the following if need to test the metrics retrieval - // WAKU_CALL( waku_get_metrics(ctx, - // event_handler, - // userData) ); - } + // Uncomment the following if need to test the metrics retrieval + // WAKU_CALL( waku_get_metrics(ctx, + // event_handler, + // userData) ); + } - pthread_mutex_destroy(&mutex); - pthread_cond_destroy(&cond); + pthread_mutex_destroy(&mutex); + pthread_cond_destroy(&cond); } diff --git a/examples/cpp/waku.cpp b/examples/cpp/waku.cpp index c47877d02..2824f8e53 100644 --- a/examples/cpp/waku.cpp +++ b/examples/cpp/waku.cpp @@ -21,37 +21,43 @@ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int callback_executed = 0; -void waitForCallback() { +void waitForCallback() +{ pthread_mutex_lock(&mutex); - while (!callback_executed) { + while (!callback_executed) + { pthread_cond_wait(&cond, &mutex); } callback_executed = 0; pthread_mutex_unlock(&mutex); } -void signal_cond() { +void signal_cond() +{ pthread_mutex_lock(&mutex); callback_executed = 1; pthread_cond_signal(&cond); pthread_mutex_unlock(&mutex); } -#define WAKU_CALL(call) \ -do { \ - int ret = call; \ - if (ret != 0) { \ - std::cout << "Failed the call to: " << #call << ". Code: " << ret << "\n"; \ - } \ - waitForCallback(); \ -} while (0) +#define WAKU_CALL(call) \ + do \ + { \ + int ret = call; \ + if (ret != 0) \ + { \ + std::cout << "Failed the call to: " << #call << ". Code: " << ret << "\n"; \ + } \ + waitForCallback(); \ + } while (0) -struct ConfigNode { - char host[128]; - int port; - char key[128]; - int relay; - char peers[2048]; +struct ConfigNode +{ + char host[128]; + int port; + char key[128]; + int relay; + char peers[2048]; }; // Arguments parsing @@ -59,70 +65,76 @@ static char doc[] = "\nC example that shows how to use the waku library."; static char args_doc[] = ""; static struct argp_option options[] = { - { "host", 'h', "HOST", 0, "IP to listen for for LibP2P traffic. (default: \"0.0.0.0\")"}, - { "port", 'p', "PORT", 0, "TCP listening port. (default: \"60000\")"}, - { "key", 'k', "KEY", 0, "P2P node private key as 64 char hex string."}, - { "relay", 'r', "RELAY", 0, "Enable relay protocol: 1 or 0. (default: 1)"}, - { "peers", 'a', "PEERS", 0, "Comma-separated list of peer-multiaddress to connect\ + {"host", 'h', "HOST", 0, "IP to listen for for LibP2P traffic. (default: \"0.0.0.0\")"}, + {"port", 'p', "PORT", 0, "TCP listening port. (default: \"60000\")"}, + {"key", 'k', "KEY", 0, "P2P node private key as 64 char hex string."}, + {"relay", 'r', "RELAY", 0, "Enable relay protocol: 1 or 0. (default: 1)"}, + {"peers", 'a', "PEERS", 0, "Comma-separated list of peer-multiaddress to connect\ to. (default: \"\") e.g. \"/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\""}, - { 0 } -}; + {0}}; -static error_t parse_opt(int key, char *arg, struct argp_state *state) { +static error_t parse_opt(int key, char *arg, struct argp_state *state) +{ - struct ConfigNode *cfgNode = (ConfigNode *) state->input; - switch (key) { - case 'h': - snprintf(cfgNode->host, 128, "%s", arg); - break; - case 'p': - cfgNode->port = atoi(arg); - break; - case 'k': - snprintf(cfgNode->key, 128, "%s", arg); - break; - case 'r': - cfgNode->relay = atoi(arg); - break; - case 'a': - snprintf(cfgNode->peers, 2048, "%s", arg); - break; - case ARGP_KEY_ARG: - if (state->arg_num >= 1) /* Too many arguments. */ + struct ConfigNode *cfgNode = (ConfigNode *)state->input; + switch (key) + { + case 'h': + snprintf(cfgNode->host, 128, "%s", arg); + break; + case 'p': + cfgNode->port = atoi(arg); + break; + case 'k': + snprintf(cfgNode->key, 128, "%s", arg); + break; + case 'r': + cfgNode->relay = atoi(arg); + break; + case 'a': + snprintf(cfgNode->peers, 2048, "%s", arg); + break; + case ARGP_KEY_ARG: + if (state->arg_num >= 1) /* Too many arguments. */ argp_usage(state); - break; - case ARGP_KEY_END: - break; - default: - return ARGP_ERR_UNKNOWN; - } + break; + case ARGP_KEY_END: + break; + default: + return ARGP_ERR_UNKNOWN; + } return 0; } -void event_handler(const char* msg, size_t len) { +void event_handler(const char *msg, size_t len) +{ printf("Receiving event: %s\n", msg); } -void handle_error(const char* msg, size_t len) { +void handle_error(const char *msg, size_t len) +{ printf("handle_error: %s\n", msg); exit(1); } template -auto cify(F&& f) { - static F fn = std::forward(f); - return [](int callerRet, const char* msg, size_t len, void* userData) { - signal_cond(); - return fn(msg, len); - }; +auto cify(F &&f) +{ + static F fn = std::forward(f); + return [](int callerRet, const char *msg, size_t len, void *userData) + { + signal_cond(); + return fn(msg, len); + }; } -static struct argp argp = { options, parse_opt, args_doc, doc, 0, 0, 0 }; +static struct argp argp = {options, parse_opt, args_doc, doc, 0, 0, 0}; // Beginning of UI program logic -enum PROGRAM_STATE { +enum PROGRAM_STATE +{ MAIN_MENU, SUBSCRIBE_TOPIC_MENU, CONNECT_TO_OTHER_NODE_MENU, @@ -131,18 +143,21 @@ enum PROGRAM_STATE { enum PROGRAM_STATE current_state = MAIN_MENU; -void show_main_menu() { +void show_main_menu() +{ printf("\nPlease, select an option:\n"); printf("\t1.) Subscribe to topic\n"); printf("\t2.) Connect to other node\n"); printf("\t3.) Publish a message\n"); } -void handle_user_input(void* ctx) { +void handle_user_input(void *ctx) +{ char cmd[1024]; memset(cmd, 0, 1024); int numRead = read(0, cmd, 1024); - if (numRead <= 0) { + if (numRead <= 0) + { return; } @@ -154,12 +169,11 @@ void handle_user_input(void* ctx) { char pubsubTopic[128]; scanf("%127s", pubsubTopic); - WAKU_CALL( waku_relay_subscribe(ctx, - pubsubTopic, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr) ); + WAKU_CALL(waku_relay_subscribe(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr, + pubsubTopic)); printf("The subscription went well\n"); show_main_menu(); @@ -171,15 +185,14 @@ void handle_user_input(void* ctx) { printf("e.g.: /ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\n"); char peerAddr[512]; scanf("%511s", peerAddr); - WAKU_CALL( waku_connect(ctx, - peerAddr, - 10000 /* timeoutMs */, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr)); + WAKU_CALL(waku_connect(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr, + peerAddr, + 10000 /* timeoutMs */)); show_main_menu(); - break; + break; case PUBLISH_MESSAGE_MENU: { @@ -193,28 +206,26 @@ void handle_user_input(void* ctx) { std::string contentTopic; waku_content_topic(ctx, + cify([&contentTopic](const char *msg, size_t len) + { contentTopic = msg; }), + nullptr, "appName", - 1, - "contentTopicName", - "encoding", - cify([&contentTopic](const char* msg, size_t len) { - contentTopic = msg; - }), - nullptr); + 1, + "contentTopicName", + "encoding"); snprintf(jsonWakuMsg, 2048, "{\"payload\":\"%s\",\"contentTopic\":\"%s\"}", msgPayload.data(), contentTopic.c_str()); - WAKU_CALL( waku_relay_publish(ctx, - "/waku/2/rs/16/32", - jsonWakuMsg, - 10000 /*timeout ms*/, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr) ); + WAKU_CALL(waku_relay_publish(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr, + "/waku/2/rs/16/32", + jsonWakuMsg, + 10000 /*timeout ms*/)); show_main_menu(); } @@ -227,12 +238,14 @@ void handle_user_input(void* ctx) { // End of UI program logic -void show_help_and_exit() { +void show_help_and_exit() +{ printf("Wrong parameters\n"); exit(1); } -int main(int argc, char** argv) { +int main(int argc, char **argv) +{ struct ConfigNode cfgNode; // default values snprintf(cfgNode.host, 128, "0.0.0.0"); @@ -241,8 +254,8 @@ int main(int argc, char** argv) { cfgNode.port = 60000; cfgNode.relay = 1; - if (argp_parse(&argp, argc, argv, 0, 0, &cfgNode) - == ARGP_ERR_UNKNOWN) { + if (argp_parse(&argp, argc, argv, 0, 0, &cfgNode) == ARGP_ERR_UNKNOWN) + { show_help_and_exit(); } @@ -260,72 +273,64 @@ int main(int argc, char** argv) { \"discv5UdpPort\": 9999, \ \"dnsDiscoveryUrl\": \"enrtree://AMOJVZX4V6EXP7NTJPMAYJYST2QP6AJXYW76IU6VGJS7UVSNDYZG4@boot.prod.status.nodes.status.im\", \ \"dnsDiscoveryNameServers\": [\"8.8.8.8\", \"1.0.0.1\"] \ - }", cfgNode.host, - cfgNode.port); + }", + cfgNode.host, + cfgNode.port); - void* ctx = + void *ctx = waku_new(jsonConfig, - cify([](const char* msg, size_t len) { - std::cout << "waku_new feedback: " << msg << std::endl; - } - ), - nullptr - ); + cify([](const char *msg, size_t len) + { std::cout << "waku_new feedback: " << msg << std::endl; }), + nullptr); waitForCallback(); // example on how to retrieve a value from the `libwaku` callback. std::string defaultPubsubTopic; WAKU_CALL( waku_default_pubsub_topic( - ctx, - cify([&defaultPubsubTopic](const char* msg, size_t len) { - defaultPubsubTopic = msg; - } - ), - nullptr)); + ctx, + cify([&defaultPubsubTopic](const char *msg, size_t len) + { defaultPubsubTopic = msg; }), + nullptr)); std::cout << "Default pubsub topic: " << defaultPubsubTopic << std::endl; - WAKU_CALL(waku_version(ctx, - cify([&](const char* msg, size_t len) { - std::cout << "Git Version: " << msg << std::endl; - }), + WAKU_CALL(waku_version(ctx, + cify([&](const char *msg, size_t len) + { std::cout << "Git Version: " << msg << std::endl; }), nullptr)); printf("Bind addr: %s:%u\n", cfgNode.host, cfgNode.port); - printf("Waku Relay enabled: %s\n", cfgNode.relay == 1 ? "YES": "NO"); + printf("Waku Relay enabled: %s\n", cfgNode.relay == 1 ? "YES" : "NO"); std::string pubsubTopic; - WAKU_CALL(waku_pubsub_topic(ctx, - "example", - cify([&](const char* msg, size_t len) { - pubsubTopic = msg; - }), - nullptr)); + WAKU_CALL(waku_pubsub_topic(ctx, + cify([&](const char *msg, size_t len) + { pubsubTopic = msg; }), + nullptr, + "example")); std::cout << "Custom pubsub topic: " << pubsubTopic << std::endl; - waku_set_event_callback(ctx, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr); + set_event_callback(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr); - WAKU_CALL( waku_start(ctx, - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr)); + WAKU_CALL(waku_start(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr)); - WAKU_CALL( waku_relay_subscribe(ctx, - defaultPubsubTopic.c_str(), - cify([&](const char* msg, size_t len) { - event_handler(msg, len); - }), - nullptr) ); + WAKU_CALL(waku_relay_subscribe(ctx, + cify([&](const char *msg, size_t len) + { event_handler(msg, len); }), + nullptr, + defaultPubsubTopic.c_str())); show_main_menu(); - while(1) { + while (1) + { handle_user_input(ctx); } } diff --git a/examples/filter_subscriber.nim b/examples/filter_subscriber.nim index e4e26bdb7..03a5de4eb 100644 --- a/examples/filter_subscriber.nim +++ b/examples/filter_subscriber.nim @@ -62,13 +62,9 @@ proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} = "Building ENR with relay sharding failed" ) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) var builder = WakuNodeBuilder.init() builder.withNodeKey(nodeKey) @@ -92,20 +88,18 @@ proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} = while true: notice "maintaining subscription" # First use filter-ping to check if we have an active subscription - let pingRes = await node.wakuFilterClient.ping(filterPeer) - if pingRes.isErr(): + if (await node.wakuFilterClient.ping(filterPeer)).isErr(): # No subscription found. Let's subscribe. notice "no subscription found. Sending subscribe request" - let subscribeRes = await node.wakuFilterClient.subscribe( - filterPeer, FilterPubsubTopic, @[FilterContentTopic] - ) - - if subscribeRes.isErr(): - notice "subscribe request failed. Quitting.", err = subscribeRes.error + ( + await node.wakuFilterClient.subscribe( + filterPeer, FilterPubsubTopic, @[FilterContentTopic] + ) + ).isOkOr: + notice "subscribe request failed. Quitting.", error = error break - else: - notice "subscribe request successful." + notice "subscribe request successful." else: notice "subscription found." diff --git a/examples/golang/waku.go b/examples/golang/waku.go index 846362dfe..e205ecd09 100644 --- a/examples/golang/waku.go +++ b/examples/golang/waku.go @@ -71,32 +71,32 @@ package main static void* cGoWakuNew(const char* configJson, void* resp) { // We pass NULL because we are not interested in retrieving data from this callback - void* ret = waku_new(configJson, (WakuCallBack) callback, resp); + void* ret = waku_new(configJson, (FFICallBack) callback, resp); return ret; } static void cGoWakuStart(void* wakuCtx, void* resp) { - WAKU_CALL(waku_start(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_start(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuStop(void* wakuCtx, void* resp) { - WAKU_CALL(waku_stop(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_stop(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuDestroy(void* wakuCtx, void* resp) { - WAKU_CALL(waku_destroy(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_destroy(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuStartDiscV5(void* wakuCtx, void* resp) { - WAKU_CALL(waku_start_discv5(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_start_discv5(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuStopDiscV5(void* wakuCtx, void* resp) { - WAKU_CALL(waku_stop_discv5(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_stop_discv5(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuVersion(void* wakuCtx, void* resp) { - WAKU_CALL(waku_version(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL(waku_version(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuSetEventCallback(void* wakuCtx) { @@ -112,7 +112,7 @@ package main // This technique is needed because cgo only allows to export Go functions and not methods. - waku_set_event_callback(wakuCtx, (WakuCallBack) globalEventCallback, wakuCtx); + set_event_callback(wakuCtx, (FFICallBack) globalEventCallback, wakuCtx); } static void cGoWakuContentTopic(void* wakuCtx, @@ -123,20 +123,21 @@ package main void* resp) { WAKU_CALL( waku_content_topic(wakuCtx, + (FFICallBack) callback, + resp, appName, appVersion, contentTopicName, - encoding, - (WakuCallBack) callback, - resp) ); + encoding + ) ); } static void cGoWakuPubsubTopic(void* wakuCtx, char* topicName, void* resp) { - WAKU_CALL( waku_pubsub_topic(wakuCtx, topicName, (WakuCallBack) callback, resp) ); + WAKU_CALL( waku_pubsub_topic(wakuCtx, (FFICallBack) callback, resp, topicName) ); } static void cGoWakuDefaultPubsubTopic(void* wakuCtx, void* resp) { - WAKU_CALL (waku_default_pubsub_topic(wakuCtx, (WakuCallBack) callback, resp)); + WAKU_CALL (waku_default_pubsub_topic(wakuCtx, (FFICallBack) callback, resp)); } static void cGoWakuRelayPublish(void* wakuCtx, @@ -146,34 +147,36 @@ package main void* resp) { WAKU_CALL (waku_relay_publish(wakuCtx, + (FFICallBack) callback, + resp, pubSubTopic, jsonWakuMessage, - timeoutMs, - (WakuCallBack) callback, - resp)); + timeoutMs + )); } static void cGoWakuRelaySubscribe(void* wakuCtx, char* pubSubTopic, void* resp) { WAKU_CALL ( waku_relay_subscribe(wakuCtx, - pubSubTopic, - (WakuCallBack) callback, - resp) ); + (FFICallBack) callback, + resp, + pubSubTopic) ); } static void cGoWakuRelayUnsubscribe(void* wakuCtx, char* pubSubTopic, void* resp) { WAKU_CALL ( waku_relay_unsubscribe(wakuCtx, - pubSubTopic, - (WakuCallBack) callback, - resp) ); + (FFICallBack) callback, + resp, + pubSubTopic) ); } static void cGoWakuConnect(void* wakuCtx, char* peerMultiAddr, int timeoutMs, void* resp) { WAKU_CALL( waku_connect(wakuCtx, + (FFICallBack) callback, + resp, peerMultiAddr, - timeoutMs, - (WakuCallBack) callback, - resp) ); + timeoutMs + ) ); } static void cGoWakuDialPeerById(void* wakuCtx, @@ -183,42 +186,44 @@ package main void* resp) { WAKU_CALL( waku_dial_peer_by_id(wakuCtx, + (FFICallBack) callback, + resp, peerId, protocol, - timeoutMs, - (WakuCallBack) callback, - resp) ); + timeoutMs + ) ); } static void cGoWakuDisconnectPeerById(void* wakuCtx, char* peerId, void* resp) { WAKU_CALL( waku_disconnect_peer_by_id(wakuCtx, - peerId, - (WakuCallBack) callback, - resp) ); + (FFICallBack) callback, + resp, + peerId + ) ); } static void cGoWakuListenAddresses(void* wakuCtx, void* resp) { - WAKU_CALL (waku_listen_addresses(wakuCtx, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_listen_addresses(wakuCtx, (FFICallBack) callback, resp) ); } static void cGoWakuGetMyENR(void* ctx, void* resp) { - WAKU_CALL (waku_get_my_enr(ctx, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_get_my_enr(ctx, (FFICallBack) callback, resp) ); } static void cGoWakuGetMyPeerId(void* ctx, void* resp) { - WAKU_CALL (waku_get_my_peerid(ctx, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_get_my_peerid(ctx, (FFICallBack) callback, resp) ); } static void cGoWakuListPeersInMesh(void* ctx, char* pubSubTopic, void* resp) { - WAKU_CALL (waku_relay_get_num_peers_in_mesh(ctx, pubSubTopic, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_relay_get_num_peers_in_mesh(ctx, (FFICallBack) callback, resp, pubSubTopic) ); } static void cGoWakuGetNumConnectedPeers(void* ctx, char* pubSubTopic, void* resp) { - WAKU_CALL (waku_relay_get_num_connected_peers(ctx, pubSubTopic, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_relay_get_num_connected_peers(ctx, (FFICallBack) callback, resp, pubSubTopic) ); } static void cGoWakuGetPeerIdsFromPeerStore(void* wakuCtx, void* resp) { - WAKU_CALL (waku_get_peerids_from_peerstore(wakuCtx, (WakuCallBack) callback, resp) ); + WAKU_CALL (waku_get_peerids_from_peerstore(wakuCtx, (FFICallBack) callback, resp) ); } static void cGoWakuLightpushPublish(void* wakuCtx, @@ -227,10 +232,11 @@ package main void* resp) { WAKU_CALL (waku_lightpush_publish(wakuCtx, + (FFICallBack) callback, + resp, pubSubTopic, - jsonWakuMessage, - (WakuCallBack) callback, - resp)); + jsonWakuMessage + )); } static void cGoWakuStoreQuery(void* wakuCtx, @@ -240,11 +246,12 @@ package main void* resp) { WAKU_CALL (waku_store_query(wakuCtx, + (FFICallBack) callback, + resp, jsonQuery, peerAddr, - timeoutMs, - (WakuCallBack) callback, - resp)); + timeoutMs + )); } static void cGoWakuPeerExchangeQuery(void* wakuCtx, @@ -252,9 +259,10 @@ package main void* resp) { WAKU_CALL (waku_peer_exchange_request(wakuCtx, - numPeers, - (WakuCallBack) callback, - resp)); + (FFICallBack) callback, + resp, + numPeers + )); } static void cGoWakuGetPeerIdsByProtocol(void* wakuCtx, @@ -262,9 +270,10 @@ package main void* resp) { WAKU_CALL (waku_get_peerids_by_protocol(wakuCtx, - protocol, - (WakuCallBack) callback, - resp)); + (FFICallBack) callback, + resp, + protocol + )); } */ diff --git a/examples/ios/WakuExample.xcodeproj/project.pbxproj b/examples/ios/WakuExample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..b7ce1dce7 --- /dev/null +++ b/examples/ios/WakuExample.xcodeproj/project.pbxproj @@ -0,0 +1,331 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 63; + objects = { + +/* Begin PBXBuildFile section */ + 45714AF6D1D12AF5C36694FB /* WakuExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */; }; + 6468FA3F5F760D3FCAD6CDBF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D8744E36DADC11F38A1CC99 /* ContentView.swift */; }; + C4EA202B782038F96336401F /* WakuNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 638A565C495A63CFF7396FBC /* WakuNode.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WakuExampleApp.swift; sourceTree = ""; }; + 31BE20DB2755A11000723420 /* libwaku.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = libwaku.h; sourceTree = ""; }; + 5C5AAC91E0166D28BFA986DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 638A565C495A63CFF7396FBC /* WakuNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WakuNode.swift; sourceTree = ""; }; + 7D8744E36DADC11F38A1CC99 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + A8655016B3DF9B0877631CE5 /* WakuExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "WakuExample-Bridging-Header.h"; sourceTree = ""; }; + CFBE844B6E18ACB81C65F83B /* WakuExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = WakuExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + 34547A6259485BD047D6375C /* Products */ = { + isa = PBXGroup; + children = ( + CFBE844B6E18ACB81C65F83B /* WakuExample.app */, + ); + name = Products; + sourceTree = ""; + }; + 4F76CB85EC44E951B8E75522 /* WakuExample */ = { + isa = PBXGroup; + children = ( + 7D8744E36DADC11F38A1CC99 /* ContentView.swift */, + 5C5AAC91E0166D28BFA986DB /* Info.plist */, + 31BE20DB2755A11000723420 /* libwaku.h */, + A8655016B3DF9B0877631CE5 /* WakuExample-Bridging-Header.h */, + 0671AF6DCB0D788B0C1E9C8B /* WakuExampleApp.swift */, + 638A565C495A63CFF7396FBC /* WakuNode.swift */, + ); + path = WakuExample; + sourceTree = ""; + }; + D40CD2446F177CAABB0A747A = { + isa = PBXGroup; + children = ( + 4F76CB85EC44E951B8E75522 /* WakuExample */, + 34547A6259485BD047D6375C /* Products */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F751EF8294AD21F713D47FDA /* WakuExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 757FA0123629BD63CB254113 /* Build configuration list for PBXNativeTarget "WakuExample" */; + buildPhases = ( + D3AFD8C4DA68BF5C4F7D8E10 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = WakuExample; + packageProductDependencies = ( + ); + productName = WakuExample; + productReference = CFBE844B6E18ACB81C65F83B /* WakuExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 4FF82F0F4AF8E1E34728F150 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1500; + }; + buildConfigurationList = B3A4F48294254543E79767C4 /* Build configuration list for PBXProject "WakuExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = D40CD2446F177CAABB0A747A; + minimizedProjectReferenceProxies = 1; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F751EF8294AD21F713D47FDA /* WakuExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + D3AFD8C4DA68BF5C4F7D8E10 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6468FA3F5F760D3FCAD6CDBF /* ContentView.swift in Sources */, + 45714AF6D1D12AF5C36694FB /* WakuExampleApp.swift in Sources */, + C4EA202B782038F96336401F /* WakuNode.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 36939122077C66DD94082311 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 2Q52K2W84K; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/WakuExample"; + INFOPLIST_FILE = WakuExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64"; + MACOSX_DEPLOYMENT_TARGET = 15.6; + OTHER_LDFLAGS = ( + "-lc++", + "-force_load", + "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64/libwaku.a", + "-lsqlite3", + "-lz", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.waku.example; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "WakuExample/WakuExample-Bridging-Header.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 9BA833A09EEDB4B3FCCD8F8E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + A59ABFB792FED8974231E5AC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "DEBUG=1", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + AF5ADDAA865B1F6BD4E70A79 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "iPhone Developer"; + DEVELOPMENT_TEAM = 2Q52K2W84K; + HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/WakuExample"; + INFOPLIST_FILE = WakuExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 18.6; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64"; + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64"; + MACOSX_DEPLOYMENT_TARGET = 15.6; + OTHER_LDFLAGS = ( + "-lc++", + "-force_load", + "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64/libwaku.a", + "-lsqlite3", + "-lz", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.waku.example; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; + SWIFT_OBJC_BRIDGING_HEADER = "WakuExample/WakuExample-Bridging-Header.h"; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 757FA0123629BD63CB254113 /* Build configuration list for PBXNativeTarget "WakuExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AF5ADDAA865B1F6BD4E70A79 /* Debug */, + 36939122077C66DD94082311 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + B3A4F48294254543E79767C4 /* Build configuration list for PBXProject "WakuExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A59ABFB792FED8974231E5AC /* Debug */, + 9BA833A09EEDB4B3FCCD8F8E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 4FF82F0F4AF8E1E34728F150 /* Project object */; +} diff --git a/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/examples/ios/WakuExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/ios/WakuExample/ContentView.swift b/examples/ios/WakuExample/ContentView.swift new file mode 100644 index 000000000..14bb4ee1d --- /dev/null +++ b/examples/ios/WakuExample/ContentView.swift @@ -0,0 +1,229 @@ +// +// ContentView.swift +// WakuExample +// +// Minimal chat PoC using libwaku on iOS +// + +import SwiftUI + +struct ContentView: View { + @StateObject private var wakuNode = WakuNode() + @State private var messageText = "" + + var body: some View { + ZStack { + // Main content + VStack(spacing: 0) { + // Header with status + HStack { + Circle() + .fill(statusColor) + .frame(width: 10, height: 10) + VStack(alignment: .leading, spacing: 2) { + Text(wakuNode.status.rawValue) + .font(.caption) + if wakuNode.status == .running { + HStack(spacing: 4) { + Text(wakuNode.isConnected ? "Connected" : "Discovering...") + Text("•") + filterStatusView + } + .font(.caption2) + .foregroundColor(.secondary) + + // Subscription maintenance status + if wakuNode.subscriptionMaintenanceActive { + HStack(spacing: 4) { + Image(systemName: "arrow.triangle.2.circlepath") + .foregroundColor(.blue) + Text("Maintenance active") + if wakuNode.failedSubscribeAttempts > 0 { + Text("(\(wakuNode.failedSubscribeAttempts) retries)") + .foregroundColor(.orange) + } + } + .font(.caption2) + .foregroundColor(.secondary) + } + } + } + Spacer() + if wakuNode.status == .stopped { + Button("Start") { + wakuNode.start() + } + .buttonStyle(.borderedProminent) + .controlSize(.small) + } else if wakuNode.status == .running { + if !wakuNode.filterSubscribed { + Button("Resub") { + wakuNode.resubscribe() + } + .buttonStyle(.bordered) + .controlSize(.small) + } + Button("Stop") { + wakuNode.stop() + } + .buttonStyle(.bordered) + .controlSize(.small) + } + } + .padding() + .background(Color.gray.opacity(0.1)) + + // Messages list + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 8) { + ForEach(wakuNode.receivedMessages.reversed()) { message in + MessageBubble(message: message) + .id(message.id) + } + } + .padding() + } + .onChange(of: wakuNode.receivedMessages.count) { _, newCount in + if let lastMessage = wakuNode.receivedMessages.first { + withAnimation { + proxy.scrollTo(lastMessage.id, anchor: .bottom) + } + } + } + } + + Divider() + + // Message input + HStack(spacing: 12) { + TextField("Message", text: $messageText) + .textFieldStyle(.roundedBorder) + .disabled(wakuNode.status != .running) + + Button(action: sendMessage) { + Image(systemName: "paperplane.fill") + .foregroundColor(.white) + .padding(10) + .background(canSend ? Color.blue : Color.gray) + .clipShape(Circle()) + } + .disabled(!canSend) + } + .padding() + .background(Color.gray.opacity(0.1)) + } + + // Toast overlay for errors + VStack { + ForEach(wakuNode.errorQueue) { error in + ToastView(error: error) { + wakuNode.dismissError(error) + } + .transition(.asymmetric( + insertion: .move(edge: .top).combined(with: .opacity), + removal: .opacity + )) + } + Spacer() + } + .padding(.top, 8) + .animation(.easeInOut(duration: 0.3), value: wakuNode.errorQueue) + } + } + + private var statusColor: Color { + switch wakuNode.status { + case .stopped: return .gray + case .starting: return .yellow + case .running: return .green + case .error: return .red + } + } + + @ViewBuilder + private var filterStatusView: some View { + if wakuNode.filterSubscribed { + Text("Filter OK") + .foregroundColor(.green) + } else if wakuNode.failedSubscribeAttempts > 0 { + Text("Filter retrying (\(wakuNode.failedSubscribeAttempts))") + .foregroundColor(.orange) + } else { + Text("Filter pending") + .foregroundColor(.orange) + } + } + + private var canSend: Bool { + wakuNode.status == .running && wakuNode.isConnected && !messageText.trimmingCharacters(in: .whitespaces).isEmpty + } + + private func sendMessage() { + let text = messageText.trimmingCharacters(in: .whitespaces) + guard !text.isEmpty else { return } + + wakuNode.publish(message: text) + messageText = "" + } +} + +// MARK: - Toast View + +struct ToastView: View { + let error: TimestampedError + let onDismiss: () -> Void + + var body: some View { + HStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.white) + + Text(error.message) + .font(.subheadline) + .foregroundColor(.white) + .lineLimit(2) + + Spacer() + + Button(action: onDismiss) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.white.opacity(0.8)) + .font(.title3) + } + .buttonStyle(.plain) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + .background( + RoundedRectangle(cornerRadius: 12) + .fill(Color.red.opacity(0.9)) + .shadow(color: .black.opacity(0.2), radius: 8, x: 0, y: 4) + ) + .padding(.horizontal, 16) + .padding(.vertical, 4) + } +} + +// MARK: - Message Bubble + +struct MessageBubble: View { + let message: WakuMessage + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text(message.payload) + .padding(10) + .background(Color.blue.opacity(0.1)) + .cornerRadius(12) + + Text(message.timestamp, style: .time) + .font(.caption2) + .foregroundColor(.secondary) + } + } +} + +#Preview { + ContentView() +} diff --git a/examples/ios/WakuExample/Info.plist b/examples/ios/WakuExample/Info.plist new file mode 100644 index 000000000..a9222555a --- /dev/null +++ b/examples/ios/WakuExample/Info.plist @@ -0,0 +1,36 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Waku Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.waku.example + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + WakuExample + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + + + diff --git a/examples/ios/WakuExample/WakuExample-Bridging-Header.h b/examples/ios/WakuExample/WakuExample-Bridging-Header.h new file mode 100644 index 000000000..50595450e --- /dev/null +++ b/examples/ios/WakuExample/WakuExample-Bridging-Header.h @@ -0,0 +1,15 @@ +// +// WakuExample-Bridging-Header.h +// WakuExample +// +// Bridging header to expose libwaku C functions to Swift +// + +#ifndef WakuExample_Bridging_Header_h +#define WakuExample_Bridging_Header_h + +#import "libwaku.h" + +#endif /* WakuExample_Bridging_Header_h */ + + diff --git a/examples/ios/WakuExample/WakuExampleApp.swift b/examples/ios/WakuExample/WakuExampleApp.swift new file mode 100644 index 000000000..fb99785aa --- /dev/null +++ b/examples/ios/WakuExample/WakuExampleApp.swift @@ -0,0 +1,19 @@ +// +// WakuExampleApp.swift +// WakuExample +// +// SwiftUI app entry point for Waku iOS example +// + +import SwiftUI + +@main +struct WakuExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} + + diff --git a/examples/ios/WakuExample/WakuNode.swift b/examples/ios/WakuExample/WakuNode.swift new file mode 100644 index 000000000..245529a2f --- /dev/null +++ b/examples/ios/WakuExample/WakuNode.swift @@ -0,0 +1,739 @@ +// +// WakuNode.swift +// WakuExample +// +// Swift wrapper around libwaku C API for edge mode (lightpush + filter) +// Uses Swift actors for thread safety and UI responsiveness +// + +import Foundation + +// MARK: - Data Types + +/// Message received from Waku network +struct WakuMessage: Identifiable, Equatable, Sendable { + let id: String // messageHash from Waku - unique identifier for deduplication + let payload: String + let contentTopic: String + let timestamp: Date +} + +/// Waku node status +enum WakuNodeStatus: String, Sendable { + case stopped = "Stopped" + case starting = "Starting..." + case running = "Running" + case error = "Error" +} + +/// Status updates from WakuActor to WakuNode +enum WakuStatusUpdate: Sendable { + case statusChanged(WakuNodeStatus) + case connectionChanged(isConnected: Bool) + case filterSubscriptionChanged(subscribed: Bool, failedAttempts: Int) + case maintenanceChanged(active: Bool) + case error(String) +} + +/// Error with timestamp for toast queue +struct TimestampedError: Identifiable, Equatable { + let id = UUID() + let message: String + let timestamp: Date + + static func == (lhs: TimestampedError, rhs: TimestampedError) -> Bool { + lhs.id == rhs.id + } +} + +// MARK: - Callback Context for C API + +private final class CallbackContext: @unchecked Sendable { + private let lock = NSLock() + private var _continuation: CheckedContinuation<(success: Bool, result: String?), Never>? + private var _resumed = false + var success: Bool = false + var result: String? + + var continuation: CheckedContinuation<(success: Bool, result: String?), Never>? { + get { + lock.lock() + defer { lock.unlock() } + return _continuation + } + set { + lock.lock() + defer { lock.unlock() } + _continuation = newValue + } + } + + /// Thread-safe resume - ensures continuation is only resumed once + /// Returns true if this call actually resumed, false if already resumed + @discardableResult + func resumeOnce(returning value: (success: Bool, result: String?)) -> Bool { + lock.lock() + defer { lock.unlock() } + + guard !_resumed, let cont = _continuation else { + return false + } + + _resumed = true + _continuation = nil + cont.resume(returning: value) + return true + } +} + +// MARK: - WakuActor + +/// Actor that isolates all Waku operations from the main thread +/// All C API calls and mutable state are contained here +actor WakuActor { + + // MARK: - State + + private var ctx: UnsafeMutableRawPointer? + private var seenMessageHashes: Set = [] + private var isSubscribed: Bool = false + private var isSubscribing: Bool = false + private var hasPeers: Bool = false + private var maintenanceTask: Task? + private var eventProcessingTask: Task? + + // Stream continuations for communicating with UI + private var messageContinuation: AsyncStream.Continuation? + private var statusContinuation: AsyncStream.Continuation? + + // Event stream from C callbacks + private var eventContinuation: AsyncStream.Continuation? + + // Configuration + let defaultPubsubTopic = "/waku/2/rs/1/0" + let defaultContentTopic = "/waku-ios-example/1/chat/proto" + private let staticPeer = "/dns4/node-01.do-ams3.waku.sandbox.status.im/tcp/30303/p2p/16Uiu2HAmPLe7Mzm8TsYUubgCAW1aJoeFScxrLj8ppHFivPo97bUZ" + + // Subscription maintenance settings + private let maxFailedSubscribes = 3 + private let retryWaitSeconds: UInt64 = 2_000_000_000 // 2 seconds in nanoseconds + private let maintenanceIntervalSeconds: UInt64 = 30_000_000_000 // 30 seconds in nanoseconds + private let maxSeenHashes = 1000 + + // MARK: - Static callback storage (for C callbacks) + + // We need a way for C callbacks to reach the actor + // Using a simple static reference (safe because we only have one instance) + private static var sharedEventContinuation: AsyncStream.Continuation? + + private static let eventCallback: WakuCallBack = { ret, msg, len, userData in + guard ret == RET_OK, let msg = msg else { return } + let str = String(cString: msg) + WakuActor.sharedEventContinuation?.yield(str) + } + + private static let syncCallback: WakuCallBack = { ret, msg, len, userData in + guard let userData = userData else { return } + let context = Unmanaged.fromOpaque(userData).takeUnretainedValue() + let success = (ret == RET_OK) + var resultStr: String? = nil + if let msg = msg { + resultStr = String(cString: msg) + } + context.resumeOnce(returning: (success, resultStr)) + } + + // MARK: - Stream Setup + + func setMessageContinuation(_ continuation: AsyncStream.Continuation?) { + self.messageContinuation = continuation + } + + func setStatusContinuation(_ continuation: AsyncStream.Continuation?) { + self.statusContinuation = continuation + } + + // MARK: - Public API + + var isRunning: Bool { + ctx != nil + } + + var hasConnectedPeers: Bool { + hasPeers + } + + func start() async { + guard ctx == nil else { + print("[WakuActor] Already started") + return + } + + statusContinuation?.yield(.statusChanged(.starting)) + + // Create event stream for C callbacks + let eventStream = AsyncStream { continuation in + self.eventContinuation = continuation + WakuActor.sharedEventContinuation = continuation + } + + // Start event processing task + eventProcessingTask = Task { [weak self] in + for await eventJson in eventStream { + await self?.handleEvent(eventJson) + } + } + + // Initialize the node + let success = await initializeNode() + + if success { + statusContinuation?.yield(.statusChanged(.running)) + + // Connect to peer + let connected = await connectToPeer() + if connected { + hasPeers = true + statusContinuation?.yield(.connectionChanged(isConnected: true)) + + // Start maintenance loop + startMaintenanceLoop() + } else { + statusContinuation?.yield(.error("Failed to connect to service peer")) + } + } + } + + func stop() async { + guard let context = ctx else { return } + + // Stop maintenance loop + maintenanceTask?.cancel() + maintenanceTask = nil + + // Stop event processing + eventProcessingTask?.cancel() + eventProcessingTask = nil + + // Close event stream + eventContinuation?.finish() + eventContinuation = nil + WakuActor.sharedEventContinuation = nil + + statusContinuation?.yield(.statusChanged(.stopped)) + statusContinuation?.yield(.connectionChanged(isConnected: false)) + statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0)) + statusContinuation?.yield(.maintenanceChanged(active: false)) + + // Reset state + let ctxToStop = context + ctx = nil + isSubscribed = false + isSubscribing = false + hasPeers = false + seenMessageHashes.removeAll() + + // Unsubscribe and stop in background (fire and forget) + Task.detached { + // Unsubscribe + _ = await self.callWakuSync { waku_filter_unsubscribe_all(ctxToStop, WakuActor.syncCallback, $0) } + print("[WakuActor] Unsubscribed from filter") + + // Stop + _ = await self.callWakuSync { waku_stop(ctxToStop, WakuActor.syncCallback, $0) } + print("[WakuActor] Node stopped") + + // Destroy + _ = await self.callWakuSync { waku_destroy(ctxToStop, WakuActor.syncCallback, $0) } + print("[WakuActor] Node destroyed") + } + } + + func publish(message: String, contentTopic: String? = nil) async { + guard let context = ctx else { + print("[WakuActor] Node not started") + return + } + + guard hasPeers else { + print("[WakuActor] No peers connected yet") + statusContinuation?.yield(.error("No peers connected yet. Please wait...")) + return + } + + let topic = contentTopic ?? defaultContentTopic + guard let payloadData = message.data(using: .utf8) else { return } + let payloadBase64 = payloadData.base64EncodedString() + let timestamp = Int64(Date().timeIntervalSince1970 * 1_000_000_000) + let jsonMessage = """ + {"payload":"\(payloadBase64)","contentTopic":"\(topic)","timestamp":\(timestamp)} + """ + + let result = await callWakuSync { userData in + waku_lightpush_publish( + context, + self.defaultPubsubTopic, + jsonMessage, + WakuActor.syncCallback, + userData + ) + } + + if result.success { + print("[WakuActor] Published message") + } else { + print("[WakuActor] Publish error: \(result.result ?? "unknown")") + statusContinuation?.yield(.error("Failed to send message")) + } + } + + func resubscribe() async { + print("[WakuActor] Force resubscribe requested") + isSubscribed = false + isSubscribing = false + statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0)) + _ = await subscribe() + } + + // MARK: - Private Methods + + private func initializeNode() async -> Bool { + let config = """ + { + "tcpPort": 60000, + "clusterId": 1, + "shards": [0], + "relay": false, + "lightpush": true, + "filter": true, + "logLevel": "DEBUG", + "discv5Discovery": true, + "discv5BootstrapNodes": [ + "enr:-QESuEB4Dchgjn7gfAvwB00CxTA-nGiyk-aALI-H4dYSZD3rUk7bZHmP8d2U6xDiQ2vZffpo45Jp7zKNdnwDUx6g4o6XAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOvD3S3jUNICsrOILlmhENiWAMmMVlAl6-Q8wRB7hidY4N0Y3CCdl-DdWRwgiMohXdha3UyDw", + "enr:-QEkuEBIkb8q8_mrorHndoXH9t5N6ZfD-jehQCrYeoJDPHqT0l0wyaONa2-piRQsi3oVKAzDShDVeoQhy0uwN1xbZfPZAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQKnGt-GSgqPSf3IAPM7bFgTlpczpMZZLF3geeoNNsxzSoN0Y3CCdl-DdWRwgiMohXdha3UyDw" + ], + "discv5UdpPort": 9999, + "dnsDiscovery": true, + "dnsDiscoveryUrl": "enrtree://AOGYWMBYOUIMOENHXCHILPKY3ZRFEULMFI4DOM442QSZ73TT2A7VI@test.waku.nodes.status.im", + "dnsDiscoveryNameServers": ["8.8.8.8", "1.0.0.1"] + } + """ + + // Create node - waku_new is special, it returns the context directly + let createResult = await withCheckedContinuation { (continuation: CheckedContinuation<(ctx: UnsafeMutableRawPointer?, success: Bool, result: String?), Never>) in + let callbackCtx = CallbackContext() + let userDataPtr = Unmanaged.passRetained(callbackCtx).toOpaque() + + // Set up a simple callback for waku_new + let newCtx = waku_new(config, { ret, msg, len, userData in + guard let userData = userData else { return } + let context = Unmanaged.fromOpaque(userData).takeUnretainedValue() + context.success = (ret == RET_OK) + if let msg = msg { + context.result = String(cString: msg) + } + }, userDataPtr) + + // Small delay to ensure callback completes + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + Unmanaged.fromOpaque(userDataPtr).release() + continuation.resume(returning: (newCtx, callbackCtx.success, callbackCtx.result)) + } + } + + guard createResult.ctx != nil else { + statusContinuation?.yield(.statusChanged(.error)) + statusContinuation?.yield(.error("Failed to create node: \(createResult.result ?? "unknown")")) + return false + } + + ctx = createResult.ctx + + // Set event callback + waku_set_event_callback(ctx, WakuActor.eventCallback, nil) + + // Start node + let startResult = await callWakuSync { userData in + waku_start(self.ctx, WakuActor.syncCallback, userData) + } + + guard startResult.success else { + statusContinuation?.yield(.statusChanged(.error)) + statusContinuation?.yield(.error("Failed to start node: \(startResult.result ?? "unknown")")) + ctx = nil + return false + } + + print("[WakuActor] Node started") + return true + } + + private func connectToPeer() async -> Bool { + guard let context = ctx else { return false } + + print("[WakuActor] Connecting to static peer...") + + let result = await callWakuSync { userData in + waku_connect(context, self.staticPeer, 10000, WakuActor.syncCallback, userData) + } + + if result.success { + print("[WakuActor] Connected to peer successfully") + return true + } else { + print("[WakuActor] Failed to connect: \(result.result ?? "unknown")") + return false + } + } + + private func subscribe(contentTopic: String? = nil) async -> Bool { + guard let context = ctx else { return false } + guard !isSubscribed && !isSubscribing else { return isSubscribed } + + isSubscribing = true + let topic = contentTopic ?? defaultContentTopic + + let result = await callWakuSync { userData in + waku_filter_subscribe( + context, + self.defaultPubsubTopic, + topic, + WakuActor.syncCallback, + userData + ) + } + + isSubscribing = false + + if result.success { + print("[WakuActor] Subscribe request successful to \(topic)") + isSubscribed = true + statusContinuation?.yield(.filterSubscriptionChanged(subscribed: true, failedAttempts: 0)) + return true + } else { + print("[WakuActor] Subscribe error: \(result.result ?? "unknown")") + isSubscribed = false + return false + } + } + + private func pingFilterPeer() async -> Bool { + guard let context = ctx else { return false } + + let result = await callWakuSync { userData in + waku_ping_peer( + context, + self.staticPeer, + 10000, + WakuActor.syncCallback, + userData + ) + } + + return result.success + } + + // MARK: - Subscription Maintenance + + private func startMaintenanceLoop() { + guard maintenanceTask == nil else { + print("[WakuActor] Maintenance loop already running") + return + } + + statusContinuation?.yield(.maintenanceChanged(active: true)) + print("[WakuActor] Starting subscription maintenance loop") + + maintenanceTask = Task { [weak self] in + guard let self = self else { return } + + var failedSubscribes = 0 + var isFirstPingOnConnection = true + + while !Task.isCancelled { + guard await self.isRunning else { break } + + print("[WakuActor] Maintaining subscription...") + + let pingSuccess = await self.pingFilterPeer() + let currentlySubscribed = await self.isSubscribed + + if pingSuccess && currentlySubscribed { + print("[WakuActor] Subscription is live, waiting 30s") + try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds) + continue + } + + if !isFirstPingOnConnection && !pingSuccess { + print("[WakuActor] Ping failed - subscription may be lost") + await self.statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: failedSubscribes)) + } + isFirstPingOnConnection = false + + print("[WakuActor] No active subscription found. Sending subscribe request...") + + await self.resetSubscriptionState() + let subscribeSuccess = await self.subscribe() + + if subscribeSuccess { + print("[WakuActor] Subscribe request successful") + failedSubscribes = 0 + try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds) + continue + } + + failedSubscribes += 1 + await self.statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: failedSubscribes)) + print("[WakuActor] Subscribe request failed. Attempt \(failedSubscribes)/\(self.maxFailedSubscribes)") + + if failedSubscribes < self.maxFailedSubscribes { + print("[WakuActor] Retrying in 2s...") + try? await Task.sleep(nanoseconds: self.retryWaitSeconds) + } else { + print("[WakuActor] Max subscribe failures reached") + await self.statusContinuation?.yield(.error("Filter subscription failed after \(self.maxFailedSubscribes) attempts")) + failedSubscribes = 0 + try? await Task.sleep(nanoseconds: self.maintenanceIntervalSeconds) + } + } + + print("[WakuActor] Subscription maintenance loop stopped") + await self.statusContinuation?.yield(.maintenanceChanged(active: false)) + } + } + + private func resetSubscriptionState() { + isSubscribed = false + isSubscribing = false + } + + // MARK: - Event Handling + + private func handleEvent(_ eventJson: String) { + guard let data = eventJson.data(using: .utf8), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let eventType = json["eventType"] as? String else { + return + } + + if eventType == "connection_change" { + handleConnectionChange(json) + } else if eventType == "message" { + handleMessage(json) + } + } + + private func handleConnectionChange(_ json: [String: Any]) { + guard let peerEvent = json["peerEvent"] as? String else { return } + + if peerEvent == "Joined" || peerEvent == "Identified" { + hasPeers = true + statusContinuation?.yield(.connectionChanged(isConnected: true)) + } else if peerEvent == "Left" { + statusContinuation?.yield(.filterSubscriptionChanged(subscribed: false, failedAttempts: 0)) + } + } + + private func handleMessage(_ json: [String: Any]) { + guard let messageHash = json["messageHash"] as? String, + let wakuMessage = json["wakuMessage"] as? [String: Any], + let payloadBase64 = wakuMessage["payload"] as? String, + let contentTopic = wakuMessage["contentTopic"] as? String, + let payloadData = Data(base64Encoded: payloadBase64), + let payloadString = String(data: payloadData, encoding: .utf8) else { + return + } + + // Deduplicate + guard !seenMessageHashes.contains(messageHash) else { + return + } + + seenMessageHashes.insert(messageHash) + + // Limit memory usage + if seenMessageHashes.count > maxSeenHashes { + seenMessageHashes.removeAll() + } + + let message = WakuMessage( + id: messageHash, + payload: payloadString, + contentTopic: contentTopic, + timestamp: Date() + ) + + messageContinuation?.yield(message) + } + + // MARK: - Helper for synchronous C calls + + private func callWakuSync(_ work: @escaping (UnsafeMutableRawPointer) -> Void) async -> (success: Bool, result: String?) { + await withCheckedContinuation { continuation in + let context = CallbackContext() + context.continuation = continuation + let userDataPtr = Unmanaged.passRetained(context).toOpaque() + + work(userDataPtr) + + // Set a timeout to avoid hanging forever + DispatchQueue.global().asyncAfter(deadline: .now() + 15) { + // Try to resume with timeout - will be ignored if callback already resumed + let didTimeout = context.resumeOnce(returning: (false, "Timeout")) + if didTimeout { + print("[WakuActor] Call timed out") + } + Unmanaged.fromOpaque(userDataPtr).release() + } + } + } +} + +// MARK: - WakuNode (MainActor UI Wrapper) + +/// Main-thread UI wrapper that consumes updates from WakuActor via AsyncStreams +@MainActor +class WakuNode: ObservableObject { + + // MARK: - Published Properties (UI State) + + @Published var status: WakuNodeStatus = .stopped + @Published var receivedMessages: [WakuMessage] = [] + @Published var errorQueue: [TimestampedError] = [] + @Published var isConnected: Bool = false + @Published var filterSubscribed: Bool = false + @Published var subscriptionMaintenanceActive: Bool = false + @Published var failedSubscribeAttempts: Int = 0 + + // Topics (read-only access to actor's config) + var defaultPubsubTopic: String { "/waku/2/rs/1/0" } + var defaultContentTopic: String { "/waku-ios-example/1/chat/proto" } + + // MARK: - Private Properties + + private let actor = WakuActor() + private var messageTask: Task? + private var statusTask: Task? + + // MARK: - Initialization + + init() {} + + deinit { + messageTask?.cancel() + statusTask?.cancel() + } + + // MARK: - Public API + + func start() { + guard status == .stopped || status == .error else { + print("[WakuNode] Already started or starting") + return + } + + // Create message stream + let messageStream = AsyncStream { continuation in + Task { + await self.actor.setMessageContinuation(continuation) + } + } + + // Create status stream + let statusStream = AsyncStream { continuation in + Task { + await self.actor.setStatusContinuation(continuation) + } + } + + // Start consuming messages + messageTask = Task { @MainActor in + for await message in messageStream { + self.receivedMessages.insert(message, at: 0) + if self.receivedMessages.count > 100 { + self.receivedMessages.removeLast() + } + } + } + + // Start consuming status updates + statusTask = Task { @MainActor in + for await update in statusStream { + self.handleStatusUpdate(update) + } + } + + // Start the actor + Task { + await actor.start() + } + } + + func stop() { + messageTask?.cancel() + messageTask = nil + statusTask?.cancel() + statusTask = nil + + Task { + await actor.stop() + } + + // Immediate UI update + status = .stopped + isConnected = false + filterSubscribed = false + subscriptionMaintenanceActive = false + failedSubscribeAttempts = 0 + } + + func publish(message: String, contentTopic: String? = nil) { + Task { + await actor.publish(message: message, contentTopic: contentTopic) + } + } + + func resubscribe() { + Task { + await actor.resubscribe() + } + } + + func dismissError(_ error: TimestampedError) { + errorQueue.removeAll { $0.id == error.id } + } + + func dismissAllErrors() { + errorQueue.removeAll() + } + + // MARK: - Private Methods + + private func handleStatusUpdate(_ update: WakuStatusUpdate) { + switch update { + case .statusChanged(let newStatus): + status = newStatus + + case .connectionChanged(let connected): + isConnected = connected + + case .filterSubscriptionChanged(let subscribed, let attempts): + filterSubscribed = subscribed + failedSubscribeAttempts = attempts + + case .maintenanceChanged(let active): + subscriptionMaintenanceActive = active + + case .error(let message): + let error = TimestampedError(message: message, timestamp: Date()) + errorQueue.append(error) + + // Schedule auto-dismiss after 10 seconds + let errorId = error.id + Task { @MainActor in + try? await Task.sleep(nanoseconds: 10_000_000_000) + self.errorQueue.removeAll { $0.id == errorId } + } + } + } +} diff --git a/examples/ios/WakuExample/libwaku.h b/examples/ios/WakuExample/libwaku.h new file mode 100644 index 000000000..b5d6c9bab --- /dev/null +++ b/examples/ios/WakuExample/libwaku.h @@ -0,0 +1,253 @@ + +// Generated manually and inspired by the one generated by the Nim Compiler. +// In order to see the header file generated by Nim just run `make libwaku` +// from the root repo folder and the header should be created in +// nimcache/release/libwaku/libwaku.h +#ifndef __libwaku__ +#define __libwaku__ + +#include +#include + +// The possible returned values for the functions that return int +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*WakuCallBack) (int callerRet, const char* msg, size_t len, void* userData); + +// Creates a new instance of the waku node. +// Sets up the waku node from the given configuration. +// Returns a pointer to the Context needed by the rest of the API functions. +void* waku_new( + const char* configJson, + WakuCallBack callback, + void* userData); + +int waku_start(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_stop(void* ctx, + WakuCallBack callback, + void* userData); + +// Destroys an instance of a waku node created with waku_new +int waku_destroy(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_version(void* ctx, + WakuCallBack callback, + void* userData); + +// Sets a callback that will be invoked whenever an event occurs. +// It is crucial that the passed callback is fast, non-blocking and potentially thread-safe. +void waku_set_event_callback(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_content_topic(void* ctx, + const char* appName, + unsigned int appVersion, + const char* contentTopicName, + const char* encoding, + WakuCallBack callback, + void* userData); + +int waku_pubsub_topic(void* ctx, + const char* topicName, + WakuCallBack callback, + void* userData); + +int waku_default_pubsub_topic(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_relay_publish(void* ctx, + const char* pubSubTopic, + const char* jsonWakuMessage, + unsigned int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_lightpush_publish(void* ctx, + const char* pubSubTopic, + const char* jsonWakuMessage, + WakuCallBack callback, + void* userData); + +int waku_relay_subscribe(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_relay_add_protected_shard(void* ctx, + int clusterId, + int shardId, + char* publicKey, + WakuCallBack callback, + void* userData); + +int waku_relay_unsubscribe(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_filter_subscribe(void* ctx, + const char* pubSubTopic, + const char* contentTopics, + WakuCallBack callback, + void* userData); + +int waku_filter_unsubscribe(void* ctx, + const char* pubSubTopic, + const char* contentTopics, + WakuCallBack callback, + void* userData); + +int waku_filter_unsubscribe_all(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_relay_get_num_connected_peers(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_relay_get_connected_peers(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_relay_get_num_peers_in_mesh(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_relay_get_peers_in_mesh(void* ctx, + const char* pubSubTopic, + WakuCallBack callback, + void* userData); + +int waku_store_query(void* ctx, + const char* jsonQuery, + const char* peerAddr, + int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_connect(void* ctx, + const char* peerMultiAddr, + unsigned int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_disconnect_peer_by_id(void* ctx, + const char* peerId, + WakuCallBack callback, + void* userData); + +int waku_disconnect_all_peers(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_dial_peer(void* ctx, + const char* peerMultiAddr, + const char* protocol, + int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_dial_peer_by_id(void* ctx, + const char* peerId, + const char* protocol, + int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_get_peerids_from_peerstore(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_connected_peers_info(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_peerids_by_protocol(void* ctx, + const char* protocol, + WakuCallBack callback, + void* userData); + +int waku_listen_addresses(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_connected_peers(void* ctx, + WakuCallBack callback, + void* userData); + +// Returns a list of multiaddress given a url to a DNS discoverable ENR tree +// Parameters +// char* entTreeUrl: URL containing a discoverable ENR tree +// char* nameDnsServer: The nameserver to resolve the ENR tree url. +// int timeoutMs: Timeout value in milliseconds to execute the call. +int waku_dns_discovery(void* ctx, + const char* entTreeUrl, + const char* nameDnsServer, + int timeoutMs, + WakuCallBack callback, + void* userData); + +// Updates the bootnode list used for discovering new peers via DiscoveryV5 +// bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` +int waku_discv5_update_bootnodes(void* ctx, + char* bootnodes, + WakuCallBack callback, + void* userData); + +int waku_start_discv5(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_stop_discv5(void* ctx, + WakuCallBack callback, + void* userData); + +// Retrieves the ENR information +int waku_get_my_enr(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_my_peerid(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_get_metrics(void* ctx, + WakuCallBack callback, + void* userData); + +int waku_peer_exchange_request(void* ctx, + int numPeers, + WakuCallBack callback, + void* userData); + +int waku_ping_peer(void* ctx, + const char* peerAddr, + int timeoutMs, + WakuCallBack callback, + void* userData); + +int waku_is_online(void* ctx, + WakuCallBack callback, + void* userData); + +#ifdef __cplusplus +} +#endif + +#endif /* __libwaku__ */ diff --git a/examples/ios/project.yml b/examples/ios/project.yml new file mode 100644 index 000000000..9519e8b9e --- /dev/null +++ b/examples/ios/project.yml @@ -0,0 +1,47 @@ +name: WakuExample +options: + bundleIdPrefix: org.waku + deploymentTarget: + iOS: "14.0" + xcodeVersion: "15.0" + +settings: + SWIFT_VERSION: "5.0" + SUPPORTED_PLATFORMS: "iphoneos iphonesimulator" + SUPPORTS_MACCATALYST: "NO" + +targets: + WakuExample: + type: application + platform: iOS + supportedDestinations: [iOS] + sources: + - WakuExample + settings: + INFOPLIST_FILE: WakuExample/Info.plist + PRODUCT_BUNDLE_IDENTIFIER: org.waku.example + SWIFT_OBJC_BRIDGING_HEADER: WakuExample/WakuExample-Bridging-Header.h + HEADER_SEARCH_PATHS: + - "$(PROJECT_DIR)/WakuExample" + "LIBRARY_SEARCH_PATHS[sdk=iphoneos*]": + - "$(PROJECT_DIR)/../../build/ios/iphoneos-arm64" + "LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]": + - "$(PROJECT_DIR)/../../build/ios/iphonesimulator-arm64" + OTHER_LDFLAGS: + - "-lc++" + - "-lwaku" + IPHONEOS_DEPLOYMENT_TARGET: "14.0" + info: + path: WakuExample/Info.plist + properties: + CFBundleName: WakuExample + CFBundleDisplayName: Waku Example + CFBundleIdentifier: org.waku.example + CFBundleVersion: "1" + CFBundleShortVersionString: "1.0" + UILaunchScreen: {} + UISupportedInterfaceOrientations: + - UIInterfaceOrientationPortrait + NSAppTransportSecurity: + NSAllowsArbitraryLoads: true + diff --git a/examples/lightpush_mix/lightpush_publisher_mix.nim b/examples/lightpush_mix/lightpush_publisher_mix.nim index 1e26daa9b..104de8552 100644 --- a/examples/lightpush_mix/lightpush_publisher_mix.nim +++ b/examples/lightpush_mix/lightpush_publisher_mix.nim @@ -51,7 +51,6 @@ proc splitPeerIdAndAddr(maddr: string): (string, string) = proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} = # use notice to filter all waku messaging setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT) - notice "starting publisher", wakuPort = conf.port let @@ -114,17 +113,8 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} let dPeerId = PeerId.init(destPeerId).valueOr: error "Failed to initialize PeerId", error = error return - var conn: Connection - if not conf.mixDisabled: - conn = node.wakuMix.toConnection( - MixDestination.init(dPeerId, pxPeerInfo.addrs[0]), # destination lightpush peer - WakuLightPushCodec, # protocol codec which will be used over the mix connection - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - # mix parameters indicating we expect a single reply - ).valueOr: - error "failed to create mix connection", error = error - return + await node.mountRendezvousClient(clusterId) await node.start() node.peerManager.start() node.startPeerExchangeLoop() @@ -145,20 +135,26 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} var i = 0 while i < conf.numMsgs: + var conn: Connection if conf.mixDisabled: let connOpt = await node.peerManager.dialPeer(dPeerId, WakuLightPushCodec) if connOpt.isNone(): error "failed to dial peer with WakuLightPushCodec", target_peer_id = dPeerId return conn = connOpt.get() + else: + conn = node.wakuMix.toConnection( + MixDestination.exitNode(dPeerId), # destination lightpush peer + WakuLightPushCodec, # protocol codec which will be used over the mix connection + MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), + # mix parameters indicating we expect a single reply + ).valueOr: + error "failed to create mix connection", error = error + return i = i + 1 let text = """Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam venenatis magna ut tortor faucibus, in vestibulum nibh commodo. Aenean eget vestibulum augue. Nullam suscipit urna non nunc efficitur, at iaculis nisl consequat. Mauris quis ultrices elit. Suspendisse lobortis odio vitae laoreet facilisis. Cras ornare sem felis, at vulputate magna aliquam ac. Duis quis est ultricies, euismod nulla ac, interdum dui. Maecenas sit amet est vitae enim commodo gravida. Proin vitae elit nulla. Donec tempor dolor lectus, in faucibus velit elementum quis. Donec non mauris eu nibh faucibus cursus ut egestas dolor. Aliquam venenatis ligula id velit pulvinar malesuada. Vestibulum scelerisque, justo non porta gravida, nulla justo tempor purus, at sollicitudin erat erat vel libero. - Fusce nec eros eu metus tristique aliquet. Sed ut magna sagittis, vulputate diam sit amet, aliquam magna. Aenean sollicitudin velit lacus, eu ultrices magna semper at. Integer vitae felis ligula. In a eros nec risus condimentum tincidunt fermentum sit amet ex. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nullam vitae justo maximus, fringilla tellus nec, rutrum purus. Etiam efficitur nisi dapibus euismod vestibulum. Phasellus at felis elementum, tristique nulla ac, consectetur neque. - Maecenas hendrerit nibh eget velit rutrum, in ornare mauris molestie. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Praesent dignissim efficitur eros, sit amet rutrum justo mattis a. Fusce mollis neque at erat placerat bibendum. Ut fringilla fringilla orci, ut fringilla metus fermentum vel. In hac habitasse platea dictumst. Donec hendrerit porttitor odio. Suspendisse ornare sollicitudin mauris, sodales pulvinar velit finibus vel. Fusce id pulvinar neque. Suspendisse eget tincidunt sapien, ac accumsan turpis. - Curabitur cursus tincidunt leo at aliquet. Nunc dapibus quam id venenatis varius. Aenean eget augue vel velit dapibus aliquam. Nulla facilisi. Curabitur cursus, turpis vel congue volutpat, tellus eros cursus lacus, eu fringilla turpis orci non ipsum. In hac habitasse platea dictumst. Nulla aliquam nisl a nunc placerat, eget dignissim felis pulvinar. Fusce sed porta mauris. Donec sodales arcu in nisl sodales, quis posuere massa ultricies. Nam feugiat massa eget felis ultricies finibus. Nunc magna nulla, interdum a elit vel, egestas efficitur urna. Ut posuere tincidunt odio in maximus. Sed at dignissim est. - Morbi accumsan elementum ligula ut fringilla. Praesent in ex metus. Phasellus urna est, tempus sit amet elementum vitae, sollicitudin vel ipsum. Fusce hendrerit eleifend dignissim. Maecenas tempor dapibus dui quis laoreet. Cras tincidunt sed ipsum sed pellentesque. Proin ut tellus nec ipsum varius interdum. Curabitur id velit ligula. Etiam sapien nulla, cursus sodales orci eu, porta lobortis nunc. Nunc at dapibus velit. Nulla et nunc vehicula, condimentum erat quis, elementum dolor. Quisque eu metus fermentum, vestibulum tellus at, sollicitudin odio. Ut vel neque justo. - Praesent porta porta velit, vel porttitor sem. Donec sagittis at nulla venenatis iaculis. Nullam vel eleifend felis. Nullam a pellentesque lectus. Aliquam tincidunt semper dui sed bibendum. Donec hendrerit, urna et cursus dictum, neque neque convallis magna, id condimentum sem urna quis massa. Fusce non quam vulputate, fermentum mauris at, malesuada ipsum. Mauris id pellentesque libero. Donec vel erat ullamcorper, dapibus quam id, imperdiet urna. Praesent sed ligula ut est pellentesque pharetra quis et diam. Ut placerat lorem eget mi fermentum aliquet. + Fusce nec eros eu metus tristique aliquet. This is message #""" & $i & """ sent from a publisher using mix. End of transmission.""" let message = WakuMessage( @@ -168,25 +164,34 @@ proc setupAndPublish(rng: ref HmacDrbgContext, conf: LightPushMixConf) {.async.} timestamp: getNowInNanosecondTime(), ) # current timestamp - let res = await node.wakuLightpushClient.publishWithConn( - LightpushPubsubTopic, message, conn, dPeerId - ) + let res = + await node.wakuLightpushClient.publish(some(LightpushPubsubTopic), message, conn) - if res.isOk(): - lp_mix_success.inc() - notice "published message", - text = text, - timestamp = message.timestamp, - psTopic = LightpushPubsubTopic, - contentTopic = LightpushContentTopic - else: - error "failed to publish message", error = $res.error + let startTime = getNowInNanosecondTime() + + ( + await node.wakuLightpushClient.publishWithConn( + LightpushPubsubTopic, message, conn, dPeerId + ) + ).isOkOr: + error "failed to publish message via mix", error = error.desc lp_mix_failed.inc(labelValues = ["publish_error"]) + return + + let latency = float64(getNowInNanosecondTime() - startTime) / 1_000_000.0 + lp_mix_latency.observe(latency) + lp_mix_success.inc() + notice "published message", + text = text, + timestamp = message.timestamp, + latency = latency, + psTopic = LightpushPubsubTopic, + contentTopic = LightpushContentTopic if conf.mixDisabled: await conn.close() await sleepAsync(conf.msgIntervalMilliseconds) - info "###########Sent all messages via mix" + info "Sent all messages via mix" quit(0) when isMainModule: diff --git a/examples/lightpush_mix/lightpush_publisher_mix_metrics.nim b/examples/lightpush_mix/lightpush_publisher_mix_metrics.nim index cd06b3e3e..3c467e28c 100644 --- a/examples/lightpush_mix/lightpush_publisher_mix_metrics.nim +++ b/examples/lightpush_mix/lightpush_publisher_mix_metrics.nim @@ -6,3 +6,6 @@ declarePublicCounter lp_mix_success, "number of lightpush messages sent via mix" declarePublicCounter lp_mix_failed, "number of lightpush messages failed via mix", labels = ["error"] + +declarePublicHistogram lp_mix_latency, + "lightpush publish latency via mix in milliseconds" diff --git a/examples/lightpush_publisher.nim b/examples/lightpush_publisher.nim index 70ebd9c53..c7eacdd30 100644 --- a/examples/lightpush_publisher.nim +++ b/examples/lightpush_publisher.nim @@ -54,13 +54,9 @@ proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} = "Building ENR with relay sharding failed" ) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) var builder = WakuNodeBuilder.init() builder.withNodeKey(nodeKey) diff --git a/examples/publisher.nim b/examples/publisher.nim index 8c2d03679..6f5d34bc4 100644 --- a/examples/publisher.nim +++ b/examples/publisher.nim @@ -49,13 +49,9 @@ proc setupAndPublish(rng: ref HmacDrbgContext) {.async.} = var enrBuilder = EnrBuilder.init(nodeKey) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) var builder = WakuNodeBuilder.init() builder.withNodeKey(nodeKey) diff --git a/examples/python/waku.py b/examples/python/waku.py index 4d5f5643e..b2303e5e3 100644 --- a/examples/python/waku.py +++ b/examples/python/waku.py @@ -1,23 +1,32 @@ -from flask import Flask import ctypes import argparse +import sys + +if sys.platform == "darwin": + _lib_ext = "dylib" +elif sys.platform == "win32": + _lib_ext = "dll" +else: + _lib_ext = "so" + +_lib_path = f"build/libwaku.{_lib_ext}" libwaku = object try: # This python script should be run from the root repo folder - libwaku = ctypes.CDLL("build/libwaku.so") -except Exception as e: - print("Exception: ", e) - print(""" -The 'libwaku.so' library can be created with the next command from + libwaku = ctypes.CDLL(_lib_path) +except OSError as e: + print(f"Exception: {e}") + print(f""" +The '{_lib_path}' library can be created with the next command from the repo's root folder: `make libwaku`. -And it should build the library in 'build/libwaku.so'. +And it should build the library in '{_lib_path}'. -Therefore, make sure the LD_LIBRARY_PATH env var points at the location that -contains the 'libwaku.so' library. +Therefore, make sure the library path env var points at the location that +contains the '{_lib_path}' library. """) - exit(-1) + exit(1) def handle_event(ret, msg, user_data): print("Event received: %s" % msg) @@ -102,8 +111,8 @@ print("Waku Relay enabled: {}".format(args.relay)) # Set the event callback callback = callback_type(handle_event) # This line is important so that the callback is not gc'ed -libwaku.waku_set_event_callback.argtypes = [callback_type, ctypes.c_void_p] -libwaku.waku_set_event_callback(callback, ctypes.c_void_p(0)) +libwaku.set_event_callback.argtypes = [callback_type, ctypes.c_void_p] +libwaku.set_event_callback(callback, ctypes.c_void_p(0)) # Start the node libwaku.waku_start.argtypes = [ctypes.c_void_p, @@ -117,32 +126,32 @@ libwaku.waku_start(ctx, # Subscribe to the default pubsub topic libwaku.waku_relay_subscribe.argtypes = [ctypes.c_void_p, - ctypes.c_char_p, callback_type, - ctypes.c_void_p] + ctypes.c_void_p, + ctypes.c_char_p] libwaku.waku_relay_subscribe(ctx, - default_pubsub_topic.encode('utf-8'), callback_type( #onErrCb lambda ret, msg, len: print("Error calling waku_relay_subscribe: %s" % msg.decode('utf-8')) ), - ctypes.c_void_p(0)) + ctypes.c_void_p(0), + default_pubsub_topic.encode('utf-8')) libwaku.waku_connect.argtypes = [ctypes.c_void_p, - ctypes.c_char_p, - ctypes.c_int, callback_type, - ctypes.c_void_p] + ctypes.c_void_p, + ctypes.c_char_p, + ctypes.c_int] libwaku.waku_connect(ctx, - args.peer.encode('utf-8'), - 10000, # onErrCb callback_type( lambda ret, msg, len: print("Error calling waku_connect: %s" % msg.decode('utf-8'))), - ctypes.c_void_p(0)) + ctypes.c_void_p(0), + args.peer.encode('utf-8'), + 10000) # app = Flask(__name__) # @app.route("/") diff --git a/examples/qt/waku_handler.h b/examples/qt/waku_handler.h index 161a17c82..2fb3ce3b7 100644 --- a/examples/qt/waku_handler.h +++ b/examples/qt/waku_handler.h @@ -27,7 +27,7 @@ public: void initialize(const QString& jsonConfig, WakuCallBack event_handler, void* userData) { ctx = waku_new(jsonConfig.toUtf8().constData(), WakuCallBack(event_handler), userData); - waku_set_event_callback(ctx, on_event_received, userData); + set_event_callback(ctx, on_event_received, userData); qDebug() << "Waku context initialized, ready to start."; } diff --git a/examples/rust/src/main.rs b/examples/rust/src/main.rs index 926d0e3b0..d26e9627e 100644 --- a/examples/rust/src/main.rs +++ b/examples/rust/src/main.rs @@ -3,22 +3,22 @@ use std::ffi::CString; use std::os::raw::{c_char, c_int, c_void}; use std::{slice, thread, time}; -pub type WakuCallback = unsafe extern "C" fn(c_int, *const c_char, usize, *const c_void); +pub type FFICallBack = unsafe extern "C" fn(c_int, *const c_char, usize, *const c_void); extern "C" { pub fn waku_new( config_json: *const u8, - cb: WakuCallback, + cb: FFICallBack, user_data: *const c_void, ) -> *mut c_void; - pub fn waku_version(ctx: *const c_void, cb: WakuCallback, user_data: *const c_void) -> c_int; + pub fn waku_version(ctx: *const c_void, cb: FFICallBack, user_data: *const c_void) -> c_int; - pub fn waku_start(ctx: *const c_void, cb: WakuCallback, user_data: *const c_void) -> c_int; + pub fn waku_start(ctx: *const c_void, cb: FFICallBack, user_data: *const c_void) -> c_int; pub fn waku_default_pubsub_topic( ctx: *mut c_void, - cb: WakuCallback, + cb: FFICallBack, user_data: *const c_void, ) -> *mut c_void; } @@ -40,7 +40,7 @@ pub unsafe extern "C" fn trampoline( closure(return_val, &buffer_utf8); } -pub fn get_trampoline(_closure: &C) -> WakuCallback +pub fn get_trampoline(_closure: &C) -> FFICallBack where C: FnMut(i32, &str), { diff --git a/examples/subscriber.nim b/examples/subscriber.nim index fb040b05a..ce64bb803 100644 --- a/examples/subscriber.nim +++ b/examples/subscriber.nim @@ -47,13 +47,9 @@ proc setupAndSubscribe(rng: ref HmacDrbgContext) {.async.} = var enrBuilder = EnrBuilder.init(nodeKey) - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create enr record", error = recordRes.error - quit(QuitFailure) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + error "failed to create enr record", error = error + quit(QuitFailure) var builder = WakuNodeBuilder.init() builder.withNodeKey(nodeKey) diff --git a/examples/waku_example.nim b/examples/waku_example.nim deleted file mode 100644 index ebac0b466..000000000 --- a/examples/waku_example.nim +++ /dev/null @@ -1,40 +0,0 @@ -import std/options -import chronos, results, confutils, confutils/defs -import waku - -type CliArgs = object - ethRpcEndpoint* {. - defaultValue: "", desc: "ETH RPC Endpoint, if passed, RLN is enabled" - .}: string - -when isMainModule: - let args = CliArgs.load() - - echo "Starting Waku node..." - - let config = - if (args.ethRpcEndpoint == ""): - # Create a basic configuration for the Waku node - # No RLN as we don't have an ETH RPC Endpoint - NodeConfig.init( - protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 42) - ) - else: - # Connect to TWN, use ETH RPC Endpoint for RLN - NodeConfig.init(ethRpcEndpoints = @[args.ethRpcEndpoint]) - - # Create the node using the library API's createNode function - let node = (waitFor createNode(config)).valueOr: - echo "Failed to create node: ", error - quit(QuitFailure) - - echo("Waku node created successfully!") - - # Start the node - (waitFor startWaku(addr node)).isOkOr: - echo "Failed to start node: ", error - quit(QuitFailure) - - echo "Node started successfully!" - - runForever() diff --git a/examples/wakustealthcommitments/node_spec.nim b/examples/wakustealthcommitments/node_spec.nim index b935f9ab1..d85e83a5b 100644 --- a/examples/wakustealthcommitments/node_spec.nim +++ b/examples/wakustealthcommitments/node_spec.nim @@ -1,6 +1,6 @@ {.push raises: [].} -import ../../apps/wakunode2/cli_args +import tools/confutils/cli_args import waku/[common/logging, factory/[waku, networks_config]] import std/[options, strutils, os, sequtils], @@ -18,13 +18,10 @@ proc setup*(): Waku = const versionString = "version / git commit hash: " & waku.git_version let rng = crypto.newRng() - let confRes = WakuNodeConf.load(version = versionString) - if confRes.isErr(): - error "failure while loading the configuration", error = $confRes.error + let conf = WakuNodeConf.load(version = versionString).valueOr: + error "failure while loading the configuration", error = $error quit(QuitFailure) - var conf = confRes.get() - let twnNetworkConf = NetworkConf.TheWakuNetworkConf() if len(conf.shards) != 0: conf.pubsubTopics = conf.shards.mapIt(twnNetworkConf.pubsubTopics[it.uint16]) diff --git a/examples/wakustealthcommitments/stealth_commitment_protocol.nim b/examples/wakustealthcommitments/stealth_commitment_protocol.nim index 9a2045a67..63311bf7b 100644 --- a/examples/wakustealthcommitments/stealth_commitment_protocol.nim +++ b/examples/wakustealthcommitments/stealth_commitment_protocol.nim @@ -95,61 +95,54 @@ proc sendResponse*( type SCPHandler* = proc(msg: WakuMessage): Future[void] {.async.} proc getSCPHandler(self: StealthCommitmentProtocol): SCPHandler = let handler = proc(msg: WakuMessage): Future[void] {.async.} = - let decodedRes = WakuStealthCommitmentMsg.decode(msg.payload) - if decodedRes.isErr(): - error "could not decode scp message" - let decoded = decodedRes.get() + let decoded = WakuStealthCommitmentMsg.decode(msg.payload).valueOr: + error "could not decode scp message", error = error + quit(QuitFailure) if decoded.request == false: # check if the generated stealth commitment belongs to the receiver # if not, continue - let ephemeralPubKeyRes = - deserialize(StealthCommitmentFFI.PublicKey, decoded.ephemeralPubKey.get()) - if ephemeralPubKeyRes.isErr(): - error "could not deserialize ephemeral public key: ", - err = ephemeralPubKeyRes.error() - let ephemeralPubKey = ephemeralPubKeyRes.get() - let stealthCommitmentPrivateKeyRes = StealthCommitmentFFI.generateStealthPrivateKey( + let ephemeralPubKey = deserialize( + StealthCommitmentFFI.PublicKey, decoded.ephemeralPubKey.get() + ).valueOr: + error "could not deserialize ephemeral public key: ", error = error + quit(QuitFailure) + let stealthCommitmentPrivateKey = StealthCommitmentFFI.generateStealthPrivateKey( ephemeralPubKey, self.spendingKeyPair.privateKey, self.viewingKeyPair.privateKey, decoded.viewTag.get(), - ) - if stealthCommitmentPrivateKeyRes.isErr(): - info "received stealth commitment does not belong to the receiver: ", - err = stealthCommitmentPrivateKeyRes.error() - - let stealthCommitmentPrivateKey = stealthCommitmentPrivateKeyRes.get() + ).valueOr: + error "received stealth commitment does not belong to the receiver: ", + error = error + quit(QuitFailure) info "received stealth commitment belongs to the receiver: ", stealthCommitmentPrivateKey, stealthCommitmentPubKey = decoded.stealthCommitment.get() return # send response # deseralize the keys - let spendingKeyRes = - deserialize(StealthCommitmentFFI.PublicKey, decoded.spendingPubKey.get()) - if spendingKeyRes.isErr(): - error "could not deserialize spending key: ", err = spendingKeyRes.error() - let spendingKey = spendingKeyRes.get() - let viewingKeyRes = - (deserialize(StealthCommitmentFFI.PublicKey, decoded.viewingPubKey.get())) - if viewingKeyRes.isErr(): - error "could not deserialize viewing key: ", err = viewingKeyRes.error() - let viewingKey = viewingKeyRes.get() + let spendingKey = deserialize( + StealthCommitmentFFI.PublicKey, decoded.spendingPubKey.get() + ).valueOr: + error "could not deserialize spending key: ", error = error + quit(QuitFailure) + let viewingKey = ( + deserialize(StealthCommitmentFFI.PublicKey, decoded.viewingPubKey.get()) + ).valueOr: + error "could not deserialize viewing key: ", error = error + quit(QuitFailure) info "received spending key", spendingKey info "received viewing key", viewingKey - let ephemeralKeyPairRes = StealthCommitmentFFI.generateKeyPair() - if ephemeralKeyPairRes.isErr(): - error "could not generate ephemeral key pair: ", err = ephemeralKeyPairRes.error() - let ephemeralKeyPair = ephemeralKeyPairRes.get() + let ephemeralKeyPair = StealthCommitmentFFI.generateKeyPair().valueOr: + error "could not generate ephemeral key pair: ", error = error + quit(QuitFailure) - let stealthCommitmentRes = StealthCommitmentFFI.generateStealthCommitment( + let stealthCommitment = StealthCommitmentFFI.generateStealthCommitment( spendingKey, viewingKey, ephemeralKeyPair.privateKey - ) - if stealthCommitmentRes.isErr(): - error "could not generate stealth commitment: ", - err = stealthCommitmentRes.error() - let stealthCommitment = stealthCommitmentRes.get() + ).valueOr: + error "could not generate stealth commitment: ", error = error + quit(QuitFailure) ( await self.sendResponse( @@ -157,7 +150,7 @@ proc getSCPHandler(self: StealthCommitmentProtocol): SCPHandler = stealthCommitment.viewTag, ) ).isOkOr: - error "could not send response: ", err = $error + error "could not send response: ", error = $error return handler diff --git a/flake.lock b/flake.lock index 359ae2579..8d0db9269 100644 --- a/flake.lock +++ b/flake.lock @@ -2,44 +2,85 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1740603184, - "narHash": "sha256-t+VaahjQAWyA+Ctn2idyo1yxRIYpaDxMgHkgCNiMJa4=", + "lastModified": 1770464364, + "narHash": "sha256-z5NJPSBwsLf/OfD8WTmh79tlSU8XgIbwmk6qB1/TFzY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49", + "rev": "23d72dabcb3b12469f57b37170fcbc1789bd7457", "type": "github" }, "original": { "owner": "NixOS", "repo": "nixpkgs", - "rev": "f44bd8ca21e026135061a0a57dcf3d0775b67a49", + "rev": "23d72dabcb3b12469f57b37170fcbc1789bd7457", "type": "github" } }, "root": { "inputs": { "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay", "zerokit": "zerokit" } }, - "zerokit": { + "rust-overlay": { "inputs": { "nixpkgs": [ "nixpkgs" ] }, "locked": { - "lastModified": 1743756626, - "narHash": "sha256-SvhfEl0bJcRsCd79jYvZbxQecGV2aT+TXjJ57WVv7Aw=", + "lastModified": 1775099554, + "narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "rust-overlay_2": { + "inputs": { + "nixpkgs": [ + "zerokit", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1771211437, + "narHash": "sha256-lcNK438i4DGtyA+bPXXyVLHVmJjYpVKmpux9WASa3ro=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "c62195b3d6e1bb11e0c2fb2a494117d3b55d410f", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "zerokit": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-overlay": "rust-overlay_2" + }, + "locked": { "owner": "vacp2p", "repo": "zerokit", - "rev": "c60e0c33fc6350a4b1c20e6b6727c44317129582", + "rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63", "type": "github" }, "original": { "owner": "vacp2p", "repo": "zerokit", - "rev": "c60e0c33fc6350a4b1c20e6b6727c44317129582", + "rev": "5e64cb8822bee65eed6cf459f95ae72b80c6ba63", "type": "github" } } diff --git a/flake.nix b/flake.nix index 760f49337..077668b9a 100644 --- a/flake.nix +++ b/flake.nix @@ -1,64 +1,116 @@ { - description = "NWaku build flake"; + description = "logos-delivery nim build flake"; nixConfig = { extra-substituters = [ "https://nix-cache.status.im/" ]; - extra-trusted-public-keys = [ "nix-cache.status.im-1:x/93lOfLU+duPplwMSBR+OlY4+mo+dCN7n0mr4oPwgY=" ]; + extra-trusted-public-keys = [ + "nix-cache.status.im-1:x/93lOfLU+duPplwMSBR+OlY4+mo+dCN7n0mr4oPwgY=" + ]; }; inputs = { - nixpkgs.url = "github:NixOS/nixpkgs?rev=f44bd8ca21e026135061a0a57dcf3d0775b67a49"; + # Pinning the commit to use same commit across different projects. + # A commit from nixpkgs 25.11 release: https://github.com/NixOS/nixpkgs/tree/release-25.11 + nixpkgs.url = "github:NixOS/nixpkgs?rev=23d72dabcb3b12469f57b37170fcbc1789bd7457"; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + # External flake input: Zerokit pinned to a specific commit. + # Update the rev here when a new zerokit version is needed. zerokit = { - url = "github:vacp2p/zerokit?rev=c60e0c33fc6350a4b1c20e6b6727c44317129582"; + # Pinned to v2.0.2 (5e64cb8822bee65eed6cf459f95ae72b80c6ba63) to match + # the vendor/zerokit submodule. Keep these two in sync: the nix build + # links librln from this input, the Makefile build from the submodule. + url = "github:vacp2p/zerokit/5e64cb8822bee65eed6cf459f95ae72b80c6ba63"; inputs.nixpkgs.follows = "nixpkgs"; }; }; - outputs = { self, nixpkgs, zerokit }: + outputs = { self, nixpkgs, rust-overlay, zerokit }: let - stableSystems = [ + systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" - "x86_64-windows" "i686-linux" - "i686-windows" + "x86_64-windows" ]; - forAllSystems = f: nixpkgs.lib.genAttrs stableSystems (system: f system); + forAllSystems = nixpkgs.lib.genAttrs systems; - pkgsFor = forAllSystems ( - system: import nixpkgs { - inherit system; - config = { - android_sdk.accept_license = true; - allowUnfree = true; + lib = nixpkgs.lib; + + # Single source of truth for the semver: the `version` field of + # waku.nimble. Kept in sync with git tags by the version-check CI. + nimbleVersion = + let line = lib.findFirst (l: lib.hasPrefix "version = " l) + "version = \"unknown\"" + (lib.splitString "\n" (builtins.readFile ./waku.nimble)); + in lib.removeSuffix "\"" (lib.removePrefix "version = \"" line); + + # A flake sandbox has no .git, so `git describe` is impossible; the + # commit comes from the flake metadata instead. + shortRev = self.shortRev or self.dirtyShortRev or "dirty"; + + nimbleOverlay = final: prev: { + nimble = prev.nimble.overrideAttrs (_: { + version = "0.22.3"; + src = prev.fetchFromGitHub { + owner = "nim-lang"; + repo = "nimble"; + rev = "v0.22.3"; + sha256 = "sha256-f7DYpRGVUeSi6basK1lfu5AxZpMFOSJ3oYsy+urYErg="; }; - overlays = [ - (final: prev: { - androidEnvCustom = prev.callPackage ./nix/pkgs/android-sdk { }; - androidPkgs = final.androidEnvCustom.pkgs; - androidShell = final.androidEnvCustom.shell; - }) - ]; + }); + }; + + pkgsFor = system: import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) nimbleOverlay ]; + }; + in { + packages = forAllSystems (system: + let + pkgs = pkgsFor system; + + # HACK: Fix for stale cargoHash in 2.0.2 release. + zerokitRln = zerokit.packages.${system}.rln.overrideAttrs (old: { + cargoDeps = old.cargoDeps.overrideAttrs (oldCargoDeps: { + vendorStaging = oldCargoDeps.vendorStaging.overrideAttrs (_: { + outputHash = "sha256-PNwEdZLgGQPqQDrEK2hsQtSybVfBbD6xn4K47fPFJUU="; + }); + }); + }); + + liblogosdelivery = pkgs.callPackage ./nix/default.nix { + inherit pkgs; + src = ./.; + inherit zerokitRln; + gitVersion = "v${nimbleVersion}-g${builtins.substring 0 6 shortRev}"; + }; + in { + inherit liblogosdelivery; + # Expose the cargoHash-corrected librln so downstream consumers + # (e.g. logos-delivery-module) bundle the exact same librln this + # build links, instead of pulling zerokit's rln directly — whose + # committed cargoHash is stale for v2.0.2 (see zerokitRln above). + rln = zerokitRln; + default = liblogosdelivery; } ); - in rec { - packages = forAllSystems (system: let - pkgs = pkgsFor.${system}; - in rec { - libwaku-android-arm64 = pkgs.callPackage ./nix/default.nix { - inherit stableSystems; - src = self; - targets = ["libwaku-android-arm64"]; - androidArch = "aarch64-linux-android"; - abidir = "arm64-v8a"; - zerokitPkg = zerokit.packages.${system}.zerokit-android-arm64; - }; - default = libwaku-android-arm64; - }); - - devShells = forAllSystems (system: { - default = pkgsFor.${system}.callPackage ./nix/shell.nix {}; - }); + devShells = forAllSystems (system: + let + pkgs = pkgsFor system; + in { + default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + nim-2_2 + nimble + ]; + }; + } + ); }; -} \ No newline at end of file +} diff --git a/liblogosdelivery/BUILD.md b/liblogosdelivery/BUILD.md new file mode 100644 index 000000000..011fbb438 --- /dev/null +++ b/liblogosdelivery/BUILD.md @@ -0,0 +1,123 @@ +# Building liblogosdelivery and Examples + +## Prerequisites + +- Nim 2.x compiler +- Rust toolchain (for RLN dependencies) +- GCC or Clang compiler +- Make + +## Building the Library + +### Dynamic Library + +```bash +make liblogosdelivery +``` + +This creates `build/liblogosdelivery.dylib` (macOS) or `build/liblogosdelivery.so` (Linux). + +### Static Library + +```bash +nim liblogosdelivery STATIC=1 +``` + +This creates `build/liblogosdelivery.a`. + +## Building Examples + +### liblogosdelivery Example + +Compile the C example that demonstrates all library features: + +```bash +# Using Make (recommended) +make liblogosdelivery_example + +## Running Examples + +```bash +./build/liblogosdelivery_example +``` + +The example will: +1. Create a Logos Messaging node +2. Register event callbacks for message events +3. Start the node +4. Subscribe to a content topic +5. Send a message +6. Show message delivery events (sent, propagated, or error) +7. Unsubscribe and cleanup + +## Build Artifacts + +After building, you'll have: + +``` +build/ +├── liblogosdelivery.dylib # Dynamic library (34MB) +├── liblogosdelivery.dylib.dSYM/ # Debug symbols +└── liblogosdelivery_example # Compiled example (34KB) +``` + +## Library Headers + +The main header file is: +- `liblogosdelivery/liblogosdelivery.h` - C API declarations + +## Troubleshooting + +### Library not found at runtime + +If you get "library not found" errors when running the example: + +**macOS:** +```bash +export DYLD_LIBRARY_PATH=/path/to/build:$DYLD_LIBRARY_PATH +./build/liblogosdelivery_example +``` + +**Linux:** +```bash +export LD_LIBRARY_PATH=/path/to/build:$LD_LIBRARY_PATH +./build/liblogosdelivery_example +``` +## Cross-Compilation + +For cross-compilation, you need to: +1. Build the Nim library for the target platform +2. Use the appropriate cross-compiler +3. Link against the target platform's liblogosdelivery + +Example for Linux from macOS: +```bash +# Build library for Linux (requires Docker or cross-compilation setup) +# Then compile with cross-compiler +``` + +## Integration with Your Project + +### CMake + +```cmake +find_library(LMAPI_LIBRARY NAMES lmapi PATHS ${PROJECT_SOURCE_DIR}/build) +include_directories(${PROJECT_SOURCE_DIR}/liblogosdelivery) +target_link_libraries(your_target ${LMAPI_LIBRARY}) +``` + +### Makefile + +```makefile +CFLAGS += -I/path/to/liblogosdelivery +LDFLAGS += -L/path/to/build -llmapi -Wl,-rpath,/path/to/build + +your_program: your_program.c + $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +``` + +## API Documentation + +See: +- [liblogosdelivery.h](liblogosdelivery/liblogosdelivery.h) - API function declarations +- [MESSAGE_EVENTS.md](liblogosdelivery/MESSAGE_EVENTS.md) - Message event handling guide diff --git a/liblogosdelivery/MESSAGE_EVENTS.md b/liblogosdelivery/MESSAGE_EVENTS.md new file mode 100644 index 000000000..60740fb62 --- /dev/null +++ b/liblogosdelivery/MESSAGE_EVENTS.md @@ -0,0 +1,148 @@ +# Message Event Handling in LMAPI + +## Overview + +The liblogosdelivery library emits three types of message delivery events that clients can listen to by registering an event callback using `logosdelivery_set_event_callback()`. + +## Event Types + +### 1. message_sent +Emitted when a message is successfully accepted by the send service and queued for delivery. + +**JSON Structure:** +```json +{ + "eventType": "message_sent", + "requestId": "unique-request-id", + "messageHash": "0x..." +} +``` + +**Fields:** +- `eventType`: Always "message_sent" +- `requestId`: Request ID returned from the send operation +- `messageHash`: Hash of the message that was sent + +### 2. message_propagated +Emitted when a message has been successfully propagated to neighboring nodes on the network. + +**JSON Structure:** +```json +{ + "eventType": "message_propagated", + "requestId": "unique-request-id", + "messageHash": "0x..." +} +``` + +**Fields:** +- `eventType`: Always "message_propagated" +- `requestId`: Request ID from the send operation +- `messageHash`: Hash of the message that was propagated + +### 3. message_error +Emitted when an error occurs during message sending or propagation. + +**JSON Structure:** +```json +{ + "eventType": "message_error", + "requestId": "unique-request-id", + "messageHash": "0x...", + "error": "error description" +} +``` + +**Fields:** +- `eventType`: Always "message_error" +- `requestId`: Request ID from the send operation +- `messageHash`: Hash of the message that failed +- `error`: Description of what went wrong + +## Usage + +### 1. Define an Event Callback + +```c +void event_callback(int ret, const char *msg, size_t len, void *userData) { + if (ret != RET_OK || msg == NULL || len == 0) { + return; + } + + // Parse the JSON message + // Extract eventType field + // Handle based on event type + + if (eventType == "message_sent") { + // Handle message sent + } else if (eventType == "message_propagated") { + // Handle message propagated + } else if (eventType == "message_error") { + // Handle message error + } +} +``` + +### 2. Register the Callback + +```c +void *ctx = logosdelivery_create_node(config, callback, userData); +logosdelivery_set_event_callback(ctx, event_callback, NULL); +``` + +### 3. Start the Node + +Once the node is started, events will be delivered to your callback: + +```c +logosdelivery_start_node(ctx, callback, userData); +``` + +## Event Flow + +For a typical successful message send: + +1. **send** → Returns request ID +2. **message_sent** → Message accepted and queued +3. **message_propagated** → Message delivered to peers + +For a failed message send: + +1. **send** → Returns request ID +2. **message_sent** → Message accepted and queued +3. **message_error** → Delivery failed with error description + +## Important Notes + +1. **Thread Safety**: The event callback is invoked from the FFI worker thread. Ensure your callback is thread-safe if it accesses shared state. + +2. **Non-Blocking**: Keep the callback fast and non-blocking. Do not perform long-running operations in the callback. + +3. **JSON Parsing**: The example uses a simple string-based parser. For production, use a proper JSON library like: + - [cJSON](https://github.com/DaveGamble/cJSON) + - [json-c](https://github.com/json-c/json-c) + - [Jansson](https://github.com/akheron/jansson) + +4. **Memory Management**: The message buffer is owned by the library. Copy any data you need to retain. + +5. **Event Order**: Events are delivered in the order they occur, but timing depends on network conditions. + +## Example Implementation + +See `examples/liblogosdelivery_example.c` for a complete working example that: +- Registers an event callback +- Sends a message +- Receives and prints all three event types +- Properly parses the JSON event structure + +## Debugging Events + +To see all events during development: + +```c +void debug_event_callback(int ret, const char *msg, size_t len, void *userData) { + printf("Event received: %.*s\n", (int)len, msg); +} +``` + +This will print the raw JSON for all events, helping you understand the event structure. diff --git a/liblogosdelivery/README.md b/liblogosdelivery/README.md new file mode 100644 index 000000000..e8352c611 --- /dev/null +++ b/liblogosdelivery/README.md @@ -0,0 +1,262 @@ +# Logos Messaging API (LMAPI) Library + +A C FFI library providing a simplified interface to Logos Messaging functionality. + +## Overview + +This library wraps the high-level API functions from `waku/api/api.nim` and exposes them via a C FFI interface, making them accessible from C, C++, and other languages that support C FFI. + +## API Functions + +### Node Lifecycle + +#### `logosdelivery_create_node` +Creates a new instance of the node from the given configuration JSON. + +```c +void *logosdelivery_create_node( + const char *configJson, + FFICallBack callback, + void *userData +); +``` + +**Parameters:** +- `configJson`: JSON string containing node configuration +- `callback`: Callback function to receive the result +- `userData`: User data passed to the callback + +**Returns:** Pointer to the context needed by other API functions, or NULL on error. + +**Example configuration JSON:** +```json +{ + "mode": "Core", + "preset": "logos.dev", + "listenAddress": "0.0.0.0", + "tcpPort": 60000, + "discv5UdpPort": 9000 +} +``` + +Configuration uses flat field names matching `WakuNodeConf` in `tools/confutils/cli_args.nim`. +Use `"preset"` to select a network preset (e.g., `"twn"`, `"logos.dev"`) which auto-configures +entry nodes, cluster ID, sharding, and other network-specific settings. + +#### `logosdelivery_start_node` +Starts the node. + +```c +int logosdelivery_start_node( + void *ctx, + FFICallBack callback, + void *userData +); +``` + +#### `logosdelivery_stop_node` +Stops the node. + +```c +int logosdelivery_stop_node( + void *ctx, + FFICallBack callback, + void *userData +); +``` + +#### `logosdelivery_destroy` +Destroys a node instance and frees resources. + +```c +int logosdelivery_destroy( + void *ctx, + FFICallBack callback, + void *userData +); +``` + +### Messaging + +#### `logosdelivery_subscribe` +Subscribe to a content topic to receive messages. + +```c +int logosdelivery_subscribe( + void *ctx, + FFICallBack callback, + void *userData, + const char *contentTopic +); +``` + +**Parameters:** +- `ctx`: Context pointer from `logosdelivery_create_node` +- `callback`: Callback function to receive the result +- `userData`: User data passed to the callback +- `contentTopic`: Content topic string (e.g., "/myapp/1/chat/proto") + +#### `logosdelivery_unsubscribe` +Unsubscribe from a content topic. + +```c +int logosdelivery_unsubscribe( + void *ctx, + FFICallBack callback, + void *userData, + const char *contentTopic +); +``` + +#### `logosdelivery_send` +Send a message. + +```c +int logosdelivery_send( + void *ctx, + FFICallBack callback, + void *userData, + const char *messageJson +); +``` + +**Parameters:** +- `messageJson`: JSON string containing the message + +**Example message JSON:** +```json +{ + "contentTopic": "/myapp/1/chat/proto", + "payload": "SGVsbG8gV29ybGQ=", + "ephemeral": false +} +``` + +Note: The `payload` field should be base64-encoded. + +**Returns:** Request ID in the callback message that can be used to track message delivery. + +### Events + +#### `logosdelivery_set_event_callback` +Sets a callback that will be invoked whenever an event occurs (e.g., message received). + +```c +void logosdelivery_set_event_callback( + void *ctx, + FFICallBack callback, + void *userData +); +``` + +**Important:** The callback should be fast, non-blocking, and thread-safe. + +## Building + +The library follows the same build system as the main Logos Messaging project. + +### Build the library + +```bash +make liblogosdeliveryStatic # Build static library +# or +make liblogosdeliveryDynamic # Build dynamic library +``` + +## Return Codes + +All functions that return `int` use the following return codes: + +- `RET_OK` (0): Success +- `RET_ERR` (1): Error +- `RET_MISSING_CALLBACK` (2): Missing callback function + +## Callback Function + +All API functions use the following callback signature: + +```c +typedef void (*FFICallBack)( + int callerRet, + const char *msg, + size_t len, + void *userData +); +``` + +**Parameters:** +- `callerRet`: Return code (RET_OK, RET_ERR, etc.) +- `msg`: Response message (may be empty for success) +- `len`: Length of the message +- `userData`: User data passed in the original call + +## Example Usage + +```c +#include "liblogosdelivery.h" +#include + +void callback(int ret, const char *msg, size_t len, void *userData) { + if (ret == RET_OK) { + printf("Success: %.*s\n", (int)len, msg); + } else { + printf("Error: %.*s\n", (int)len, msg); + } +} + +int main() { + const char *config = "{" + "\"logLevel\": \"INFO\"," + "\"mode\": \"Core\"," + "\"preset\": \"logos.dev\"" + "}"; + + // Create node + void *ctx = logosdelivery_create_node(config, callback, NULL); + if (ctx == NULL) { + return 1; + } + + // Start node + logosdelivery_start_node(ctx, callback, NULL); + + // Subscribe to a topic + logosdelivery_subscribe(ctx, callback, NULL, "/myapp/1/chat/proto"); + + // Send a message + const char *msg = "{" + "\"contentTopic\": \"/myapp/1/chat/proto\"," + "\"payload\": \"SGVsbG8gV29ybGQ=\"," + "\"ephemeral\": false" + "}"; + logosdelivery_send(ctx, callback, NULL, msg); + + // Clean up + logosdelivery_stop_node(ctx, callback, NULL); + logosdelivery_destroy(ctx, callback, NULL); + + return 0; +} +``` + +## Architecture + +The library is structured as follows: + +- `liblogosdelivery.h`: C header file with function declarations +- `liblogosdelivery.nim`: Main library entry point +- `declare_lib.nim`: Library declaration and initialization +- `lmapi/node_api.nim`: Node lifecycle API implementation +- `lmapi/messaging_api.nim`: Subscribe/send API implementation + +The library uses the nim-ffi framework for FFI infrastructure, which handles: +- Thread-safe request processing +- Async operation management +- Memory management between C and Nim +- Callback marshaling + +## See Also + +- Main API documentation: `waku/api/api.nim` +- Original libwaku library: `library/libwaku.nim` +- nim-ffi framework: `vendor/nim-ffi/` diff --git a/liblogosdelivery/declare_lib.nim b/liblogosdelivery/declare_lib.nim new file mode 100644 index 000000000..5087a0dee --- /dev/null +++ b/liblogosdelivery/declare_lib.nim @@ -0,0 +1,33 @@ +import ffi +import std/locks +import waku/factory/waku + +declareLibrary("logosdelivery") + +var eventCallbackLock: Lock +initLock(eventCallbackLock) + +template requireInitializedNode*( + ctx: ptr FFIContext[Waku], opName: string, onError: untyped +) = + if isNil(ctx): + let errMsg {.inject.} = opName & " failed: invalid context" + onError + elif isNil(ctx.myLib) or isNil(ctx.myLib[]): + let errMsg {.inject.} = opName & " failed: node is not initialized" + onError + +proc logosdelivery_set_event_callback( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.dynlib, exportc, cdecl.} = + if isNil(ctx): + echo "error: invalid context in logosdelivery_set_event_callback" + return + + # prevent race conditions that might happen due incorrect usage. + eventCallbackLock.acquire() + defer: + eventCallbackLock.release() + + ctx[].eventCallback = cast[pointer](callback) + ctx[].eventUserData = userData diff --git a/liblogosdelivery/examples/json_utils.c b/liblogosdelivery/examples/json_utils.c new file mode 100644 index 000000000..8b33bb648 --- /dev/null +++ b/liblogosdelivery/examples/json_utils.c @@ -0,0 +1,96 @@ +#include "json_utils.h" +#include +#include + +const char* extract_json_field(const char *json, const char *field, char *buffer, size_t bufSize) { + char searchStr[256]; + snprintf(searchStr, sizeof(searchStr), "\"%s\":\"", field); + + const char *start = strstr(json, searchStr); + if (!start) { + return NULL; + } + + start += strlen(searchStr); + const char *end = strchr(start, '"'); + if (!end) { + return NULL; + } + + size_t len = end - start; + if (len >= bufSize) { + len = bufSize - 1; + } + + memcpy(buffer, start, len); + buffer[len] = '\0'; + + return buffer; +} + +const char* extract_json_object(const char *json, const char *field, size_t *outLen) { + char searchStr[256]; + snprintf(searchStr, sizeof(searchStr), "\"%s\":{", field); + + const char *start = strstr(json, searchStr); + if (!start) { + return NULL; + } + + // Advance to the opening brace + start = strchr(start, '{'); + if (!start) { + return NULL; + } + + // Find the matching closing brace (handles nested braces) + int depth = 0; + const char *p = start; + while (*p) { + if (*p == '{') depth++; + else if (*p == '}') { + depth--; + if (depth == 0) { + *outLen = (size_t)(p - start + 1); + return start; + } + } + p++; + } + return NULL; +} + +int decode_json_byte_array(const char *json, const char *field, char *buffer, size_t bufSize) { + char searchStr[256]; + snprintf(searchStr, sizeof(searchStr), "\"%s\":[", field); + + const char *start = strstr(json, searchStr); + if (!start) { + return -1; + } + + // Advance to the opening bracket + start = strchr(start, '['); + if (!start) { + return -1; + } + start++; // skip '[' + + size_t pos = 0; + const char *p = start; + while (*p && *p != ']' && pos < bufSize - 1) { + // Skip whitespace and commas + while (*p == ' ' || *p == ',' || *p == '\n' || *p == '\r' || *p == '\t') p++; + if (*p == ']') break; + + // Parse integer + int val = 0; + while (*p >= '0' && *p <= '9') { + val = val * 10 + (*p - '0'); + p++; + } + buffer[pos++] = (char)val; + } + buffer[pos] = '\0'; + return (int)pos; +} diff --git a/liblogosdelivery/examples/json_utils.h b/liblogosdelivery/examples/json_utils.h new file mode 100644 index 000000000..4039ca4f6 --- /dev/null +++ b/liblogosdelivery/examples/json_utils.h @@ -0,0 +1,21 @@ +#ifndef JSON_UTILS_H +#define JSON_UTILS_H + +#include + +// Extract a JSON string field value into buffer. +// Returns pointer to buffer on success, NULL on failure. +// Very basic parser - for production use a proper JSON library. +const char* extract_json_field(const char *json, const char *field, char *buffer, size_t bufSize); + +// Extract a nested JSON object as a raw string. +// Returns a pointer into `json` at the start of the object, and sets `outLen`. +// Handles nested braces. +const char* extract_json_object(const char *json, const char *field, size_t *outLen); + +// Decode a JSON array of integers (byte values) into a buffer. +// Parses e.g. [72,101,108,108,111] into "Hello". +// Returns number of bytes decoded, or -1 on error. +int decode_json_byte_array(const char *json, const char *field, char *buffer, size_t bufSize); + +#endif // JSON_UTILS_H diff --git a/liblogosdelivery/examples/logosdelivery_example.c b/liblogosdelivery/examples/logosdelivery_example.c new file mode 100644 index 000000000..729f7f0dc --- /dev/null +++ b/liblogosdelivery/examples/logosdelivery_example.c @@ -0,0 +1,227 @@ +#include "../liblogosdelivery.h" +#include "json_utils.h" +#include +#include +#include +#include + +static int create_node_ok = -1; + +// Flags set by event callback, polled by main thread +static volatile int got_message_sent = 0; +static volatile int got_message_error = 0; +static volatile int got_message_received = 0; + +// Event callback that handles message events +void event_callback(int ret, const char *msg, size_t len, void *userData) { + if (ret != RET_OK || msg == NULL || len == 0) { + return; + } + + // Create null-terminated string for easier parsing + char *eventJson = malloc(len + 1); + if (!eventJson) { + return; + } + memcpy(eventJson, msg, len); + eventJson[len] = '\0'; + + // Extract eventType + char eventType[64]; + if (!extract_json_field(eventJson, "eventType", eventType, sizeof(eventType))) { + free(eventJson); + return; + } + + // Handle different event types + if (strcmp(eventType, "message_sent") == 0) { + char requestId[128]; + char messageHash[128]; + extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); + extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); + printf("[EVENT] Message sent - RequestID: %s, Hash: %s\n", requestId, messageHash); + got_message_sent = 1; + + } else if (strcmp(eventType, "message_error") == 0) { + char requestId[128]; + char messageHash[128]; + char error[256]; + extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); + extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); + extract_json_field(eventJson, "error", error, sizeof(error)); + printf("[EVENT] Message error - RequestID: %s, Hash: %s, Error: %s\n", + requestId, messageHash, error); + got_message_error = 1; + + } else if (strcmp(eventType, "message_propagated") == 0) { + char requestId[128]; + char messageHash[128]; + extract_json_field(eventJson, "requestId", requestId, sizeof(requestId)); + extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); + printf("[EVENT] Message propagated - RequestID: %s, Hash: %s\n", requestId, messageHash); + + } else if (strcmp(eventType, "connection_status_change") == 0) { + char connectionStatus[256]; + extract_json_field(eventJson, "connectionStatus", connectionStatus, sizeof(connectionStatus)); + printf("[EVENT] Connection status change - Status: %s\n", connectionStatus); + + } else if (strcmp(eventType, "message_received") == 0) { + char messageHash[128]; + extract_json_field(eventJson, "messageHash", messageHash, sizeof(messageHash)); + + // Extract the nested "message" object + size_t msgObjLen = 0; + const char *msgObj = extract_json_object(eventJson, "message", &msgObjLen); + if (msgObj) { + // Make a null-terminated copy of the message object + char *msgJson = malloc(msgObjLen + 1); + if (msgJson) { + memcpy(msgJson, msgObj, msgObjLen); + msgJson[msgObjLen] = '\0'; + + char contentTopic[256]; + extract_json_field(msgJson, "contentTopic", contentTopic, sizeof(contentTopic)); + + // Decode payload from JSON byte array to string + char payload[4096]; + int payloadLen = decode_json_byte_array(msgJson, "payload", payload, sizeof(payload)); + + printf("[EVENT] Message received - Hash: %s, ContentTopic: %s\n", messageHash, contentTopic); + if (payloadLen > 0) { + printf(" Payload (%d bytes): %.*s\n", payloadLen, payloadLen, payload); + } else { + printf(" Payload: (empty or could not decode)\n"); + } + + free(msgJson); + } + } else { + printf("[EVENT] Message received - Hash: %s (could not parse message)\n", messageHash); + } + got_message_received = 1; + + } else { + printf("[EVENT] Unknown event type: %s\n", eventType); + } + + free(eventJson); +} + +// Simple callback that prints results +void simple_callback(int ret, const char *msg, size_t len, void *userData) { + const char *operation = (const char *)userData; + + if (operation != NULL && strcmp(operation, "create_node") == 0) { + create_node_ok = (ret == RET_OK) ? 1 : 0; + } + + if (ret == RET_OK) { + if (len > 0) { + printf("[%s] Success: %.*s\n", operation, (int)len, msg); + } else { + printf("[%s] Success\n", operation); + } + } else { + printf("[%s] Error: %.*s\n", operation, (int)len, msg); + } +} + +int main() { + printf("=== Logos Messaging API (LMAPI) Example ===\n\n"); + + // Configuration JSON using WakuNodeConf field names (flat structure). + // Field names match Nim identifiers from WakuNodeConf in tools/confutils/cli_args.nim. + const char *config = "{" + "\"logLevel\": \"INFO\"," + "\"mode\": \"Core\"," + "\"preset\": \"logos.dev\"" + "}"; + + printf("1. Creating node...\n"); + void *ctx = logosdelivery_create_node(config, simple_callback, (void *)"create_node"); + if (ctx == NULL) { + printf("Failed to create node\n"); + return 1; + } + + // Wait a bit for the callback + sleep(1); + + if (create_node_ok != 1) { + printf("Create node failed, stopping example early.\n"); + logosdelivery_destroy(ctx, simple_callback, (void *)"destroy"); + return 1; + } + + printf("\n2. Setting up event callback...\n"); + logosdelivery_set_event_callback(ctx, event_callback, NULL); + printf("Event callback registered for message events\n"); + + printf("\n3. Starting node...\n"); + logosdelivery_start_node(ctx, simple_callback, (void *)"start_node"); + + // Wait for node to start + sleep(5); + + printf("\n4. Subscribing to content topic...\n"); + const char *contentTopic = "/example/1/chat/proto"; + logosdelivery_subscribe(ctx, simple_callback, (void *)"subscribe", contentTopic); + + // Wait for subscription + sleep(1); + + printf("\n5. Retrieving all possibl node info ids...\n"); + logosdelivery_get_available_node_info_ids(ctx, simple_callback, (void *)"get_available_node_info_ids"); + + printf("\nRetrieving node info for a specific invalid ID...\n"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "WrongNodeInfoId"); + + printf("\nRetrieving several node info for specific correct IDs...\n"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "Version"); + // logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "Metrics"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "MyMultiaddresses"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "MyENR"); + logosdelivery_get_node_info(ctx, simple_callback, (void *)"get_node_info", "MyPeerId"); + + printf("\nRetrieving available configs...\n"); + logosdelivery_get_available_configs(ctx, simple_callback, (void *)"get_available_configs"); + + printf("\n6. Sending a message...\n"); + printf("Watch for message events (sent, propagated, or error):\n"); + // Create base64-encoded payload: "Hello, Logos Messaging!" + const char *message = "{" + "\"contentTopic\": \"/example/1/chat/proto\"," + "\"payload\": \"SGVsbG8sIExvZ29zIE1lc3NhZ2luZyE=\"," + "\"ephemeral\": false" + "}"; + logosdelivery_send(ctx, simple_callback, (void *)"send", message); + + // Poll for terminal message events (sent, error, or received) with timeout + printf("Waiting for message delivery events...\n"); + int timeout_sec = 60; + int elapsed = 0; + while (!(got_message_sent || got_message_error || got_message_received) + && elapsed < timeout_sec) { + usleep(100000); // 100ms + elapsed++; + } + if (elapsed >= timeout_sec) { + printf("Timed out waiting for message events after %d seconds\n", timeout_sec); + } + + printf("\n7. Unsubscribing from content topic...\n"); + logosdelivery_unsubscribe(ctx, simple_callback, (void *)"unsubscribe", contentTopic); + + sleep(1); + + printf("\n8. Stopping node...\n"); + logosdelivery_stop_node(ctx, simple_callback, (void *)"stop_node"); + + sleep(1); + + printf("\n9. Destroying context...\n"); + logosdelivery_destroy(ctx, simple_callback, (void *)"destroy"); + + printf("\n=== Example completed ===\n"); + return 0; +} diff --git a/liblogosdelivery/json_event.nim b/liblogosdelivery/json_event.nim new file mode 100644 index 000000000..389e29120 --- /dev/null +++ b/liblogosdelivery/json_event.nim @@ -0,0 +1,27 @@ +import std/[json, macros] + +type JsonEvent*[T] = ref object + eventType*: string + payload*: T + +macro toFlatJson*(event: JsonEvent): JsonNode = + ## Serializes JsonEvent[T] to flat JSON with eventType first, + ## followed by all fields from T's payload + result = quote: + var jsonObj = newJObject() + jsonObj["eventType"] = %`event`.eventType + + # Serialize payload fields into the same object (flattening) + let payloadJson = %`event`.payload + for key, val in payloadJson.pairs: + jsonObj[key] = val + + jsonObj + +proc `$`*[T](event: JsonEvent[T]): string = + $toFlatJson(event) + +proc newJsonEvent*[T](eventType: string, payload: T): JsonEvent[T] = + ## Creates a new JsonEvent with the given eventType and payload. + ## The payload's fields will be flattened into the JSON output. + JsonEvent[T](eventType: eventType, payload: payload) diff --git a/liblogosdelivery/liblogosdelivery.h b/liblogosdelivery/liblogosdelivery.h new file mode 100644 index 000000000..5092db9f2 --- /dev/null +++ b/liblogosdelivery/liblogosdelivery.h @@ -0,0 +1,100 @@ + +// Generated manually and inspired by libwaku.h +// Header file for Logos Messaging API (LMAPI) library +#pragma once +#ifndef __liblogosdelivery__ +#define __liblogosdelivery__ + +#include +#include + +// The possible returned values for the functions that return int +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 + +#ifdef __cplusplus +extern "C" +{ +#endif + + typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); + + // Creates a new instance of the node from the given configuration JSON. + // Returns a pointer to the Context needed by the rest of the API functions. + // Configuration should be in JSON format using WakuNodeConf field names. + // Field names match Nim identifiers from WakuNodeConf (camelCase). + // Example: {"mode": "Core", "clusterId": 42, "relay": true} + void *logosdelivery_create_node( + const char *configJson, + FFICallBack callback, + void *userData); + + // Starts the node. + int logosdelivery_start_node(void *ctx, + FFICallBack callback, + void *userData); + + // Stops the node. + int logosdelivery_stop_node(void *ctx, + FFICallBack callback, + void *userData); + + // Destroys an instance of a node created with logosdelivery_create_node + int logosdelivery_destroy(void *ctx, + FFICallBack callback, + void *userData); + + // Subscribe to a content topic. + // contentTopic: string representing the content topic (e.g., "/myapp/1/chat/proto") + int logosdelivery_subscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *contentTopic); + + // Unsubscribe from a content topic. + int logosdelivery_unsubscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *contentTopic); + + // Send a message. + // messageJson: JSON string with the following structure: + // { + // "contentTopic": "/myapp/1/chat/proto", + // "payload": "base64-encoded-payload", + // "ephemeral": false + // } + // Returns a request ID that can be used to track the message delivery. + int logosdelivery_send(void *ctx, + FFICallBack callback, + void *userData, + const char *messageJson); + + // Sets a callback that will be invoked whenever an event occurs. + // It is crucial that the passed callback is fast, non-blocking and potentially thread-safe. + void logosdelivery_set_event_callback(void *ctx, + FFICallBack callback, + void *userData); + + // Retrieves the list of available node info IDs. + int logosdelivery_get_available_node_info_ids(void *ctx, + FFICallBack callback, + void *userData); + + // Given a node info ID, retrieves the corresponding info. + int logosdelivery_get_node_info(void *ctx, + FFICallBack callback, + void *userData, + const char *nodeInfoId); + + // Retrieves the list of available configurations. + int logosdelivery_get_available_configs(void *ctx, + FFICallBack callback, + void *userData); + +#ifdef __cplusplus +} +#endif + +#endif /* __liblogosdelivery__ */ diff --git a/liblogosdelivery/liblogosdelivery.nim b/liblogosdelivery/liblogosdelivery.nim new file mode 100644 index 000000000..fc907498a --- /dev/null +++ b/liblogosdelivery/liblogosdelivery.nim @@ -0,0 +1,11 @@ +import std/[atomics, options] +import chronicles, chronos, chronos/threadsync, ffi +import waku/factory/waku, waku/node/waku_node, ./declare_lib + +################################################################################ +## Include different APIs, i.e. all procs with {.ffi.} pragma + +include + ./logos_delivery_api/node_api, + ./logos_delivery_api/messaging_api, + ./logos_delivery_api/debug_api diff --git a/liblogosdelivery/logos_delivery_api/debug_api.nim b/liblogosdelivery/logos_delivery_api/debug_api.nim new file mode 100644 index 000000000..bb66a0e3f --- /dev/null +++ b/liblogosdelivery/logos_delivery_api/debug_api.nim @@ -0,0 +1,56 @@ +import std/[json, strutils] +import waku/factory/waku_state_info +import tools/confutils/[cli_args, config_option_meta] + +proc logosdelivery_get_available_node_info_ids( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## Returns the list of all available node info item ids that + ## can be queried with `get_node_info_item`. + requireInitializedNode(ctx, "GetNodeInfoIds"): + return err(errMsg) + + return ok($ctx.myLib[].stateInfo.getAllPossibleInfoItemIds()) + +proc logosdelivery_get_node_info( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + nodeInfoId: cstring, +) {.ffi.} = + ## Returns the content of the node info item with the given id if it exists. + requireInitializedNode(ctx, "GetNodeInfoItem"): + return err(errMsg) + + let infoItemIdEnum = + try: + parseEnum[NodeInfoId]($nodeInfoId) + except ValueError: + return err("Invalid node info id: " & $nodeInfoId) + + return ok(ctx.myLib[].stateInfo.getNodeInfoItem(infoItemIdEnum)) + +proc logosdelivery_get_available_configs( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## Returns information about the accepted config items. + requireInitializedNode(ctx, "GetAvailableConfigs"): + return err(errMsg) + + let optionMetas: seq[ConfigOptionMeta] = extractConfigOptionMeta(WakuNodeConf) + var configOptionDetails = newJArray() + + # for confField, confValue in fieldPairs(conf): + # defaultConfig[confField] = $confValue + + for meta in optionMetas: + configOptionDetails.add( + %*{ + meta.fieldName: meta.typeName & "(" & meta.defaultValue & ")", "desc": meta.desc + } + ) + + var jsonNode = newJObject() + jsonNode["configOptions"] = configOptionDetails + let asString = pretty(jsonNode) + return ok(pretty(jsonNode)) diff --git a/liblogosdelivery/logos_delivery_api/messaging_api.nim b/liblogosdelivery/logos_delivery_api/messaging_api.nim new file mode 100644 index 000000000..cb2771034 --- /dev/null +++ b/liblogosdelivery/logos_delivery_api/messaging_api.nim @@ -0,0 +1,91 @@ +import std/[json] +import chronos, results, ffi +import stew/byteutils +import + waku/common/base64, + waku/factory/waku, + waku/waku_core/topics/content_topic, + waku/api/[api, types], + ../declare_lib + +proc logosdelivery_subscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + contentTopicStr: cstring, +) {.ffi.} = + requireInitializedNode(ctx, "Subscribe"): + return err(errMsg) + + # ContentTopic is just a string type alias + let contentTopic = ContentTopic($contentTopicStr) + + (await api.subscribe(ctx.myLib[], contentTopic)).isOkOr: + let errMsg = $error + return err("Subscribe failed: " & errMsg) + + return ok("") + +proc logosdelivery_unsubscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + contentTopicStr: cstring, +) {.ffi.} = + requireInitializedNode(ctx, "Unsubscribe"): + return err(errMsg) + + # ContentTopic is just a string type alias + let contentTopic = ContentTopic($contentTopicStr) + + api.unsubscribe(ctx.myLib[], contentTopic).isOkOr: + let errMsg = $error + return err("Unsubscribe failed: " & errMsg) + + return ok("") + +proc logosdelivery_send( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + messageJson: cstring, +) {.ffi.} = + requireInitializedNode(ctx, "Send"): + return err(errMsg) + + ## Parse the message JSON and send the message + var jsonNode: JsonNode + try: + jsonNode = parseJson($messageJson) + except Exception as e: + return err("Failed to parse message JSON: " & e.msg) + + # Extract content topic + if not jsonNode.hasKey("contentTopic"): + return err("Missing contentTopic field") + + # ContentTopic is just a string type alias + let contentTopic = ContentTopic(jsonNode["contentTopic"].getStr()) + + # Extract payload (expect base64 encoded string) + if not jsonNode.hasKey("payload"): + return err("Missing payload field") + + let payloadStr = jsonNode["payload"].getStr() + let payload = base64.decode(Base64String(payloadStr)).valueOr: + return err("invalid payload format: " & error) + + # Extract ephemeral flag + let ephemeral = jsonNode.getOrDefault("ephemeral").getBool(false) + + # Create message envelope + let envelope = MessageEnvelope.init( + contentTopic = contentTopic, payload = payload, ephemeral = ephemeral + ) + + # Send the message + let requestId = (await api.send(ctx.myLib[], envelope)).valueOr: + let errMsg = $error + return err("Send failed: " & errMsg) + + return ok($requestId) diff --git a/liblogosdelivery/logos_delivery_api/node_api.nim b/liblogosdelivery/logos_delivery_api/node_api.nim new file mode 100644 index 000000000..2e30d1b43 --- /dev/null +++ b/liblogosdelivery/logos_delivery_api/node_api.nim @@ -0,0 +1,197 @@ +import std/[json, strutils, tables] +import chronos, chronicles, results, confutils, confutils/std/net, ffi +import + waku/factory/waku, + waku/node/waku_node, + waku/api/[api, types], + waku/events/[message_events, health_events], + tools/confutils/cli_args, + ../declare_lib, + ../json_event + +# Add JSON serialization for RequestId +proc `%`*(id: RequestId): JsonNode = + %($id) + +registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): + proc(configJson: cstring): Future[Result[string, string]] {.async.} = + ## Parse the JSON configuration using fieldPairs approach (WakuNodeConf) + var conf = defaultWakuNodeConf().valueOr: + return err("Failed creating default conf: " & error) + + var jsonNode: JsonNode + try: + jsonNode = parseJson($configJson) + except Exception: + let exceptionMsg = getCurrentExceptionMsg() + error "Failed to parse config JSON", + error = exceptionMsg, configJson = $configJson + return err( + "Failed to parse config JSON: " & exceptionMsg & " configJson string: " & + $configJson + ) + + var jsonFields: Table[string, (string, JsonNode)] + for key, value in jsonNode: + let lowerKey = key.toLowerAscii() + + if jsonFields.hasKey(lowerKey): + error "Duplicate configuration option found when normalized to lowercase", + key = key + return err( + "Duplicate configuration option found when normalized to lowercase: '" & key & + "'" + ) + + jsonFields[lowerKey] = (key, value) + + for confField, confValue in fieldPairs(conf): + let lowerField = confField.toLowerAscii() + if jsonFields.hasKey(lowerField): + let (jsonKey, jsonValue) = jsonFields[lowerField] + let formattedString = ($jsonValue).strip(chars = {'\"'}) + try: + confValue = parseCmdArg(typeof(confValue), formattedString) + except Exception: + return err( + "Failed to parse field '" & confField & "' from JSON key '" & jsonKey & "': " & + getCurrentExceptionMsg() & ". Value: " & formattedString + ) + + jsonFields.del(lowerField) + + if jsonFields.len > 0: + var unknownKeys = newSeq[string]() + for _, (jsonKey, _) in pairs(jsonFields): + unknownKeys.add(jsonKey) + error "Unrecognized configuration option(s) found", option = unknownKeys + return err("Unrecognized configuration option(s) found: " & $unknownKeys) + + # Create the node + ctx.myLib[] = (await api.createNode(conf)).valueOr: + let errMsg = $error + chronicles.error "CreateNodeRequest failed", err = errMsg + return err(errMsg) + + return ok("") + +proc logosdelivery_destroy( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +): cint {.dynlib, exportc, cdecl.} = + initializeLibrary() + checkParams(ctx, callback, userData) + + ffi.destroyFFIContext(ctx).isOkOr: + let msg = "liblogosdelivery error: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return RET_ERR + + ## always need to invoke the callback although we don't retrieve value to the caller + callback(RET_OK, nil, 0, userData) + + return RET_OK + +proc logosdelivery_create_node( + configJson: cstring, callback: FFICallback, userData: pointer +): pointer {.dynlib, exportc, cdecl.} = + initializeLibrary() + + if isNil(callback): + echo "error: missing callback in logosdelivery_create_node" + return nil + + var ctx = ffi.createFFIContext[Waku]().valueOr: + let msg = "Error in createFFIContext: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + return nil + + ctx.userData = userData + + ffi.sendRequestToFFIThread( + ctx, CreateNodeRequest.ffiNewReq(callback, userData, configJson) + ).isOkOr: + let msg = "error in sendRequestToFFIThread: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) + # free allocated resources as they won't be available + ffi.destroyFFIContext(ctx).isOkOr: + chronicles.error "Error in destroyFFIContext after sendRequestToFFIThread during creation", + err = $error + return nil + + return ctx + +proc logosdelivery_start_node( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + requireInitializedNode(ctx, "START_NODE"): + return err(errMsg) + + # setting up outgoing event listeners + let sentListener = MessageSentEvent.listen( + ctx.myLib[].brokerCtx, + proc(event: MessageSentEvent) {.async: (raises: []).} = + callEventCallback(ctx, "onMessageSent"): + $newJsonEvent("message_sent", event), + ).valueOr: + chronicles.error "MessageSentEvent.listen failed", err = $error + return err("MessageSentEvent.listen failed: " & $error) + + let errorListener = MessageErrorEvent.listen( + ctx.myLib[].brokerCtx, + proc(event: MessageErrorEvent) {.async: (raises: []).} = + callEventCallback(ctx, "onMessageError"): + $newJsonEvent("message_error", event), + ).valueOr: + chronicles.error "MessageErrorEvent.listen failed", err = $error + return err("MessageErrorEvent.listen failed: " & $error) + + let propagatedListener = MessagePropagatedEvent.listen( + ctx.myLib[].brokerCtx, + proc(event: MessagePropagatedEvent) {.async: (raises: []).} = + callEventCallback(ctx, "onMessagePropagated"): + $newJsonEvent("message_propagated", event), + ).valueOr: + chronicles.error "MessagePropagatedEvent.listen failed", err = $error + return err("MessagePropagatedEvent.listen failed: " & $error) + + let receivedListener = MessageReceivedEvent.listen( + ctx.myLib[].brokerCtx, + proc(event: MessageReceivedEvent) {.async: (raises: []).} = + callEventCallback(ctx, "onMessageReceived"): + $newJsonEvent("message_received", event), + ).valueOr: + chronicles.error "MessageReceivedEvent.listen failed", err = $error + return err("MessageReceivedEvent.listen failed: " & $error) + + let ConnectionStatusChangeListener = EventConnectionStatusChange.listen( + ctx.myLib[].brokerCtx, + proc(event: EventConnectionStatusChange) {.async: (raises: []).} = + callEventCallback(ctx, "onConnectionStatusChange"): + $newJsonEvent("connection_status_change", event), + ).valueOr: + chronicles.error "ConnectionStatusChange.listen failed", err = $error + return err("ConnectionStatusChange.listen failed: " & $error) + + (await startWaku(addr ctx.myLib[])).isOkOr: + let errMsg = $error + chronicles.error "START_NODE failed", err = errMsg + return err("failed to start: " & errMsg) + return ok("") + +proc logosdelivery_stop_node( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + requireInitializedNode(ctx, "STOP_NODE"): + return err(errMsg) + + await MessageErrorEvent.dropAllListeners(ctx.myLib[].brokerCtx) + await MessageSentEvent.dropAllListeners(ctx.myLib[].brokerCtx) + await MessagePropagatedEvent.dropAllListeners(ctx.myLib[].brokerCtx) + await MessageReceivedEvent.dropAllListeners(ctx.myLib[].brokerCtx) + await EventConnectionStatusChange.dropAllListeners(ctx.myLib[].brokerCtx) + + (await ctx.myLib[].stop()).isOkOr: + let errMsg = $error + chronicles.error "STOP_NODE failed", err = errMsg + return err("failed to stop: " & errMsg) + return ok("") diff --git a/liblogosdelivery/nim.cfg b/liblogosdelivery/nim.cfg new file mode 100644 index 000000000..3fd5adb32 --- /dev/null +++ b/liblogosdelivery/nim.cfg @@ -0,0 +1,27 @@ +# Nim configuration for liblogosdelivery + +# Ensure correct compiler configuration +--gc: + refc +--threads: + on + +# Include paths +--path: + "../vendor/nim-ffi" +--path: + "../" + +# Optimization and debugging +--opt: + speed +--debugger: + native + +# Export symbols for dynamic library +--app: + lib +--noMain + +# Enable FFI macro features when needed for debugging +# --define:ffiDumpMacros diff --git a/library/alloc.nim b/library/alloc.nim deleted file mode 100644 index 1a6f118b5..000000000 --- a/library/alloc.nim +++ /dev/null @@ -1,42 +0,0 @@ -## Can be shared safely between threads -type SharedSeq*[T] = tuple[data: ptr UncheckedArray[T], len: int] - -proc alloc*(str: cstring): cstring = - # Byte allocation from the given address. - # There should be the corresponding manual deallocation with deallocShared ! - if str.isNil(): - var ret = cast[cstring](allocShared(1)) # Allocate memory for the null terminator - ret[0] = '\0' # Set the null terminator - return ret - - let ret = cast[cstring](allocShared(len(str) + 1)) - copyMem(ret, str, len(str) + 1) - return ret - -proc alloc*(str: string): cstring = - ## Byte allocation from the given address. - ## There should be the corresponding manual deallocation with deallocShared ! - var ret = cast[cstring](allocShared(str.len + 1)) - let s = cast[seq[char]](str) - for i in 0 ..< str.len: - ret[i] = s[i] - ret[str.len] = '\0' - return ret - -proc allocSharedSeq*[T](s: seq[T]): SharedSeq[T] = - let data = allocShared(sizeof(T) * s.len) - if s.len != 0: - copyMem(data, unsafeAddr s[0], s.len) - return (cast[ptr UncheckedArray[T]](data), s.len) - -proc deallocSharedSeq*[T](s: var SharedSeq[T]) = - deallocShared(s.data) - s.len = 0 - -proc toSeq*[T](s: SharedSeq[T]): seq[T] = - ## Creates a seq[T] from a SharedSeq[T]. No explicit dealloc is required - ## as req[T] is a GC managed type. - var ret = newSeq[T]() - for i in 0 ..< s.len: - ret.add(s.data[i]) - return ret diff --git a/library/declare_lib.nim b/library/declare_lib.nim new file mode 100644 index 000000000..188de8549 --- /dev/null +++ b/library/declare_lib.nim @@ -0,0 +1,10 @@ +import ffi +import waku/factory/waku + +declareLibrary("waku") + +proc set_event_callback( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.dynlib, exportc, cdecl.} = + ctx[].eventCallback = cast[pointer](callback) + ctx[].eventUserData = userData diff --git a/library/events/json_connection_status_change_event.nim b/library/events/json_connection_status_change_event.nim new file mode 100644 index 000000000..86bfda780 --- /dev/null +++ b/library/events/json_connection_status_change_event.nim @@ -0,0 +1,15 @@ +{.push raises: [].} + +import system, std/json +import ./json_base_event +import ../../waku/api/types + +type JsonConnectionStatusChangeEvent* = ref object of JsonEvent + status*: ConnectionStatus + +proc new*(T: type JsonConnectionStatusChangeEvent, status: ConnectionStatus): T = + return + JsonConnectionStatusChangeEvent(eventType: "node_health_change", status: status) + +method `$`*(event: JsonConnectionStatusChangeEvent): string = + $(%*event) diff --git a/library/events/json_waku_not_responding_event.nim b/library/events/json_waku_not_responding_event.nim deleted file mode 100644 index 1e1d5fcc5..000000000 --- a/library/events/json_waku_not_responding_event.nim +++ /dev/null @@ -1,9 +0,0 @@ -import system, std/json, ./json_base_event - -type JsonWakuNotRespondingEvent* = ref object of JsonEvent - -proc new*(T: type JsonWakuNotRespondingEvent): T = - return JsonWakuNotRespondingEvent(eventType: "waku_not_responding") - -method `$`*(event: JsonWakuNotRespondingEvent): string = - $(%*event) diff --git a/library/ffi_types.nim b/library/ffi_types.nim deleted file mode 100644 index a5eeb9711..000000000 --- a/library/ffi_types.nim +++ /dev/null @@ -1,30 +0,0 @@ -################################################################################ -### Exported types - -type WakuCallBack* = proc( - callerRet: cint, msg: ptr cchar, len: csize_t, userData: pointer -) {.cdecl, gcsafe, raises: [].} - -const RET_OK*: cint = 0 -const RET_ERR*: cint = 1 -const RET_MISSING_CALLBACK*: cint = 2 - -### End of exported types -################################################################################ - -################################################################################ -### FFI utils - -template foreignThreadGc*(body: untyped) = - when declared(setupForeignThreadGc): - setupForeignThreadGc() - - body - - when declared(tearDownForeignThreadGc): - tearDownForeignThreadGc() - -type onDone* = proc() - -### End of FFI utils -################################################################################ diff --git a/library/ios_bearssl_stubs.c b/library/ios_bearssl_stubs.c new file mode 100644 index 000000000..a028cdf25 --- /dev/null +++ b/library/ios_bearssl_stubs.c @@ -0,0 +1,32 @@ +/** + * iOS stubs for BearSSL tools functions not normally included in the library. + * These are typically from the BearSSL tools/ directory which is for CLI tools. + */ + +#include + +/* x509_noanchor context - simplified stub */ +typedef struct { + void *vtable; + void *inner; +} x509_noanchor_context; + +/* Stub for x509_noanchor_init - used to skip anchor validation */ +void x509_noanchor_init(x509_noanchor_context *xwc, const void **inner) { + if (xwc && inner) { + xwc->inner = (void*)*inner; + xwc->vtable = NULL; + } +} + +/* TAs (Trust Anchors) - empty array stub */ +/* This is typically defined by applications with their CA certificates */ +typedef struct { + void *dn; + size_t dn_len; + unsigned flags; + void *pkey; +} br_x509_trust_anchor; + +const br_x509_trust_anchor TAs[1] = {{0}}; +const size_t TAs_NUM = 0; diff --git a/library/ios_natpmp_stubs.c b/library/ios_natpmp_stubs.c new file mode 100644 index 000000000..ef635db10 --- /dev/null +++ b/library/ios_natpmp_stubs.c @@ -0,0 +1,14 @@ +/** + * iOS stub for getgateway.c functions. + * iOS doesn't have net/route.h, so we provide a stub that returns failure. + * NAT-PMP functionality won't work but the library will link. + */ + +#include +#include + +/* getdefaultgateway - returns -1 (failure) on iOS */ +int getdefaultgateway(in_addr_t *addr) { + (void)addr; /* unused */ + return -1; /* failure - not supported on iOS */ +} diff --git a/library/kernel_api/debug_node_api.nim b/library/kernel_api/debug_node_api.nim new file mode 100644 index 000000000..9d5a7f134 --- /dev/null +++ b/library/kernel_api/debug_node_api.nim @@ -0,0 +1,50 @@ +import std/json +import + chronicles, + chronos, + results, + eth/p2p/discoveryv5/enr, + strutils, + libp2p/peerid, + metrics, + ffi +import + waku/factory/waku, waku/node/waku_node, waku/node/health_monitor, library/declare_lib + +proc getMultiaddresses(node: WakuNode): seq[string] = + return node.info().listenAddresses + +proc getMetrics(): string = + {.gcsafe.}: + return defaultRegistry.toText() ## defaultRegistry is {.global.} in metrics module + +proc waku_version( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok(WakuNodeVersionString) + +proc waku_listen_addresses( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## returns a comma-separated string of the listen addresses + return ok(ctx.myLib[].node.getMultiaddresses().join(",")) + +proc waku_get_my_enr( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok(ctx.myLib[].node.enr.toURI()) + +proc waku_get_my_peerid( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok($ctx.myLib[].node.peerId()) + +proc waku_get_metrics( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok(getMetrics()) + +proc waku_is_online( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + return ok($ctx.myLib[].healthMonitor.onlineMonitor.amIOnline()) diff --git a/library/kernel_api/discovery_api.nim b/library/kernel_api/discovery_api.nim new file mode 100644 index 000000000..f61b7bad1 --- /dev/null +++ b/library/kernel_api/discovery_api.nim @@ -0,0 +1,96 @@ +import std/json +import chronos, chronicles, results, strutils, libp2p/multiaddress, ffi +import + waku/factory/waku, + waku/discovery/waku_dnsdisc, + waku/discovery/waku_discv5, + waku/waku_core/peers, + waku/node/waku_node, + waku/node/kernel_api, + library/declare_lib + +proc retrieveBootstrapNodes( + enrTreeUrl: string, ipDnsServer: string +): Future[Result[seq[string], string]] {.async.} = + let dnsNameServers = @[parseIpAddress(ipDnsServer)] + let discoveredPeers: seq[RemotePeerInfo] = ( + await retrieveDynamicBootstrapNodes(enrTreeUrl, dnsNameServers) + ).valueOr: + return err("failed discovering peers from DNS: " & $error) + + var multiAddresses = newSeq[string]() + + for discPeer in discoveredPeers: + for address in discPeer.addrs: + multiAddresses.add($address & "/p2p/" & $discPeer) + + return ok(multiAddresses) + +proc updateDiscv5BootstrapNodes(nodes: string, waku: Waku): Result[void, string] = + waku.wakuDiscv5.updateBootstrapRecords(nodes).isOkOr: + return err("error in updateDiscv5BootstrapNodes: " & $error) + return ok() + +proc performPeerExchangeRequestTo*( + numPeers: uint64, waku: Waku +): Future[Result[int, string]] {.async.} = + let numPeersRecv = (await waku.node.fetchPeerExchangePeers(numPeers)).valueOr: + return err($error) + return ok(numPeersRecv) + +proc waku_discv5_update_bootnodes( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + bootnodes: cstring, +) {.ffi.} = + ## Updates the bootnode list used for discovering new peers via DiscoveryV5 + ## bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` + + updateDiscv5BootstrapNodes($bootnodes, ctx.myLib[]).isOkOr: + error "UPDATE_DISCV5_BOOTSTRAP_NODES failed", error = error + return err($error) + + return ok("discovery request processed correctly") + +proc waku_dns_discovery( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + enrTreeUrl: cstring, + nameDnsServer: cstring, + timeoutMs: cint, +) {.ffi.} = + let nodes = (await retrieveBootstrapNodes($enrTreeUrl, $nameDnsServer)).valueOr: + error "GET_BOOTSTRAP_NODES failed", error = error + return err($error) + + ## returns a comma-separated string of bootstrap nodes' multiaddresses + return ok(nodes.join(",")) + +proc waku_start_discv5( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + (await ctx.myLib[].wakuDiscv5.start()).isOkOr: + error "START_DISCV5 failed", error = error + return err("error starting discv5: " & $error) + + return ok("discv5 started correctly") + +proc waku_stop_discv5( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + await ctx.myLib[].wakuDiscv5.stop() + return ok("discv5 stopped correctly") + +proc waku_peer_exchange_request( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + numPeers: uint64, +) {.ffi.} = + let numValidPeers = (await performPeerExchangeRequestTo(numPeers, ctx.myLib[])).valueOr: + error "waku_peer_exchange_request failed", error = error + return err("failed peer exchange: " & $error) + + return ok($numValidPeers) diff --git a/library/waku_thread_requests/requests/node_lifecycle_request.nim b/library/kernel_api/node_lifecycle_api.nim similarity index 55% rename from library/waku_thread_requests/requests/node_lifecycle_request.nim rename to library/kernel_api/node_lifecycle_api.nim index 270bdf1ce..8f3e99b24 100644 --- a/library/waku_thread_requests/requests/node_lifecycle_request.nim +++ b/library/kernel_api/node_lifecycle_api.nim @@ -1,41 +1,14 @@ import std/[options, json, strutils, net] -import chronos, chronicles, results, confutils, confutils/std/net +import chronos, chronicles, results, confutils, confutils/std/net, ffi import - ../../../waku/node/peer_manager/peer_manager, - ../../../tools/confutils/cli_args, - ../../../waku/factory/waku, - ../../../waku/factory/node_factory, - ../../../waku/factory/networks_config, - ../../../waku/factory/app_callbacks, - ../../../waku/waku_api/rest/builder, - ../../alloc - -type NodeLifecycleMsgType* = enum - CREATE_NODE - START_NODE - STOP_NODE - -type NodeLifecycleRequest* = object - operation: NodeLifecycleMsgType - configJson: cstring ## Only used in 'CREATE_NODE' operation - appCallbacks: AppCallbacks - -proc createShared*( - T: type NodeLifecycleRequest, - op: NodeLifecycleMsgType, - configJson: cstring = "", - appCallbacks: AppCallbacks = nil, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].appCallbacks = appCallbacks - ret[].configJson = configJson.alloc() - return ret - -proc destroyShared(self: ptr NodeLifecycleRequest) = - deallocShared(self[].configJson) - deallocShared(self) + waku/node/peer_manager/peer_manager, + tools/confutils/cli_args, + waku/factory/waku, + waku/factory/node_factory, + waku/factory/app_callbacks, + waku/rest_api/endpoint/builder, + library/declare_lib proc createWaku( configJson: cstring, appCallbacks: AppCallbacks = nil @@ -85,26 +58,28 @@ proc createWaku( return ok(wakuRes) -proc process*( - self: ptr NodeLifecycleRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of CREATE_NODE: - waku[] = (await createWaku(self.configJson, self.appCallbacks)).valueOr: - error "CREATE_NODE failed", error = error +registerReqFFI(CreateNodeRequest, ctx: ptr FFIContext[Waku]): + proc( + configJson: cstring, appCallbacks: AppCallbacks + ): Future[Result[string, string]] {.async.} = + ctx.myLib[] = (await createWaku(configJson, cast[AppCallbacks](appCallbacks))).valueOr: + error "CreateNodeRequest failed", error = error return err($error) - of START_NODE: - (await waku.startWaku()).isOkOr: - error "START_NODE failed", error = error - return err($error) - of STOP_NODE: - try: - await waku[].stop() - except Exception: - error "STOP_NODE failed", error = getCurrentExceptionMsg() - return err(getCurrentExceptionMsg()) + return ok("") + +proc waku_start( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + (await startWaku(ctx[].myLib)).isOkOr: + error "START_NODE failed", error = error + return err("failed to start: " & $error) + return ok("") + +proc waku_stop( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + (await ctx.myLib[].stop()).isOkOr: + error "STOP_NODE failed", error = error + return err("failed to stop: " & $error) return ok("") diff --git a/library/kernel_api/peer_manager_api.nim b/library/kernel_api/peer_manager_api.nim new file mode 100644 index 000000000..f0ae37f00 --- /dev/null +++ b/library/kernel_api/peer_manager_api.nim @@ -0,0 +1,123 @@ +import std/[sequtils, strutils, tables] +import chronicles, chronos, results, options, json, ffi +import waku/factory/waku, waku/node/waku_node, waku/node/peer_manager, ../declare_lib + +type PeerInfo = object + protocols: seq[string] + addresses: seq[string] + +proc waku_get_peerids_from_peerstore( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## returns a comma-separated string of peerIDs + let peerIDs = + ctx.myLib[].node.peerManager.switch.peerStore.peers().mapIt($it.peerId).join(",") + return ok(peerIDs) + +proc waku_connect( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + peerMultiAddr: cstring, + timeoutMs: cuint, +) {.ffi.} = + let peers = ($peerMultiAddr).split(",").mapIt(strip(it)) + await ctx.myLib[].node.connectToNodes(peers, source = "static") + return ok("") + +proc waku_disconnect_peer_by_id( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer, peerId: cstring +) {.ffi.} = + let pId = PeerId.init($peerId).valueOr: + error "DISCONNECT_PEER_BY_ID failed", error = $error + return err($error) + await ctx.myLib[].node.peerManager.disconnectNode(pId) + return ok("") + +proc waku_disconnect_all_peers( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + await ctx.myLib[].node.peerManager.disconnectAllPeers() + return ok("") + +proc waku_dial_peer( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + peerMultiAddr: cstring, + protocol: cstring, + timeoutMs: cuint, +) {.ffi.} = + let remotePeerInfo = parsePeerInfo($peerMultiAddr).valueOr: + error "DIAL_PEER failed", error = $error + return err($error) + let conn = await ctx.myLib[].node.peerManager.dialPeer(remotePeerInfo, $protocol) + if conn.isNone(): + let msg = "failed dialing peer" + error "DIAL_PEER failed", error = msg, peerId = $remotePeerInfo.peerId + return err(msg) + return ok("") + +proc waku_dial_peer_by_id( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + peerId: cstring, + protocol: cstring, + timeoutMs: cuint, +) {.ffi.} = + let pId = PeerId.init($peerId).valueOr: + error "DIAL_PEER_BY_ID failed", error = $error + return err($error) + let conn = await ctx.myLib[].node.peerManager.dialPeer(pId, $protocol) + if conn.isNone(): + let msg = "failed dialing peer" + error "DIAL_PEER_BY_ID failed", error = msg, peerId = $peerId + return err(msg) + + return ok("") + +proc waku_get_connected_peers_info( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## returns a JSON string mapping peerIDs to objects with protocols and addresses + + var peersMap = initTable[string, PeerInfo]() + let peers = ctx.myLib[].node.peerManager.switch.peerStore.peers().filterIt( + it.connectedness == Connected + ) + + # Build a map of peer IDs to peer info objects + for peer in peers: + let peerIdStr = $peer.peerId + peersMap[peerIdStr] = + PeerInfo(protocols: peer.protocols, addresses: peer.addrs.mapIt($it)) + + # Convert the map to JSON string + let jsonObj = %*peersMap + let jsonStr = $jsonObj + return ok(jsonStr) + +proc waku_get_connected_peers( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + ## returns a comma-separated string of peerIDs + let + (inPeerIds, outPeerIds) = ctx.myLib[].node.peerManager.connectedPeers() + connectedPeerids = concat(inPeerIds, outPeerIds) + + return ok(connectedPeerids.mapIt($it).join(",")) + +proc waku_get_peerids_by_protocol( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + protocol: cstring, +) {.ffi.} = + ## returns a comma-separated string of peerIDs that mount the given protocol + let connectedPeers = ctx.myLib[].node.peerManager.switch.peerStore + .peers($protocol) + .filterIt(it.connectedness == Connected) + .mapIt($it.peerId) + .join(",") + return ok(connectedPeers) diff --git a/library/kernel_api/ping_api.nim b/library/kernel_api/ping_api.nim new file mode 100644 index 000000000..4f10dcf59 --- /dev/null +++ b/library/kernel_api/ping_api.nim @@ -0,0 +1,43 @@ +import std/[json, strutils] +import chronos, results, ffi +import libp2p/[protocols/ping, switch, multiaddress, multicodec] +import waku/[factory/waku, waku_core/peers, node/waku_node], library/declare_lib + +proc waku_ping_peer( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + peerAddr: cstring, + timeoutMs: cuint, +) {.ffi.} = + let peerInfo = peers.parsePeerInfo(($peerAddr).split(",")).valueOr: + return err("PingRequest failed to parse peer addr: " & $error) + + let timeout = chronos.milliseconds(timeoutMs) + proc ping(): Future[Result[Duration, string]] {.async, gcsafe.} = + try: + let conn = + await ctx.myLib[].node.switch.dial(peerInfo.peerId, peerInfo.addrs, PingCodec) + defer: + await conn.close() + + let pingRTT = await ctx.myLib[].node.libp2pPing.ping(conn) + if pingRTT == 0.nanos: + return err("could not ping peer: rtt-0") + return ok(pingRTT) + except CatchableError as exc: + return err("could not ping peer: " & exc.msg) + + let pingFuture = ping() + let pingRTT: Duration = + if timeout == chronos.milliseconds(0): # No timeout expected + (await pingFuture).valueOr: + return err("ping failed, no timeout expected: " & error) + else: + let timedOut = not (await pingFuture.withTimeout(timeout)) + if timedOut: + return err("ping timed out") + pingFuture.read().valueOr: + return err("failed to read ping future: " & error) + + return ok($(pingRTT.nanos)) diff --git a/library/kernel_api/protocols/filter_api.nim b/library/kernel_api/protocols/filter_api.nim new file mode 100644 index 000000000..c4f99510a --- /dev/null +++ b/library/kernel_api/protocols/filter_api.nim @@ -0,0 +1,109 @@ +import options, std/[strutils, sequtils] +import chronicles, chronos, results, ffi +import + waku/waku_filter_v2/client, + waku/waku_core/message/message, + waku/factory/waku, + waku/waku_relay, + waku/waku_filter_v2/common, + waku/waku_core/subscription/push_handler, + waku/node/peer_manager/peer_manager, + waku/node/waku_node, + waku/node/kernel_api, + waku/waku_core/topics/pubsub_topic, + waku/waku_core/topics/content_topic, + library/events/json_message_event, + library/declare_lib + +const FilterOpTimeout = 5.seconds + +proc checkFilterClientMounted(waku: Waku): Result[string, string] = + if waku.node.wakuFilterClient.isNil(): + let errorMsg = "wakuFilterClient is not mounted" + error "fail filter process", error = errorMsg + return err(errorMsg) + return ok("") + +proc waku_filter_subscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, + contentTopics: cstring, +) {.ffi.} = + proc onReceivedMessage(ctx: ptr FFIContext): WakuRelayHandler = + return proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async.} = + callEventCallback(ctx, "onReceivedMessage"): + $JsonMessageEvent.new(pubsubTopic, msg) + + checkFilterClientMounted(ctx.myLib[]).isOkOr: + return err($error) + + var filterPushEventCallback = FilterPushHandler(onReceivedMessage(ctx)) + ctx.myLib[].node.wakuFilterClient.registerPushHandler(filterPushEventCallback) + + let peer = ctx.myLib[].node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: + let errorMsg = "could not find peer with WakuFilterSubscribeCodec when subscribing" + error "fail filter subscribe", error = errorMsg + return err(errorMsg) + + let subFut = ctx.myLib[].node.filterSubscribe( + some(PubsubTopic($pubsubTopic)), + ($contentTopics).split(",").mapIt(ContentTopic(it)), + peer, + ) + if not await subFut.withTimeout(FilterOpTimeout): + let errorMsg = "filter subscription timed out" + error "fail filter unsubscribe", error = errorMsg + + return err(errorMsg) + + return ok("") + +proc waku_filter_unsubscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, + contentTopics: cstring, +) {.ffi.} = + checkFilterClientMounted(ctx.myLib[]).isOkOr: + return err($error) + + let peer = ctx.myLib[].node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: + let errorMsg = + "could not find peer with WakuFilterSubscribeCodec when unsubscribing" + error "fail filter process", error = errorMsg + return err(errorMsg) + + let subFut = ctx.myLib[].node.filterUnsubscribe( + some(PubsubTopic($pubsubTopic)), + ($contentTopics).split(",").mapIt(ContentTopic(it)), + peer, + ) + if not await subFut.withTimeout(FilterOpTimeout): + let errorMsg = "filter un-subscription timed out" + error "fail filter unsubscribe", error = errorMsg + return err(errorMsg) + return ok("") + +proc waku_filter_unsubscribe_all( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + checkFilterClientMounted(ctx.myLib[]).isOkOr: + return err($error) + + let peer = ctx.myLib[].node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: + let errorMsg = + "could not find peer with WakuFilterSubscribeCodec when unsubscribing all" + error "fail filter unsubscribe all", error = errorMsg + return err(errorMsg) + + let unsubFut = ctx.myLib[].node.filterUnsubscribeAll(peer) + + if not await unsubFut.withTimeout(FilterOpTimeout): + let errorMsg = "filter un-subscription all timed out" + error "fail filter unsubscribe all", error = errorMsg + + return err(errorMsg) + return ok("") diff --git a/library/kernel_api/protocols/lightpush_api.nim b/library/kernel_api/protocols/lightpush_api.nim new file mode 100644 index 000000000..e9251a3f3 --- /dev/null +++ b/library/kernel_api/protocols/lightpush_api.nim @@ -0,0 +1,51 @@ +import options, std/[json, strformat] +import chronicles, chronos, results, ffi +import + waku/waku_core/message/message, + waku/waku_core/codecs, + waku/factory/waku, + waku/waku_core/message, + waku/waku_core/topics/pubsub_topic, + waku/waku_lightpush_legacy/client, + waku/node/peer_manager/peer_manager, + library/events/json_message_event, + library/declare_lib + +proc waku_lightpush_publish( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, + jsonWakuMessage: cstring, +) {.ffi.} = + if ctx.myLib[].node.wakuLightpushClient.isNil(): + let errorMsg = "LightpushRequest waku.node.wakuLightpushClient is nil" + error "PUBLISH failed", error = errorMsg + return err(errorMsg) + + var jsonMessage: JsonMessage + try: + let jsonContent = parseJson($jsonWakuMessage) + jsonMessage = JsonMessage.fromJsonNode(jsonContent).valueOr: + raise newException(JsonParsingError, $error) + except JsonParsingError as exc: + return err(fmt"Error parsing json message: {exc.msg}") + + let msg = json_message_event.toWakuMessage(jsonMessage).valueOr: + return err("Problem building the WakuMessage: " & $error) + + let peerOpt = ctx.myLib[].node.peerManager.selectPeer(WakuLightPushCodec) + if peerOpt.isNone(): + let errorMsg = "failed to lightpublish message, no suitable remote peers" + error "PUBLISH failed", error = errorMsg + return err(errorMsg) + + let msgHashHex = ( + await ctx.myLib[].node.wakuLegacyLightpushClient.publish( + $pubsubTopic, msg, peer = peerOpt.get() + ) + ).valueOr: + error "PUBLISH failed", error = error + return err($error) + + return ok(msgHashHex) diff --git a/library/kernel_api/protocols/relay_api.nim b/library/kernel_api/protocols/relay_api.nim new file mode 100644 index 000000000..b184d6011 --- /dev/null +++ b/library/kernel_api/protocols/relay_api.nim @@ -0,0 +1,171 @@ +import std/[net, sequtils, strutils, json], strformat +import chronicles, chronos, stew/byteutils, results, ffi +import + waku/waku_core/message/message, + waku/factory/[validator_signed, waku], + tools/confutils/cli_args, + waku/waku_core/message, + waku/waku_core/topics/pubsub_topic, + waku/waku_core/topics, + waku/node/kernel_api/relay, + waku/waku_relay/protocol, + waku/node/peer_manager, + library/events/json_message_event, + library/declare_lib + +proc waku_relay_get_peers_in_mesh( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + let meshPeers = ctx.myLib[].node.wakuRelay.getPeersInMesh($pubsubTopic).valueOr: + error "LIST_MESH_PEERS failed", error = error + return err($error) + ## returns a comma-separated string of peerIDs + return ok(meshPeers.mapIt($it).join(",")) + +proc waku_relay_get_num_peers_in_mesh( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + let numPeersInMesh = ctx.myLib[].node.wakuRelay.getNumPeersInMesh($pubsubTopic).valueOr: + error "NUM_MESH_PEERS failed", error = error + return err($error) + return ok($numPeersInMesh) + +proc waku_relay_get_connected_peers( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + ## Returns the list of all connected peers to an specific pubsub topic + let connPeers = ctx.myLib[].node.wakuRelay.getConnectedPeers($pubsubTopic).valueOr: + error "LIST_CONNECTED_PEERS failed", error = error + return err($error) + ## returns a comma-separated string of peerIDs + return ok(connPeers.mapIt($it).join(",")) + +proc waku_relay_get_num_connected_peers( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + let numConnPeers = ctx.myLib[].node.wakuRelay.getNumConnectedPeers($pubsubTopic).valueOr: + error "NUM_CONNECTED_PEERS failed", error = error + return err($error) + return ok($numConnPeers) + +proc waku_relay_add_protected_shard( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + clusterId: cint, + shardId: cint, + publicKey: cstring, +) {.ffi.} = + ## Protects a shard with a public key + try: + let relayShard = RelayShard(clusterId: uint16(clusterId), shardId: uint16(shardId)) + let protectedShard = ProtectedShard.parseCmdArg($relayShard & ":" & $publicKey) + ctx.myLib[].node.wakuRelay.addSignedShardsValidator( + @[protectedShard], uint16(clusterId) + ) + except ValueError as exc: + return err("ERROR in waku_relay_add_protected_shard: " & exc.msg) + + return ok("") + +proc waku_relay_subscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + echo "Subscribing to topic: " & $pubSubTopic & " ..." + proc onReceivedMessage(ctx: ptr FFIContext[Waku]): WakuRelayHandler = + return proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async.} = + callEventCallback(ctx, "onReceivedMessage"): + $JsonMessageEvent.new(pubsubTopic, msg) + + var cb = onReceivedMessage(ctx) + + ctx.myLib[].node.subscribe( + (kind: SubscriptionKind.PubsubSub, topic: $pubsubTopic), + handler = WakuRelayHandler(cb), + ).isOkOr: + error "SUBSCRIBE failed", error = error + return err($error) + return ok("") + +proc waku_relay_unsubscribe( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, +) {.ffi.} = + ctx.myLib[].node.unsubscribe((kind: SubscriptionKind.PubsubSub, topic: $pubsubTopic)).isOkOr: + error "UNSUBSCRIBE failed", error = error + return err($error) + + return ok("") + +proc waku_relay_publish( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + pubSubTopic: cstring, + jsonWakuMessage: cstring, + timeoutMs: cuint, +) {.ffi.} = + var + # https://rfc.vac.dev/spec/36/#extern-char-waku_relay_publishchar-messagejson-char-pubsubtopic-int-timeoutms + jsonMessage: JsonMessage + try: + let jsonContent = parseJson($jsonWakuMessage) + jsonMessage = JsonMessage.fromJsonNode(jsonContent).valueOr: + raise newException(JsonParsingError, $error) + except JsonParsingError as exc: + return err(fmt"Error parsing json message: {exc.msg}") + + let msg = json_message_event.toWakuMessage(jsonMessage).valueOr: + return err("Problem building the WakuMessage: " & $error) + + (await ctx.myLib[].node.wakuRelay.publish($pubsubTopic, msg)).isOkOr: + error "PUBLISH failed", error = error + return err($error) + + let msgHash = computeMessageHash($pubSubTopic, msg).to0xHex + return ok(msgHash) + +proc waku_default_pubsub_topic( + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +) {.ffi.} = + # https://rfc.vac.dev/spec/36/#extern-char-waku_default_pubsub_topic + return ok(DefaultPubsubTopic) + +proc waku_content_topic( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + appName: cstring, + appVersion: cuint, + contentTopicName: cstring, + encoding: cstring, +) {.ffi.} = + # https://rfc.vac.dev/spec/36/#extern-char-waku_content_topicchar-applicationname-unsigned-int-applicationversion-char-contenttopicname-char-encoding + + return ok(fmt"/{$appName}/{$appVersion}/{$contentTopicName}/{$encoding}") + +proc waku_pubsub_topic( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, + topicName: cstring, +) {.ffi.} = + # https://rfc.vac.dev/spec/36/#extern-char-waku_pubsub_topicchar-name-char-encoding + return ok(fmt"/waku/2/{$topicName}") diff --git a/library/waku_thread_requests/requests/protocols/store_request.nim b/library/kernel_api/protocols/store_api.nim similarity index 57% rename from library/waku_thread_requests/requests/protocols/store_request.nim rename to library/kernel_api/protocols/store_api.nim index 3fe1e2f13..0df4d9b1f 100644 --- a/library/waku_thread_requests/requests/protocols/store_request.nim +++ b/library/kernel_api/protocols/store_api.nim @@ -1,28 +1,16 @@ import std/[json, sugar, strutils, options] -import chronos, chronicles, results, stew/byteutils +import chronos, chronicles, results, stew/byteutils, ffi import - ../../../../waku/factory/waku, - ../../../alloc, - ../../../utils, - ../../../../waku/waku_core/peers, - ../../../../waku/waku_core/time, - ../../../../waku/waku_core/message/digest, - ../../../../waku/waku_store/common, - ../../../../waku/waku_store/client, - ../../../../waku/common/paging + waku/factory/waku, + library/utils, + waku/waku_core/peers, + waku/waku_core/message/digest, + waku/waku_store/common, + waku/waku_store/client, + waku/common/paging, + library/declare_lib -type StoreReqType* = enum - REMOTE_QUERY ## to perform a query to another Store node - -type StoreRequest* = object - operation: StoreReqType - jsonQuery: cstring - peerAddr: cstring - timeoutMs: cint - -func fromJsonNode( - T: type StoreRequest, jsonContent: JsonNode -): Result[StoreQueryRequest, string] = +func fromJsonNode(jsonContent: JsonNode): Result[StoreQueryRequest, string] = var contentTopics: seq[string] if jsonContent.contains("contentTopics"): contentTopics = collect(newSeq): @@ -78,54 +66,29 @@ func fromJsonNode( ) ) -proc createShared*( - T: type StoreRequest, - op: StoreReqType, +proc waku_store_query( + ctx: ptr FFIContext[Waku], + callback: FFICallBack, + userData: pointer, jsonQuery: cstring, peerAddr: cstring, timeoutMs: cint, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].timeoutMs = timeoutMs - ret[].jsonQuery = jsonQuery.alloc() - ret[].peerAddr = peerAddr.alloc() - return ret - -proc destroyShared(self: ptr StoreRequest) = - deallocShared(self[].jsonQuery) - deallocShared(self[].peerAddr) - deallocShared(self) - -proc process_remote_query( - self: ptr StoreRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = +) {.ffi.} = let jsonContentRes = catch: - parseJson($self[].jsonQuery) + parseJson($jsonQuery) if jsonContentRes.isErr(): return err("StoreRequest failed parsing store request: " & jsonContentRes.error.msg) - let storeQueryRequest = ?StoreRequest.fromJsonNode(jsonContentRes.get()) + let storeQueryRequest = ?fromJsonNode(jsonContentRes.get()) - let peer = peers.parsePeerInfo(($self[].peerAddr).split(",")).valueOr: + let peer = peers.parsePeerInfo(($peerAddr).split(",")).valueOr: return err("StoreRequest failed to parse peer addr: " & $error) - let queryResponse = (await waku.node.wakuStoreClient.query(storeQueryRequest, peer)).valueOr: + let queryResponse = ( + await ctx.myLib[].node.wakuStoreClient.query(storeQueryRequest, peer) + ).valueOr: return err("StoreRequest failed store query: " & $error) let res = $(%*(queryResponse.toHex())) return ok(res) ## returning the response in json format - -proc process*( - self: ptr StoreRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - deallocShared(self) - - case self.operation - of REMOTE_QUERY: - return await self.process_remote_query(waku) - - error "store request not handled at all" - return err("store request not handled at all") diff --git a/library/libwaku.h b/library/libwaku.h index b5d6c9bab..67c89c7c2 100644 --- a/library/libwaku.h +++ b/library/libwaku.h @@ -10,241 +10,242 @@ #include // The possible returned values for the functions that return int -#define RET_OK 0 -#define RET_ERR 1 -#define RET_MISSING_CALLBACK 2 +#define RET_OK 0 +#define RET_ERR 1 +#define RET_MISSING_CALLBACK 2 #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -typedef void (*WakuCallBack) (int callerRet, const char* msg, size_t len, void* userData); + typedef void (*FFICallBack)(int callerRet, const char *msg, size_t len, void *userData); -// Creates a new instance of the waku node. -// Sets up the waku node from the given configuration. -// Returns a pointer to the Context needed by the rest of the API functions. -void* waku_new( - const char* configJson, - WakuCallBack callback, - void* userData); + // Creates a new instance of the waku node. + // Sets up the waku node from the given configuration. + // Returns a pointer to the Context needed by the rest of the API functions. + void *waku_new( + const char *configJson, + FFICallBack callback, + void *userData); -int waku_start(void* ctx, - WakuCallBack callback, - void* userData); + int waku_start(void *ctx, + FFICallBack callback, + void *userData); -int waku_stop(void* ctx, - WakuCallBack callback, - void* userData); + int waku_stop(void *ctx, + FFICallBack callback, + void *userData); -// Destroys an instance of a waku node created with waku_new -int waku_destroy(void* ctx, - WakuCallBack callback, - void* userData); + // Destroys an instance of a waku node created with waku_new + int waku_destroy(void *ctx, + FFICallBack callback, + void *userData); -int waku_version(void* ctx, - WakuCallBack callback, - void* userData); + int waku_version(void *ctx, + FFICallBack callback, + void *userData); -// Sets a callback that will be invoked whenever an event occurs. -// It is crucial that the passed callback is fast, non-blocking and potentially thread-safe. -void waku_set_event_callback(void* ctx, - WakuCallBack callback, - void* userData); + // Sets a callback that will be invoked whenever an event occurs. + // It is crucial that the passed callback is fast, non-blocking and potentially thread-safe. + void set_event_callback(void *ctx, + FFICallBack callback, + void *userData); -int waku_content_topic(void* ctx, - const char* appName, - unsigned int appVersion, - const char* contentTopicName, - const char* encoding, - WakuCallBack callback, - void* userData); + int waku_content_topic(void *ctx, + FFICallBack callback, + void *userData, + const char *appName, + unsigned int appVersion, + const char *contentTopicName, + const char *encoding); -int waku_pubsub_topic(void* ctx, - const char* topicName, - WakuCallBack callback, - void* userData); + int waku_pubsub_topic(void *ctx, + FFICallBack callback, + void *userData, + const char *topicName); -int waku_default_pubsub_topic(void* ctx, - WakuCallBack callback, - void* userData); + int waku_default_pubsub_topic(void *ctx, + FFICallBack callback, + void *userData); -int waku_relay_publish(void* ctx, - const char* pubSubTopic, - const char* jsonWakuMessage, - unsigned int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_relay_publish(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic, + const char *jsonWakuMessage, + unsigned int timeoutMs); -int waku_lightpush_publish(void* ctx, - const char* pubSubTopic, - const char* jsonWakuMessage, - WakuCallBack callback, - void* userData); + int waku_lightpush_publish(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic, + const char *jsonWakuMessage); -int waku_relay_subscribe(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_subscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_relay_add_protected_shard(void* ctx, - int clusterId, - int shardId, - char* publicKey, - WakuCallBack callback, - void* userData); + int waku_relay_add_protected_shard(void *ctx, + FFICallBack callback, + void *userData, + int clusterId, + int shardId, + char *publicKey); -int waku_relay_unsubscribe(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_unsubscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_filter_subscribe(void* ctx, - const char* pubSubTopic, - const char* contentTopics, - WakuCallBack callback, - void* userData); + int waku_filter_subscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic, + const char *contentTopics); -int waku_filter_unsubscribe(void* ctx, - const char* pubSubTopic, - const char* contentTopics, - WakuCallBack callback, - void* userData); + int waku_filter_unsubscribe(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic, + const char *contentTopics); -int waku_filter_unsubscribe_all(void* ctx, - WakuCallBack callback, - void* userData); + int waku_filter_unsubscribe_all(void *ctx, + FFICallBack callback, + void *userData); -int waku_relay_get_num_connected_peers(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_get_num_connected_peers(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_relay_get_connected_peers(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_get_connected_peers(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_relay_get_num_peers_in_mesh(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_get_num_peers_in_mesh(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_relay_get_peers_in_mesh(void* ctx, - const char* pubSubTopic, - WakuCallBack callback, - void* userData); + int waku_relay_get_peers_in_mesh(void *ctx, + FFICallBack callback, + void *userData, + const char *pubSubTopic); -int waku_store_query(void* ctx, - const char* jsonQuery, - const char* peerAddr, - int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_store_query(void *ctx, + FFICallBack callback, + void *userData, + const char *jsonQuery, + const char *peerAddr, + int timeoutMs); -int waku_connect(void* ctx, - const char* peerMultiAddr, - unsigned int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_connect(void *ctx, + FFICallBack callback, + void *userData, + const char *peerMultiAddr, + unsigned int timeoutMs); -int waku_disconnect_peer_by_id(void* ctx, - const char* peerId, - WakuCallBack callback, - void* userData); + int waku_disconnect_peer_by_id(void *ctx, + FFICallBack callback, + void *userData, + const char *peerId); -int waku_disconnect_all_peers(void* ctx, - WakuCallBack callback, - void* userData); + int waku_disconnect_all_peers(void *ctx, + FFICallBack callback, + void *userData); -int waku_dial_peer(void* ctx, - const char* peerMultiAddr, - const char* protocol, - int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_dial_peer(void *ctx, + FFICallBack callback, + void *userData, + const char *peerMultiAddr, + const char *protocol, + int timeoutMs); -int waku_dial_peer_by_id(void* ctx, - const char* peerId, - const char* protocol, - int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_dial_peer_by_id(void *ctx, + FFICallBack callback, + void *userData, + const char *peerId, + const char *protocol, + int timeoutMs); -int waku_get_peerids_from_peerstore(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_peerids_from_peerstore(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_connected_peers_info(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_connected_peers_info(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_peerids_by_protocol(void* ctx, - const char* protocol, - WakuCallBack callback, - void* userData); + int waku_get_peerids_by_protocol(void *ctx, + FFICallBack callback, + void *userData, + const char *protocol); -int waku_listen_addresses(void* ctx, - WakuCallBack callback, - void* userData); + int waku_listen_addresses(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_connected_peers(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_connected_peers(void *ctx, + FFICallBack callback, + void *userData); -// Returns a list of multiaddress given a url to a DNS discoverable ENR tree -// Parameters -// char* entTreeUrl: URL containing a discoverable ENR tree -// char* nameDnsServer: The nameserver to resolve the ENR tree url. -// int timeoutMs: Timeout value in milliseconds to execute the call. -int waku_dns_discovery(void* ctx, - const char* entTreeUrl, - const char* nameDnsServer, - int timeoutMs, - WakuCallBack callback, - void* userData); + // Returns a list of multiaddress given a url to a DNS discoverable ENR tree + // Parameters + // char* entTreeUrl: URL containing a discoverable ENR tree + // char* nameDnsServer: The nameserver to resolve the ENR tree url. + // int timeoutMs: Timeout value in milliseconds to execute the call. + int waku_dns_discovery(void *ctx, + FFICallBack callback, + void *userData, + const char *entTreeUrl, + const char *nameDnsServer, + int timeoutMs); -// Updates the bootnode list used for discovering new peers via DiscoveryV5 -// bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` -int waku_discv5_update_bootnodes(void* ctx, - char* bootnodes, - WakuCallBack callback, - void* userData); + // Updates the bootnode list used for discovering new peers via DiscoveryV5 + // bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` + int waku_discv5_update_bootnodes(void *ctx, + FFICallBack callback, + void *userData, + char *bootnodes); -int waku_start_discv5(void* ctx, - WakuCallBack callback, - void* userData); + int waku_start_discv5(void *ctx, + FFICallBack callback, + void *userData); -int waku_stop_discv5(void* ctx, - WakuCallBack callback, - void* userData); + int waku_stop_discv5(void *ctx, + FFICallBack callback, + void *userData); -// Retrieves the ENR information -int waku_get_my_enr(void* ctx, - WakuCallBack callback, - void* userData); + // Retrieves the ENR information + int waku_get_my_enr(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_my_peerid(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_my_peerid(void *ctx, + FFICallBack callback, + void *userData); -int waku_get_metrics(void* ctx, - WakuCallBack callback, - void* userData); + int waku_get_metrics(void *ctx, + FFICallBack callback, + void *userData); -int waku_peer_exchange_request(void* ctx, - int numPeers, - WakuCallBack callback, - void* userData); + int waku_peer_exchange_request(void *ctx, + FFICallBack callback, + void *userData, + int numPeers); -int waku_ping_peer(void* ctx, - const char* peerAddr, - int timeoutMs, - WakuCallBack callback, - void* userData); + int waku_ping_peer(void *ctx, + FFICallBack callback, + void *userData, + const char *peerAddr, + int timeoutMs); -int waku_is_online(void* ctx, - WakuCallBack callback, - void* userData); + int waku_is_online(void *ctx, + FFICallBack callback, + void *userData); #ifdef __cplusplus } diff --git a/library/libwaku.nim b/library/libwaku.nim index ad3afa134..dd1ee9fd9 100644 --- a/library/libwaku.nim +++ b/library/libwaku.nim @@ -1,107 +1,37 @@ -{.pragma: exported, exportc, cdecl, raises: [].} -{.pragma: callback, cdecl, raises: [], gcsafe.} -{.passc: "-fPIC".} - -when defined(linux): - {.passl: "-Wl,-soname,libwaku.so".} - -import std/[json, atomics, strformat, options, atomics] -import chronicles, chronos, chronos/threadsync +import std/[atomics, options, atomics, macros] +import chronicles, chronos, chronos/threadsync, ffi import - waku/common/base64, waku/waku_core/message/message, - waku/node/waku_node, - waku/node/peer_manager, waku/waku_core/topics/pubsub_topic, - waku/waku_core/subscription/push_handler, waku/waku_relay, ./events/json_message_event, - ./waku_context, - ./waku_thread_requests/requests/node_lifecycle_request, - ./waku_thread_requests/requests/peer_manager_request, - ./waku_thread_requests/requests/protocols/relay_request, - ./waku_thread_requests/requests/protocols/store_request, - ./waku_thread_requests/requests/protocols/lightpush_request, - ./waku_thread_requests/requests/protocols/filter_request, - ./waku_thread_requests/requests/debug_node_request, - ./waku_thread_requests/requests/discovery_request, - ./waku_thread_requests/requests/ping_request, - ./waku_thread_requests/waku_thread_request, - ./alloc, - ./ffi_types, - ../waku/factory/app_callbacks + ./events/json_topic_health_change_event, + ./events/json_connection_change_event, + ./events/json_connection_status_change_event, + ../waku/factory/app_callbacks, + waku/factory/waku, + waku/node/waku_node, + waku/node/health_monitor/health_status, + ./declare_lib ################################################################################ -### Wrapper around the waku node -################################################################################ - -################################################################################ -### Not-exported components - -template checkLibwakuParams*( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -) = - if not isNil(ctx): - ctx[].userData = userData - - if isNil(callback): - return RET_MISSING_CALLBACK - -proc handleRequest( - ctx: ptr WakuContext, - requestType: RequestType, - content: pointer, - callback: WakuCallBack, - userData: pointer, -): cint = - waku_context.sendRequestToWakuThread(ctx, requestType, content, callback, userData).isOkOr: - let msg = "libwaku error: " & $error - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - return RET_OK - -### End of not-exported components -################################################################################ - -################################################################################ -### Library setup - -# Every Nim library must have this function called - the name is derived from -# the `--nimMainPrefix` command line option -proc libwakuNimMain() {.importc.} - -# To control when the library has been initialized -var initialized: Atomic[bool] - -if defined(android): - # Redirect chronicles to Android System logs - when compiles(defaultChroniclesStream.outputs[0].writer): - defaultChroniclesStream.outputs[0].writer = proc( - logLevel: LogLevel, msg: LogOutputStr - ) {.raises: [].} = - echo logLevel, msg - -proc initializeLibrary() {.exported.} = - if not initialized.exchange(true): - ## Every Nim library needs to call `NimMain` once exactly, to initialize the Nim runtime. - ## Being `` the value given in the optional compilation flag --nimMainPrefix:yourprefix - libwakuNimMain() - when declared(setupForeignThreadGc): - setupForeignThreadGc() - when declared(nimGC_setStackBottom): - var locals {.volatile, noinit.}: pointer - locals = addr(locals) - nimGC_setStackBottom(locals) - -### End of library setup -################################################################################ +## Include different APIs, i.e. all procs with {.ffi.} pragma +include + ./kernel_api/peer_manager_api, + ./kernel_api/discovery_api, + ./kernel_api/node_lifecycle_api, + ./kernel_api/debug_node_api, + ./kernel_api/ping_api, + ./kernel_api/protocols/relay_api, + ./kernel_api/protocols/store_api, + ./kernel_api/protocols/lightpush_api, + ./kernel_api/protocols/filter_api ################################################################################ ### Exported procs proc waku_new( - configJson: cstring, callback: WakuCallback, userData: pointer + configJson: cstring, callback: FFICallback, userData: pointer ): pointer {.dynlib, exportc, cdecl.} = initializeLibrary() @@ -111,41 +41,56 @@ proc waku_new( return nil ## Create the Waku thread that will keep waiting for req from the main thread. - var ctx = waku_context.createWakuContext().valueOr: - let msg = "Error in createWakuContext: " & $error + var ctx = ffi.createFFIContext[Waku]().valueOr: + let msg = "Error in createFFIContext: " & $error callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) return nil ctx.userData = userData + proc onReceivedMessage(ctx: ptr FFIContext): WakuRelayHandler = + return proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async.} = + callEventCallback(ctx, "onReceivedMessage"): + $JsonMessageEvent.new(pubsubTopic, msg) + + proc onTopicHealthChange(ctx: ptr FFIContext): TopicHealthChangeHandler = + return proc(pubsubTopic: PubsubTopic, topicHealth: TopicHealth) {.async.} = + callEventCallback(ctx, "onTopicHealthChange"): + $JsonTopicHealthChangeEvent.new(pubsubTopic, topicHealth) + + proc onConnectionChange(ctx: ptr FFIContext): ConnectionChangeHandler = + return proc(peerId: PeerId, peerEvent: PeerEventKind) {.async.} = + callEventCallback(ctx, "onConnectionChange"): + $JsonConnectionChangeEvent.new($peerId, peerEvent) + + proc onConnectionStatusChange(ctx: ptr FFIContext): ConnectionStatusChangeHandler = + return proc(status: ConnectionStatus) {.async.} = + callEventCallback(ctx, "onConnectionStatusChange"): + $JsonConnectionStatusChangeEvent.new(status) + let appCallbacks = AppCallbacks( relayHandler: onReceivedMessage(ctx), topicHealthChangeHandler: onTopicHealthChange(ctx), connectionChangeHandler: onConnectionChange(ctx), + connectionStatusChangeHandler: onConnectionStatusChange(ctx), ) - let retCode = handleRequest( - ctx, - RequestType.LIFECYCLE, - NodeLifecycleRequest.createShared( - NodeLifecycleMsgType.CREATE_NODE, configJson, appCallbacks - ), - callback, - userData, - ) - - if retCode == RET_ERR: + ffi.sendRequestToFFIThread( + ctx, CreateNodeRequest.ffiNewReq(callback, userData, configJson, appCallbacks) + ).isOkOr: + let msg = "error in sendRequestToFFIThread: " & $error + callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) return nil return ctx proc waku_destroy( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = + ctx: ptr FFIContext[Waku], callback: FFICallBack, userData: pointer +): cint {.dynlib, exportc, cdecl.} = initializeLibrary() - checkLibwakuParams(ctx, callback, userData) + checkParams(ctx, callback, userData) - waku_context.destroyWakuContext(ctx).isOkOr: + ffi.destroyFFIContext(ctx).isOkOr: let msg = "libwaku error: " & $error callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) return RET_ERR @@ -155,699 +100,5 @@ proc waku_destroy( return RET_OK -proc waku_version( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - callback( - RET_OK, - cast[ptr cchar](WakuNodeVersionString), - cast[csize_t](len(WakuNodeVersionString)), - userData, - ) - - return RET_OK - -proc waku_set_event_callback( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -) {.dynlib, exportc.} = - initializeLibrary() - ctx[].eventCallback = cast[pointer](callback) - ctx[].eventUserData = userData - -proc waku_content_topic( - ctx: ptr WakuContext, - appName: cstring, - appVersion: cuint, - contentTopicName: cstring, - encoding: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - # https://rfc.vac.dev/spec/36/#extern-char-waku_content_topicchar-applicationname-unsigned-int-applicationversion-char-contenttopicname-char-encoding - - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - let contentTopic = fmt"/{$appName}/{$appVersion}/{$contentTopicName}/{$encoding}" - callback( - RET_OK, unsafeAddr contentTopic[0], cast[csize_t](len(contentTopic)), userData - ) - - return RET_OK - -proc waku_pubsub_topic( - ctx: ptr WakuContext, topicName: cstring, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc, cdecl.} = - # https://rfc.vac.dev/spec/36/#extern-char-waku_pubsub_topicchar-name-char-encoding - - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - let outPubsubTopic = fmt"/waku/2/{$topicName}" - callback( - RET_OK, unsafeAddr outPubsubTopic[0], cast[csize_t](len(outPubsubTopic)), userData - ) - - return RET_OK - -proc waku_default_pubsub_topic( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - # https://rfc.vac.dev/spec/36/#extern-char-waku_default_pubsub_topic - - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - callback( - RET_OK, - cast[ptr cchar](DefaultPubsubTopic), - cast[csize_t](len(DefaultPubsubTopic)), - userData, - ) - - return RET_OK - -proc waku_relay_publish( - ctx: ptr WakuContext, - pubSubTopic: cstring, - jsonWakuMessage: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - # https://rfc.vac.dev/spec/36/#extern-char-waku_relay_publishchar-messagejson-char-pubsubtopic-int-timeoutms - - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - var jsonMessage: JsonMessage - try: - let jsonContent = parseJson($jsonWakuMessage) - jsonMessage = JsonMessage.fromJsonNode(jsonContent).valueOr: - raise newException(JsonParsingError, $error) - except JsonParsingError: - let msg = fmt"Error parsing json message: {getCurrentExceptionMsg()}" - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - let wakuMessage = jsonMessage.toWakuMessage().valueOr: - let msg = "Problem building the WakuMessage: " & $error - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.PUBLISH, pubSubTopic, nil, wakuMessage), - callback, - userData, - ) - -proc waku_start( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - handleRequest( - ctx, - RequestType.LIFECYCLE, - NodeLifecycleRequest.createShared(NodeLifecycleMsgType.START_NODE), - callback, - userData, - ) - -proc waku_stop( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - handleRequest( - ctx, - RequestType.LIFECYCLE, - NodeLifecycleRequest.createShared(NodeLifecycleMsgType.STOP_NODE), - callback, - userData, - ) - -proc waku_relay_subscribe( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - var cb = onReceivedMessage(ctx) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.SUBSCRIBE, pubSubTopic, WakuRelayHandler(cb)), - callback, - userData, - ) - -proc waku_relay_add_protected_shard( - ctx: ptr WakuContext, - clusterId: cint, - shardId: cint, - publicKey: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared( - RelayMsgType.ADD_PROTECTED_SHARD, - clusterId = clusterId, - shardId = shardId, - publicKey = publicKey, - ), - callback, - userData, - ) - -proc waku_relay_unsubscribe( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared( - RelayMsgType.UNSUBSCRIBE, pubSubTopic, WakuRelayHandler(onReceivedMessage(ctx)) - ), - callback, - userData, - ) - -proc waku_relay_get_num_connected_peers( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.NUM_CONNECTED_PEERS, pubSubTopic), - callback, - userData, - ) - -proc waku_relay_get_connected_peers( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.LIST_CONNECTED_PEERS, pubSubTopic), - callback, - userData, - ) - -proc waku_relay_get_num_peers_in_mesh( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.NUM_MESH_PEERS, pubSubTopic), - callback, - userData, - ) - -proc waku_relay_get_peers_in_mesh( - ctx: ptr WakuContext, - pubSubTopic: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.RELAY, - RelayRequest.createShared(RelayMsgType.LIST_MESH_PEERS, pubSubTopic), - callback, - userData, - ) - -proc waku_filter_subscribe( - ctx: ptr WakuContext, - pubSubTopic: cstring, - contentTopics: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.FILTER, - FilterRequest.createShared( - FilterMsgType.SUBSCRIBE, - pubSubTopic, - contentTopics, - FilterPushHandler(onReceivedMessage(ctx)), - ), - callback, - userData, - ) - -proc waku_filter_unsubscribe( - ctx: ptr WakuContext, - pubSubTopic: cstring, - contentTopics: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.FILTER, - FilterRequest.createShared(FilterMsgType.UNSUBSCRIBE, pubSubTopic, contentTopics), - callback, - userData, - ) - -proc waku_filter_unsubscribe_all( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.FILTER, - FilterRequest.createShared(FilterMsgType.UNSUBSCRIBE_ALL), - callback, - userData, - ) - -proc waku_lightpush_publish( - ctx: ptr WakuContext, - pubSubTopic: cstring, - jsonWakuMessage: cstring, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc, cdecl.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - var jsonMessage: JsonMessage - try: - let jsonContent = parseJson($jsonWakuMessage) - jsonMessage = JsonMessage.fromJsonNode(jsonContent).valueOr: - raise newException(JsonParsingError, $error) - except JsonParsingError: - let msg = fmt"Error parsing json message: {getCurrentExceptionMsg()}" - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - let wakuMessage = jsonMessage.toWakuMessage().valueOr: - let msg = "Problem building the WakuMessage: " & $error - callback(RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), userData) - return RET_ERR - - handleRequest( - ctx, - RequestType.LIGHTPUSH, - LightpushRequest.createShared(LightpushMsgType.PUBLISH, pubSubTopic, wakuMessage), - callback, - userData, - ) - -proc waku_connect( - ctx: ptr WakuContext, - peerMultiAddr: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - PeerManagementMsgType.CONNECT_TO, $peerMultiAddr, chronos.milliseconds(timeoutMs) - ), - callback, - userData, - ) - -proc waku_disconnect_peer_by_id( - ctx: ptr WakuContext, peerId: cstring, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - op = PeerManagementMsgType.DISCONNECT_PEER_BY_ID, peerId = $peerId - ), - callback, - userData, - ) - -proc waku_disconnect_all_peers( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared(op = PeerManagementMsgType.DISCONNECT_ALL_PEERS), - callback, - userData, - ) - -proc waku_dial_peer( - ctx: ptr WakuContext, - peerMultiAddr: cstring, - protocol: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - op = PeerManagementMsgType.DIAL_PEER, - peerMultiAddr = $peerMultiAddr, - protocol = $protocol, - ), - callback, - userData, - ) - -proc waku_dial_peer_by_id( - ctx: ptr WakuContext, - peerId: cstring, - protocol: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - op = PeerManagementMsgType.DIAL_PEER_BY_ID, peerId = $peerId, protocol = $protocol - ), - callback, - userData, - ) - -proc waku_get_peerids_from_peerstore( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared(PeerManagementMsgType.GET_ALL_PEER_IDS), - callback, - userData, - ) - -proc waku_get_connected_peers_info( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared(PeerManagementMsgType.GET_CONNECTED_PEERS_INFO), - callback, - userData, - ) - -proc waku_get_connected_peers( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared(PeerManagementMsgType.GET_CONNECTED_PEERS), - callback, - userData, - ) - -proc waku_get_peerids_by_protocol( - ctx: ptr WakuContext, protocol: cstring, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PEER_MANAGER, - PeerManagementRequest.createShared( - op = PeerManagementMsgType.GET_PEER_IDS_BY_PROTOCOL, protocol = $protocol - ), - callback, - userData, - ) - -proc waku_store_query( - ctx: ptr WakuContext, - jsonQuery: cstring, - peerAddr: cstring, - timeoutMs: cint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.STORE, - StoreRequest.createShared(StoreReqType.REMOTE_QUERY, jsonQuery, peerAddr, timeoutMs), - callback, - userData, - ) - -proc waku_listen_addresses( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_LISTENING_ADDRESSES), - callback, - userData, - ) - -proc waku_dns_discovery( - ctx: ptr WakuContext, - entTreeUrl: cstring, - nameDnsServer: cstring, - timeoutMs: cint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createRetrieveBootstrapNodesRequest( - DiscoveryMsgType.GET_BOOTSTRAP_NODES, entTreeUrl, nameDnsServer, timeoutMs - ), - callback, - userData, - ) - -proc waku_discv5_update_bootnodes( - ctx: ptr WakuContext, bootnodes: cstring, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - ## Updates the bootnode list used for discovering new peers via DiscoveryV5 - ## bootnodes - JSON array containing the bootnode ENRs i.e. `["enr:...", "enr:..."]` - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createUpdateBootstrapNodesRequest( - DiscoveryMsgType.UPDATE_DISCV5_BOOTSTRAP_NODES, bootnodes - ), - callback, - userData, - ) - -proc waku_get_my_enr( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_MY_ENR), - callback, - userData, - ) - -proc waku_get_my_peerid( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_MY_PEER_ID), - callback, - userData, - ) - -proc waku_get_metrics( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_METRICS), - callback, - userData, - ) - -proc waku_start_discv5( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createDiscV5StartRequest(), - callback, - userData, - ) - -proc waku_stop_discv5( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createDiscV5StopRequest(), - callback, - userData, - ) - -proc waku_peer_exchange_request( - ctx: ptr WakuContext, numPeers: uint64, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DISCOVERY, - DiscoveryRequest.createPeerExchangeRequest(numPeers), - callback, - userData, - ) - -proc waku_ping_peer( - ctx: ptr WakuContext, - peerAddr: cstring, - timeoutMs: cuint, - callback: WakuCallBack, - userData: pointer, -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.PING, - PingRequest.createShared(peerAddr, chronos.milliseconds(timeoutMs)), - callback, - userData, - ) - -proc waku_is_online( - ctx: ptr WakuContext, callback: WakuCallBack, userData: pointer -): cint {.dynlib, exportc.} = - initializeLibrary() - checkLibwakuParams(ctx, callback, userData) - - handleRequest( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.RETRIEVE_ONLINE_STATE), - callback, - userData, - ) - -### End of exported procs -################################################################################ +# ### End of exported procs +# ################################################################################ diff --git a/library/waku_context.nim b/library/waku_context.nim deleted file mode 100644 index 64a9e3466..000000000 --- a/library/waku_context.nim +++ /dev/null @@ -1,226 +0,0 @@ -{.pragma: exported, exportc, cdecl, raises: [].} -{.pragma: callback, cdecl, raises: [], gcsafe.} -{.passc: "-fPIC".} - -import std/[options, atomics, os, net, locks] -import chronicles, chronos, chronos/threadsync, taskpools/channels_spsc_single, results -import - waku/common/logging, - waku/factory/waku, - waku/node/peer_manager, - waku/waku_relay/[protocol, topic_health], - waku/waku_core/[topics/pubsub_topic, message], - ./waku_thread_requests/[waku_thread_request, requests/debug_node_request], - ./ffi_types, - ./events/[ - json_message_event, json_topic_health_change_event, json_connection_change_event, - json_waku_not_responding_event, - ] - -type WakuContext* = object - wakuThread: Thread[(ptr WakuContext)] - watchdogThread: Thread[(ptr WakuContext)] - # monitors the Waku thread and notifies the Waku SDK consumer if it hangs - lock: Lock - reqChannel: ChannelSPSCSingle[ptr WakuThreadRequest] - reqSignal: ThreadSignalPtr - # to inform The Waku Thread (a.k.a TWT) that a new request is sent - reqReceivedSignal: ThreadSignalPtr - # to inform the main thread that the request is rx by TWT - userData*: pointer - eventCallback*: pointer - eventUserdata*: pointer - running: Atomic[bool] # To control when the threads are running - -const git_version* {.strdefine.} = "n/a" -const versionString = "version / git commit hash: " & waku.git_version - -template callEventCallback(ctx: ptr WakuContext, eventName: string, body: untyped) = - if isNil(ctx[].eventCallback): - error eventName & " - eventCallback is nil" - return - - foreignThreadGc: - try: - let event = body - cast[WakuCallBack](ctx[].eventCallback)( - RET_OK, unsafeAddr event[0], cast[csize_t](len(event)), ctx[].eventUserData - ) - except Exception, CatchableError: - let msg = - "Exception " & eventName & " when calling 'eventCallBack': " & - getCurrentExceptionMsg() - cast[WakuCallBack](ctx[].eventCallback)( - RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), ctx[].eventUserData - ) - -proc onConnectionChange*(ctx: ptr WakuContext): ConnectionChangeHandler = - return proc(peerId: PeerId, peerEvent: PeerEventKind) {.async.} = - callEventCallback(ctx, "onConnectionChange"): - $JsonConnectionChangeEvent.new($peerId, peerEvent) - -proc onReceivedMessage*(ctx: ptr WakuContext): WakuRelayHandler = - return proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async.} = - callEventCallback(ctx, "onReceivedMessage"): - $JsonMessageEvent.new(pubsubTopic, msg) - -proc onTopicHealthChange*(ctx: ptr WakuContext): TopicHealthChangeHandler = - return proc(pubsubTopic: PubsubTopic, topicHealth: TopicHealth) {.async.} = - callEventCallback(ctx, "onTopicHealthChange"): - $JsonTopicHealthChangeEvent.new(pubsubTopic, topicHealth) - -proc onWakuNotResponding*(ctx: ptr WakuContext) = - callEventCallback(ctx, "onWakuNotResponsive"): - $JsonWakuNotRespondingEvent.new() - -proc sendRequestToWakuThread*( - ctx: ptr WakuContext, - reqType: RequestType, - reqContent: pointer, - callback: WakuCallBack, - userData: pointer, - timeout = InfiniteDuration, -): Result[void, string] = - ctx.lock.acquire() - # This lock is only necessary while we use a SP Channel and while the signalling - # between threads assumes that there aren't concurrent requests. - # Rearchitecting the signaling + migrating to a MP Channel will allow us to receive - # requests concurrently and spare us the need of locks - defer: - ctx.lock.release() - - let req = WakuThreadRequest.createShared(reqType, reqContent, callback, userData) - ## Sending the request - let sentOk = ctx.reqChannel.trySend(req) - if not sentOk: - deallocShared(req) - return err("Couldn't send a request to the waku thread: " & $req[]) - - let fireSyncRes = ctx.reqSignal.fireSync() - if fireSyncRes.isErr(): - deallocShared(req) - return err("failed fireSync: " & $fireSyncRes.error) - - if fireSyncRes.get() == false: - deallocShared(req) - return err("Couldn't fireSync in time") - - ## wait until the Waku Thread properly received the request - let res = ctx.reqReceivedSignal.waitSync(timeout) - if res.isErr(): - deallocShared(req) - return err("Couldn't receive reqReceivedSignal signal") - - ## Notice that in case of "ok", the deallocShared(req) is performed by the Waku Thread in the - ## process proc. See the 'waku_thread_request.nim' module for more details. - ok() - -proc watchdogThreadBody(ctx: ptr WakuContext) {.thread.} = - ## Watchdog thread that monitors the Waku thread and notifies the library user if it hangs. - - let watchdogRun = proc(ctx: ptr WakuContext) {.async.} = - const WatchdogStartDelay = 10.seconds - const WatchdogTimeinterval = 1.seconds - const WakuNotRespondingTimeout = 3.seconds - - # Give time for the node to be created and up before sending watchdog requests - await sleepAsync(WatchdogStartDelay) - while true: - await sleepAsync(WatchdogTimeinterval) - - if ctx.running.load == false: - info "Watchdog thread exiting because WakuContext is not running" - break - - let wakuCallback = proc( - callerRet: cint, msg: ptr cchar, len: csize_t, userData: pointer - ) {.cdecl, gcsafe, raises: [].} = - discard ## Don't do anything. Just respecting the callback signature. - const nilUserData = nil - - trace "Sending watchdog request to Waku thread" - - sendRequestToWakuThread( - ctx, - RequestType.DEBUG, - DebugNodeRequest.createShared(DebugNodeMsgType.CHECK_WAKU_NOT_BLOCKED), - wakuCallback, - nilUserData, - WakuNotRespondingTimeout, - ).isOkOr: - error "Failed to send watchdog request to Waku thread", error = $error - onWakuNotResponding(ctx) - - waitFor watchdogRun(ctx) - -proc wakuThreadBody(ctx: ptr WakuContext) {.thread.} = - ## Waku thread that attends library user requests (stop, connect_to, etc.) - - logging.setupLog(logging.LogLevel.DEBUG, logging.LogFormat.TEXT) - - let wakuRun = proc(ctx: ptr WakuContext) {.async.} = - var waku: Waku - while true: - await ctx.reqSignal.wait() - - if ctx.running.load == false: - break - - ## Trying to get a request from the libwaku requestor thread - var request: ptr WakuThreadRequest - let recvOk = ctx.reqChannel.tryRecv(request) - if not recvOk: - error "waku thread could not receive a request" - continue - - ## Handle the request - asyncSpawn WakuThreadRequest.process(request, addr waku) - - let fireRes = ctx.reqReceivedSignal.fireSync() - if fireRes.isErr(): - error "could not fireSync back to requester thread", error = fireRes.error - - waitFor wakuRun(ctx) - -proc createWakuContext*(): Result[ptr WakuContext, string] = - ## This proc is called from the main thread and it creates - ## the Waku working thread. - var ctx = createShared(WakuContext, 1) - ctx.reqSignal = ThreadSignalPtr.new().valueOr: - return err("couldn't create reqSignal ThreadSignalPtr") - ctx.reqReceivedSignal = ThreadSignalPtr.new().valueOr: - return err("couldn't create reqReceivedSignal ThreadSignalPtr") - ctx.lock.initLock() - - ctx.running.store(true) - - try: - createThread(ctx.wakuThread, wakuThreadBody, ctx) - except ValueError, ResourceExhaustedError: - freeShared(ctx) - return err("failed to create the Waku thread: " & getCurrentExceptionMsg()) - - try: - createThread(ctx.watchdogThread, watchdogThreadBody, ctx) - except ValueError, ResourceExhaustedError: - freeShared(ctx) - return err("failed to create the watchdog thread: " & getCurrentExceptionMsg()) - - return ok(ctx) - -proc destroyWakuContext*(ctx: ptr WakuContext): Result[void, string] = - ctx.running.store(false) - - let signaledOnTime = ctx.reqSignal.fireSync().valueOr: - return err("error in destroyWakuContext: " & $error) - if not signaledOnTime: - return err("failed to signal reqSignal on time in destroyWakuContext") - - joinThread(ctx.wakuThread) - joinThread(ctx.watchdogThread) - ctx.lock.deinitLock() - ?ctx.reqSignal.close() - ?ctx.reqReceivedSignal.close() - freeShared(ctx) - - return ok() diff --git a/library/waku_thread_requests/requests/debug_node_request.nim b/library/waku_thread_requests/requests/debug_node_request.nim deleted file mode 100644 index c9aa5a743..000000000 --- a/library/waku_thread_requests/requests/debug_node_request.nim +++ /dev/null @@ -1,63 +0,0 @@ -import std/json -import - chronicles, - chronos, - results, - eth/p2p/discoveryv5/enr, - strutils, - libp2p/peerid, - metrics -import - ../../../waku/factory/waku, - ../../../waku/node/waku_node, - ../../../waku/node/health_monitor - -type DebugNodeMsgType* = enum - RETRIEVE_LISTENING_ADDRESSES - RETRIEVE_MY_ENR - RETRIEVE_MY_PEER_ID - RETRIEVE_METRICS - RETRIEVE_ONLINE_STATE - CHECK_WAKU_NOT_BLOCKED - -type DebugNodeRequest* = object - operation: DebugNodeMsgType - -proc createShared*(T: type DebugNodeRequest, op: DebugNodeMsgType): ptr type T = - var ret = createShared(T) - ret[].operation = op - return ret - -proc destroyShared(self: ptr DebugNodeRequest) = - deallocShared(self) - -proc getMultiaddresses(node: WakuNode): seq[string] = - return node.info().listenAddresses - -proc getMetrics(): string = - {.gcsafe.}: - return defaultRegistry.toText() ## defaultRegistry is {.global.} in metrics module - -proc process*( - self: ptr DebugNodeRequest, waku: Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of RETRIEVE_LISTENING_ADDRESSES: - ## returns a comma-separated string of the listen addresses - return ok(waku.node.getMultiaddresses().join(",")) - of RETRIEVE_MY_ENR: - return ok(waku.node.enr.toURI()) - of RETRIEVE_MY_PEER_ID: - return ok($waku.node.peerId()) - of RETRIEVE_METRICS: - return ok(getMetrics()) - of RETRIEVE_ONLINE_STATE: - return ok($waku.healthMonitor.onlineMonitor.amIOnline()) - of CHECK_WAKU_NOT_BLOCKED: - return ok("waku thread is not blocked") - - error "unsupported operation in DebugNodeRequest" - return err("unsupported operation in DebugNodeRequest") diff --git a/library/waku_thread_requests/requests/discovery_request.nim b/library/waku_thread_requests/requests/discovery_request.nim deleted file mode 100644 index 6f6780a2f..000000000 --- a/library/waku_thread_requests/requests/discovery_request.nim +++ /dev/null @@ -1,151 +0,0 @@ -import std/json -import chronos, chronicles, results, strutils, libp2p/multiaddress -import - ../../../waku/factory/waku, - ../../../waku/discovery/waku_dnsdisc, - ../../../waku/discovery/waku_discv5, - ../../../waku/waku_core/peers, - ../../../waku/node/waku_node, - ../../../waku/node/api, - ../../alloc - -type DiscoveryMsgType* = enum - GET_BOOTSTRAP_NODES - UPDATE_DISCV5_BOOTSTRAP_NODES - START_DISCV5 - STOP_DISCV5 - PEER_EXCHANGE - -type DiscoveryRequest* = object - operation: DiscoveryMsgType - - ## used in GET_BOOTSTRAP_NODES - enrTreeUrl: cstring - nameDnsServer: cstring - timeoutMs: cint - - ## used in UPDATE_DISCV5_BOOTSTRAP_NODES - nodes: cstring - - ## used in PEER_EXCHANGE - numPeers: uint64 - -proc createShared( - T: type DiscoveryRequest, - op: DiscoveryMsgType, - enrTreeUrl: cstring, - nameDnsServer: cstring, - timeoutMs: cint, - nodes: cstring, - numPeers: uint64, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].enrTreeUrl = enrTreeUrl.alloc() - ret[].nameDnsServer = nameDnsServer.alloc() - ret[].timeoutMs = timeoutMs - ret[].nodes = nodes.alloc() - ret[].numPeers = numPeers - return ret - -proc createRetrieveBootstrapNodesRequest*( - T: type DiscoveryRequest, - op: DiscoveryMsgType, - enrTreeUrl: cstring, - nameDnsServer: cstring, - timeoutMs: cint, -): ptr type T = - return T.createShared(op, enrTreeUrl, nameDnsServer, timeoutMs, "", 0) - -proc createUpdateBootstrapNodesRequest*( - T: type DiscoveryRequest, op: DiscoveryMsgType, nodes: cstring -): ptr type T = - return T.createShared(op, "", "", 0, nodes, 0) - -proc createDiscV5StartRequest*(T: type DiscoveryRequest): ptr type T = - return T.createShared(START_DISCV5, "", "", 0, "", 0) - -proc createDiscV5StopRequest*(T: type DiscoveryRequest): ptr type T = - return T.createShared(STOP_DISCV5, "", "", 0, "", 0) - -proc createPeerExchangeRequest*( - T: type DiscoveryRequest, numPeers: uint64 -): ptr type T = - return T.createShared(PEER_EXCHANGE, "", "", 0, "", numPeers) - -proc destroyShared(self: ptr DiscoveryRequest) = - deallocShared(self[].enrTreeUrl) - deallocShared(self[].nameDnsServer) - deallocShared(self[].nodes) - deallocShared(self) - -proc retrieveBootstrapNodes( - enrTreeUrl: string, ipDnsServer: string -): Future[Result[seq[string], string]] {.async.} = - let dnsNameServers = @[parseIpAddress(ipDnsServer)] - let discoveredPeers: seq[RemotePeerInfo] = ( - await retrieveDynamicBootstrapNodes(enrTreeUrl, dnsNameServers) - ).valueOr: - return err("failed discovering peers from DNS: " & $error) - - var multiAddresses = newSeq[string]() - - for discPeer in discoveredPeers: - for address in discPeer.addrs: - multiAddresses.add($address & "/p2p/" & $discPeer) - - return ok(multiAddresses) - -proc updateDiscv5BootstrapNodes(nodes: string, waku: ptr Waku): Result[void, string] = - waku.wakuDiscv5.updateBootstrapRecords(nodes).isOkOr: - return err("error in updateDiscv5BootstrapNodes: " & $error) - return ok() - -proc performPeerExchangeRequestTo( - numPeers: uint64, waku: ptr Waku -): Future[Result[int, string]] {.async.} = - let numPeersRecv = (await waku.node.fetchPeerExchangePeers(numPeers)).valueOr: - return err($error) - return ok(numPeersRecv) - -proc process*( - self: ptr DiscoveryRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of START_DISCV5: - let res = await waku.wakuDiscv5.start() - res.isOkOr: - error "START_DISCV5 failed", error = error - return err($error) - - return ok("discv5 started correctly") - of STOP_DISCV5: - await waku.wakuDiscv5.stop() - - return ok("discv5 stopped correctly") - of GET_BOOTSTRAP_NODES: - let nodes = ( - await retrieveBootstrapNodes($self[].enrTreeUrl, $self[].nameDnsServer) - ).valueOr: - error "GET_BOOTSTRAP_NODES failed", error = error - return err($error) - - ## returns a comma-separated string of bootstrap nodes' multiaddresses - return ok(nodes.join(",")) - of UPDATE_DISCV5_BOOTSTRAP_NODES: - updateDiscv5BootstrapNodes($self[].nodes, waku).isOkOr: - error "UPDATE_DISCV5_BOOTSTRAP_NODES failed", error = error - return err($error) - - return ok("discovery request processed correctly") - of PEER_EXCHANGE: - let numValidPeers = (await performPeerExchangeRequestTo(self[].numPeers, waku)).valueOr: - error "PEER_EXCHANGE failed", error = error - return err($error) - return ok($numValidPeers) - - error "discovery request not handled" - return err("discovery request not handled") diff --git a/library/waku_thread_requests/requests/peer_manager_request.nim b/library/waku_thread_requests/requests/peer_manager_request.nim deleted file mode 100644 index cac5ca30e..000000000 --- a/library/waku_thread_requests/requests/peer_manager_request.nim +++ /dev/null @@ -1,135 +0,0 @@ -import std/[sequtils, strutils, tables] -import chronicles, chronos, results, options, json -import - ../../../waku/factory/waku, - ../../../waku/node/waku_node, - ../../alloc, - ../../../waku/node/peer_manager - -type PeerManagementMsgType* {.pure.} = enum - CONNECT_TO - GET_ALL_PEER_IDS - GET_CONNECTED_PEERS_INFO - GET_PEER_IDS_BY_PROTOCOL - DISCONNECT_PEER_BY_ID - DISCONNECT_ALL_PEERS - DIAL_PEER - DIAL_PEER_BY_ID - GET_CONNECTED_PEERS - -type PeerManagementRequest* = object - operation: PeerManagementMsgType - peerMultiAddr: cstring - dialTimeout: Duration - protocol: cstring - peerId: cstring - -type PeerInfo = object - protocols: seq[string] - addresses: seq[string] - -proc createShared*( - T: type PeerManagementRequest, - op: PeerManagementMsgType, - peerMultiAddr = "", - dialTimeout = chronos.milliseconds(0), ## arbitrary Duration as not all ops needs dialTimeout - peerId = "", - protocol = "", -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].peerMultiAddr = peerMultiAddr.alloc() - ret[].peerId = peerId.alloc() - ret[].protocol = protocol.alloc() - ret[].dialTimeout = dialTimeout - return ret - -proc destroyShared(self: ptr PeerManagementRequest) = - if not isNil(self[].peerMultiAddr): - deallocShared(self[].peerMultiAddr) - - if not isNil(self[].peerId): - deallocShared(self[].peerId) - - if not isNil(self[].protocol): - deallocShared(self[].protocol) - - deallocShared(self) - -proc process*( - self: ptr PeerManagementRequest, waku: Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of CONNECT_TO: - let peers = ($self[].peerMultiAddr).split(",").mapIt(strip(it)) - await waku.node.connectToNodes(peers, source = "static") - return ok("") - of GET_ALL_PEER_IDS: - ## returns a comma-separated string of peerIDs - let peerIDs = - waku.node.peerManager.switch.peerStore.peers().mapIt($it.peerId).join(",") - return ok(peerIDs) - of GET_CONNECTED_PEERS_INFO: - ## returns a JSON string mapping peerIDs to objects with protocols and addresses - - var peersMap = initTable[string, PeerInfo]() - let peers = waku.node.peerManager.switch.peerStore.peers().filterIt( - it.connectedness == Connected - ) - - # Build a map of peer IDs to peer info objects - for peer in peers: - let peerIdStr = $peer.peerId - peersMap[peerIdStr] = - PeerInfo(protocols: peer.protocols, addresses: peer.addrs.mapIt($it)) - - # Convert the map to JSON string - let jsonObj = %*peersMap - let jsonStr = $jsonObj - return ok(jsonStr) - of GET_PEER_IDS_BY_PROTOCOL: - ## returns a comma-separated string of peerIDs that mount the given protocol - let connectedPeers = waku.node.peerManager.switch.peerStore - .peers($self[].protocol) - .filterIt(it.connectedness == Connected) - .mapIt($it.peerId) - .join(",") - return ok(connectedPeers) - of DISCONNECT_PEER_BY_ID: - let peerId = PeerId.init($self[].peerId).valueOr: - error "DISCONNECT_PEER_BY_ID failed", error = $error - return err($error) - await waku.node.peerManager.disconnectNode(peerId) - return ok("") - of DISCONNECT_ALL_PEERS: - await waku.node.peerManager.disconnectAllPeers() - return ok("") - of DIAL_PEER: - let remotePeerInfo = parsePeerInfo($self[].peerMultiAddr).valueOr: - error "DIAL_PEER failed", error = $error - return err($error) - let conn = await waku.node.peerManager.dialPeer(remotePeerInfo, $self[].protocol) - if conn.isNone(): - let msg = "failed dialing peer" - error "DIAL_PEER failed", error = msg, peerId = $remotePeerInfo.peerId - return err(msg) - of DIAL_PEER_BY_ID: - let peerId = PeerId.init($self[].peerId).valueOr: - error "DIAL_PEER_BY_ID failed", error = $error - return err($error) - let conn = await waku.node.peerManager.dialPeer(peerId, $self[].protocol) - if conn.isNone(): - let msg = "failed dialing peer" - error "DIAL_PEER_BY_ID failed", error = msg, peerId = $peerId - return err(msg) - of GET_CONNECTED_PEERS: - ## returns a comma-separated string of peerIDs - let - (inPeerIds, outPeerIds) = waku.node.peerManager.connectedPeers() - connectedPeerids = concat(inPeerIds, outPeerIds) - return ok(connectedPeerids.mapIt($it).join(",")) - - return ok("") diff --git a/library/waku_thread_requests/requests/ping_request.nim b/library/waku_thread_requests/requests/ping_request.nim deleted file mode 100644 index 53d33968e..000000000 --- a/library/waku_thread_requests/requests/ping_request.nim +++ /dev/null @@ -1,56 +0,0 @@ -import std/[json, strutils] -import chronos, results -import libp2p/[protocols/ping, switch, multiaddress, multicodec] -import ../../../waku/[factory/waku, waku_core/peers, node/waku_node], ../../alloc - -type PingRequest* = object - peerAddr: cstring - timeout: Duration - -proc createShared*( - T: type PingRequest, peerAddr: cstring, timeout: Duration -): ptr type T = - var ret = createShared(T) - ret[].peerAddr = peerAddr.alloc() - ret[].timeout = timeout - return ret - -proc destroyShared(self: ptr PingRequest) = - deallocShared(self[].peerAddr) - deallocShared(self) - -proc process*( - self: ptr PingRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - let peerInfo = peers.parsePeerInfo(($self[].peerAddr).split(",")).valueOr: - return err("PingRequest failed to parse peer addr: " & $error) - - proc ping(): Future[Result[Duration, string]] {.async, gcsafe.} = - try: - let conn = await waku.node.switch.dial(peerInfo.peerId, peerInfo.addrs, PingCodec) - defer: - await conn.close() - - let pingRTT = await waku.node.libp2pPing.ping(conn) - if pingRTT == 0.nanos: - return err("could not ping peer: rtt-0") - return ok(pingRTT) - except CatchableError: - return err("could not ping peer: " & getCurrentExceptionMsg()) - - let pingFuture = ping() - let pingRTT: Duration = - if self[].timeout == chronos.milliseconds(0): # No timeout expected - (await pingFuture).valueOr: - return err(error) - else: - let timedOut = not (await pingFuture.withTimeout(self[].timeout)) - if timedOut: - return err("ping timed out") - pingFuture.read().valueOr: - return err(error) - - ok($(pingRTT.nanos)) diff --git a/library/waku_thread_requests/requests/protocols/filter_request.nim b/library/waku_thread_requests/requests/protocols/filter_request.nim deleted file mode 100644 index c0a99f1f9..000000000 --- a/library/waku_thread_requests/requests/protocols/filter_request.nim +++ /dev/null @@ -1,106 +0,0 @@ -import options, std/[strutils, sequtils] -import chronicles, chronos, results -import - ../../../../waku/waku_filter_v2/client, - ../../../../waku/waku_core/message/message, - ../../../../waku/factory/waku, - ../../../../waku/waku_filter_v2/common, - ../../../../waku/waku_core/subscription/push_handler, - ../../../../waku/node/peer_manager/peer_manager, - ../../../../waku/node/waku_node, - ../../../../waku/node/api, - ../../../../waku/waku_core/topics/pubsub_topic, - ../../../../waku/waku_core/topics/content_topic, - ../../../alloc - -type FilterMsgType* = enum - SUBSCRIBE - UNSUBSCRIBE - UNSUBSCRIBE_ALL - -type FilterRequest* = object - operation: FilterMsgType - pubsubTopic: cstring - contentTopics: cstring ## comma-separated list of content-topics - filterPushEventCallback: FilterPushHandler ## handles incoming filter pushed msgs - -proc createShared*( - T: type FilterRequest, - op: FilterMsgType, - pubsubTopic: cstring = "", - contentTopics: cstring = "", - filterPushEventCallback: FilterPushHandler = nil, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].pubsubTopic = pubsubTopic.alloc() - ret[].contentTopics = contentTopics.alloc() - ret[].filterPushEventCallback = filterPushEventCallback - - return ret - -proc destroyShared(self: ptr FilterRequest) = - deallocShared(self[].pubsubTopic) - deallocShared(self[].contentTopics) - deallocShared(self) - -proc process*( - self: ptr FilterRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - const FilterOpTimeout = 5.seconds - if waku.node.wakuFilterClient.isNil(): - let errorMsg = "FilterRequest waku.node.wakuFilterClient is nil" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - case self.operation - of SUBSCRIBE: - waku.node.wakuFilterClient.registerPushHandler(self.filterPushEventCallback) - - let peer = waku.node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: - let errorMsg = - "could not find peer with WakuFilterSubscribeCodec when subscribing" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - let pubsubTopic = some(PubsubTopic($self[].pubsubTopic)) - let contentTopics = ($(self[].contentTopics)).split(",").mapIt(ContentTopic(it)) - - let subFut = waku.node.filterSubscribe(pubsubTopic, contentTopics, peer) - if not await subFut.withTimeout(FilterOpTimeout): - let errorMsg = "filter subscription timed out" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - of UNSUBSCRIBE: - let peer = waku.node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: - let errorMsg = - "could not find peer with WakuFilterSubscribeCodec when unsubscribing" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - let pubsubTopic = some(PubsubTopic($self[].pubsubTopic)) - let contentTopics = ($(self[].contentTopics)).split(",").mapIt(ContentTopic(it)) - - let subFut = waku.node.filterUnsubscribe(pubsubTopic, contentTopics, peer) - if not await subFut.withTimeout(FilterOpTimeout): - let errorMsg = "filter un-subscription timed out" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - of UNSUBSCRIBE_ALL: - let peer = waku.node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: - let errorMsg = - "could not find peer with WakuFilterSubscribeCodec when unsubscribing all" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - let unsubFut = waku.node.filterUnsubscribeAll(peer) - - if not await unsubFut.withTimeout(FilterOpTimeout): - let errorMsg = "filter un-subscription all timed out" - error "fail filter process", error = errorMsg, op = $(self.operation) - return err(errorMsg) - - return ok("") diff --git a/library/waku_thread_requests/requests/protocols/lightpush_request.nim b/library/waku_thread_requests/requests/protocols/lightpush_request.nim deleted file mode 100644 index bc3d9de2c..000000000 --- a/library/waku_thread_requests/requests/protocols/lightpush_request.nim +++ /dev/null @@ -1,109 +0,0 @@ -import options -import chronicles, chronos, results -import - ../../../../waku/waku_core/message/message, - ../../../../waku/waku_core/codecs, - ../../../../waku/factory/waku, - ../../../../waku/waku_core/message, - ../../../../waku/waku_core/time, # Timestamp - ../../../../waku/waku_core/topics/pubsub_topic, - ../../../../waku/waku_lightpush_legacy/client, - ../../../../waku/waku_lightpush_legacy/common, - ../../../../waku/node/peer_manager/peer_manager, - ../../../alloc - -type LightpushMsgType* = enum - PUBLISH - -type ThreadSafeWakuMessage* = object - payload: SharedSeq[byte] - contentTopic: cstring - meta: SharedSeq[byte] - version: uint32 - timestamp: Timestamp - ephemeral: bool - when defined(rln): - proof: SharedSeq[byte] - -type LightpushRequest* = object - operation: LightpushMsgType - pubsubTopic: cstring - message: ThreadSafeWakuMessage # only used in 'PUBLISH' requests - -proc createShared*( - T: type LightpushRequest, - op: LightpushMsgType, - pubsubTopic: cstring, - m = WakuMessage(), -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].pubsubTopic = pubsubTopic.alloc() - ret[].message = ThreadSafeWakuMessage( - payload: allocSharedSeq(m.payload), - contentTopic: m.contentTopic.alloc(), - meta: allocSharedSeq(m.meta), - version: m.version, - timestamp: m.timestamp, - ephemeral: m.ephemeral, - ) - when defined(rln): - ret[].message.proof = allocSharedSeq(m.proof) - - return ret - -proc destroyShared(self: ptr LightpushRequest) = - deallocSharedSeq(self[].message.payload) - deallocShared(self[].message.contentTopic) - deallocSharedSeq(self[].message.meta) - when defined(rln): - deallocSharedSeq(self[].message.proof) - - deallocShared(self) - -proc toWakuMessage(m: ThreadSafeWakuMessage): WakuMessage = - var wakuMessage = WakuMessage() - - wakuMessage.payload = m.payload.toSeq() - wakuMessage.contentTopic = $m.contentTopic - wakuMessage.meta = m.meta.toSeq() - wakuMessage.version = m.version - wakuMessage.timestamp = m.timestamp - wakuMessage.ephemeral = m.ephemeral - - when defined(rln): - wakuMessage.proof = m.proof - - return wakuMessage - -proc process*( - self: ptr LightpushRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - case self.operation - of PUBLISH: - let msg = self.message.toWakuMessage() - let pubsubTopic = $self.pubsubTopic - - if waku.node.wakuLightpushClient.isNil(): - let errorMsg = "LightpushRequest waku.node.wakuLightpushClient is nil" - error "PUBLISH failed", error = errorMsg - return err(errorMsg) - - let peerOpt = waku.node.peerManager.selectPeer(WakuLightPushCodec) - if peerOpt.isNone(): - let errorMsg = "failed to lightpublish message, no suitable remote peers" - error "PUBLISH failed", error = errorMsg - return err(errorMsg) - - let msgHashHex = ( - await waku.node.wakuLegacyLightpushClient.publish( - pubsubTopic, msg, peer = peerOpt.get() - ) - ).valueOr: - error "PUBLISH failed", error = error - return err($error) - - return ok(msgHashHex) diff --git a/library/waku_thread_requests/requests/protocols/relay_request.nim b/library/waku_thread_requests/requests/protocols/relay_request.nim deleted file mode 100644 index 5c0732768..000000000 --- a/library/waku_thread_requests/requests/protocols/relay_request.nim +++ /dev/null @@ -1,166 +0,0 @@ -import std/[net, sequtils, strutils] -import chronicles, chronos, stew/byteutils, results -import - ../../../../waku/waku_core/message/message, - ../../../../waku/factory/[validator_signed, waku], - ../../../../tools/confutils/cli_args, - ../../../../waku/waku_node, - ../../../../waku/waku_core/message, - ../../../../waku/waku_core/time, # Timestamp - ../../../../waku/waku_core/topics/pubsub_topic, - ../../../../waku/waku_core/topics, - ../../../../waku/waku_relay/protocol, - ../../../../waku/node/peer_manager, - ../../../alloc - -type RelayMsgType* = enum - SUBSCRIBE - UNSUBSCRIBE - PUBLISH - NUM_CONNECTED_PEERS - LIST_CONNECTED_PEERS - ## to return the list of all connected peers to an specific pubsub topic - NUM_MESH_PEERS - LIST_MESH_PEERS - ## to return the list of only the peers that conform the mesh for a particular pubsub topic - ADD_PROTECTED_SHARD ## Protects a shard with a public key - -type ThreadSafeWakuMessage* = object - payload: SharedSeq[byte] - contentTopic: cstring - meta: SharedSeq[byte] - version: uint32 - timestamp: Timestamp - ephemeral: bool - when defined(rln): - proof: SharedSeq[byte] - -type RelayRequest* = object - operation: RelayMsgType - pubsubTopic: cstring - relayEventCallback: WakuRelayHandler # not used in 'PUBLISH' requests - message: ThreadSafeWakuMessage # only used in 'PUBLISH' requests - clusterId: cint # only used in 'ADD_PROTECTED_SHARD' requests - shardId: cint # only used in 'ADD_PROTECTED_SHARD' requests - publicKey: cstring # only used in 'ADD_PROTECTED_SHARD' requests - -proc createShared*( - T: type RelayRequest, - op: RelayMsgType, - pubsubTopic: cstring = nil, - relayEventCallback: WakuRelayHandler = nil, - m = WakuMessage(), - clusterId: cint = 0, - shardId: cint = 0, - publicKey: cstring = nil, -): ptr type T = - var ret = createShared(T) - ret[].operation = op - ret[].pubsubTopic = pubsubTopic.alloc() - ret[].clusterId = clusterId - ret[].shardId = shardId - ret[].publicKey = publicKey.alloc() - ret[].relayEventCallback = relayEventCallback - ret[].message = ThreadSafeWakuMessage( - payload: allocSharedSeq(m.payload), - contentTopic: m.contentTopic.alloc(), - meta: allocSharedSeq(m.meta), - version: m.version, - timestamp: m.timestamp, - ephemeral: m.ephemeral, - ) - when defined(rln): - ret[].message.proof = allocSharedSeq(m.proof) - - return ret - -proc destroyShared(self: ptr RelayRequest) = - deallocSharedSeq(self[].message.payload) - deallocShared(self[].message.contentTopic) - deallocSharedSeq(self[].message.meta) - when defined(rln): - deallocSharedSeq(self[].message.proof) - deallocShared(self[].pubsubTopic) - deallocShared(self[].publicKey) - deallocShared(self) - -proc toWakuMessage(m: ThreadSafeWakuMessage): WakuMessage = - var wakuMessage = WakuMessage() - - wakuMessage.payload = m.payload.toSeq() - wakuMessage.contentTopic = $m.contentTopic - wakuMessage.meta = m.meta.toSeq() - wakuMessage.version = m.version - wakuMessage.timestamp = m.timestamp - wakuMessage.ephemeral = m.ephemeral - - when defined(rln): - wakuMessage.proof = m.proof - - return wakuMessage - -proc process*( - self: ptr RelayRequest, waku: ptr Waku -): Future[Result[string, string]] {.async.} = - defer: - destroyShared(self) - - if waku.node.wakuRelay.isNil(): - return err("Operation not supported without Waku Relay enabled.") - - case self.operation - of SUBSCRIBE: - waku.node.subscribe( - (kind: SubscriptionKind.PubsubSub, topic: $self.pubsubTopic), - handler = self.relayEventCallback, - ).isOkOr: - error "SUBSCRIBE failed", error - return err($error) - of UNSUBSCRIBE: - waku.node.unsubscribe((kind: SubscriptionKind.PubsubSub, topic: $self.pubsubTopic)).isOkOr: - error "UNSUBSCRIBE failed", error - return err($error) - of PUBLISH: - let msg = self.message.toWakuMessage() - let pubsubTopic = $self.pubsubTopic - - (await waku.node.wakuRelay.publish(pubsubTopic, msg)).isOkOr: - error "PUBLISH failed", error - return err($error) - - let msgHash = computeMessageHash(pubSubTopic, msg).to0xHex - return ok(msgHash) - of NUM_CONNECTED_PEERS: - let numConnPeers = waku.node.wakuRelay.getNumConnectedPeers($self.pubsubTopic).valueOr: - error "NUM_CONNECTED_PEERS failed", error - return err($error) - return ok($numConnPeers) - of LIST_CONNECTED_PEERS: - let connPeers = waku.node.wakuRelay.getConnectedPeers($self.pubsubTopic).valueOr: - error "LIST_CONNECTED_PEERS failed", error = error - return err($error) - ## returns a comma-separated string of peerIDs - return ok(connPeers.mapIt($it).join(",")) - of NUM_MESH_PEERS: - let numPeersInMesh = waku.node.wakuRelay.getNumPeersInMesh($self.pubsubTopic).valueOr: - error "NUM_MESH_PEERS failed", error = error - return err($error) - return ok($numPeersInMesh) - of LIST_MESH_PEERS: - let meshPeers = waku.node.wakuRelay.getPeersInMesh($self.pubsubTopic).valueOr: - error "LIST_MESH_PEERS failed", error = error - return err($error) - ## returns a comma-separated string of peerIDs - return ok(meshPeers.mapIt($it).join(",")) - of ADD_PROTECTED_SHARD: - try: - let relayShard = - RelayShard(clusterId: uint16(self.clusterId), shardId: uint16(self.shardId)) - let protectedShard = - ProtectedShard.parseCmdArg($relayShard & ":" & $self.publicKey) - waku.node.wakuRelay.addSignedShardsValidator( - @[protectedShard], uint16(self.clusterId) - ) - except ValueError: - return err(getCurrentExceptionMsg()) - return ok("") diff --git a/library/waku_thread_requests/waku_thread_request.nim b/library/waku_thread_requests/waku_thread_request.nim deleted file mode 100644 index 50462fba7..000000000 --- a/library/waku_thread_requests/waku_thread_request.nim +++ /dev/null @@ -1,104 +0,0 @@ -## This file contains the base message request type that will be handled. -## The requests are created by the main thread and processed by -## the Waku Thread. - -import std/json, results -import chronos, chronos/threadsync -import - ../../waku/factory/waku, - ../ffi_types, - ./requests/node_lifecycle_request, - ./requests/peer_manager_request, - ./requests/protocols/relay_request, - ./requests/protocols/store_request, - ./requests/protocols/lightpush_request, - ./requests/protocols/filter_request, - ./requests/debug_node_request, - ./requests/discovery_request, - ./requests/ping_request - -type RequestType* {.pure.} = enum - LIFECYCLE - PEER_MANAGER - PING - RELAY - STORE - DEBUG - DISCOVERY - LIGHTPUSH - FILTER - -type WakuThreadRequest* = object - reqType: RequestType - reqContent: pointer - callback: WakuCallBack - userData: pointer - -proc createShared*( - T: type WakuThreadRequest, - reqType: RequestType, - reqContent: pointer, - callback: WakuCallBack, - userData: pointer, -): ptr type T = - var ret = createShared(T) - ret[].reqType = reqType - ret[].reqContent = reqContent - ret[].callback = callback - ret[].userData = userData - return ret - -proc handleRes[T: string | void]( - res: Result[T, string], request: ptr WakuThreadRequest -) = - ## Handles the Result responses, which can either be Result[string, string] or - ## Result[void, string]. - - defer: - deallocShared(request) - - if res.isErr(): - foreignThreadGc: - let msg = "libwaku error: handleRes fireSyncRes error: " & $res.error - request[].callback( - RET_ERR, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData - ) - return - - foreignThreadGc: - var msg: cstring = "" - when T is string: - msg = res.get().cstring() - request[].callback( - RET_OK, unsafeAddr msg[0], cast[csize_t](len(msg)), request[].userData - ) - return - -proc process*( - T: type WakuThreadRequest, request: ptr WakuThreadRequest, waku: ptr Waku -) {.async.} = - let retFut = - case request[].reqType - of LIFECYCLE: - cast[ptr NodeLifecycleRequest](request[].reqContent).process(waku) - of PEER_MANAGER: - cast[ptr PeerManagementRequest](request[].reqContent).process(waku[]) - of PING: - cast[ptr PingRequest](request[].reqContent).process(waku) - of RELAY: - cast[ptr RelayRequest](request[].reqContent).process(waku) - of STORE: - cast[ptr StoreRequest](request[].reqContent).process(waku) - of DEBUG: - cast[ptr DebugNodeRequest](request[].reqContent).process(waku[]) - of DISCOVERY: - cast[ptr DiscoveryRequest](request[].reqContent).process(waku) - of LIGHTPUSH: - cast[ptr LightpushRequest](request[].reqContent).process(waku) - of FILTER: - cast[ptr FilterRequest](request[].reqContent).process(waku) - - handleRes(await retFut, request) - -proc `$`*(self: WakuThreadRequest): string = - return $self.reqType diff --git a/migrations/message_store_postgres/content_script_version_1.nim b/migrations/message_store_postgres/content_script_version_1.nim index 18133bdca..37c6bf2ec 100644 --- a/migrations/message_store_postgres/content_script_version_1.nim +++ b/migrations/message_store_postgres/content_script_version_1.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_1* = - """ +const ContentScriptVersion_1* = """ CREATE TABLE IF NOT EXISTS messages ( pubsubTopic VARCHAR NOT NULL, contentTopic VARCHAR NOT NULL, diff --git a/migrations/message_store_postgres/content_script_version_2.nim b/migrations/message_store_postgres/content_script_version_2.nim index 8c3656e64..4065a26c6 100644 --- a/migrations/message_store_postgres/content_script_version_2.nim +++ b/migrations/message_store_postgres/content_script_version_2.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_2* = - """ +const ContentScriptVersion_2* = """ ALTER TABLE IF EXISTS messages_backup RENAME TO messages; ALTER TABLE messages RENAME TO messages_backup; ALTER TABLE messages_backup DROP CONSTRAINT messageIndex; diff --git a/migrations/message_store_postgres/content_script_version_3.nim b/migrations/message_store_postgres/content_script_version_3.nim index 2938087cc..22e7308aa 100644 --- a/migrations/message_store_postgres/content_script_version_3.nim +++ b/migrations/message_store_postgres/content_script_version_3.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_3* = - """ +const ContentScriptVersion_3* = """ CREATE INDEX IF NOT EXISTS i_query ON messages (contentTopic, pubsubTopic, storedAt, id); diff --git a/migrations/message_store_postgres/content_script_version_4.nim b/migrations/message_store_postgres/content_script_version_4.nim index 50ee269f6..6412371e5 100644 --- a/migrations/message_store_postgres/content_script_version_4.nim +++ b/migrations/message_store_postgres/content_script_version_4.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_4* = - """ +const ContentScriptVersion_4* = """ ALTER TABLE messages ADD meta VARCHAR default null; CREATE INDEX IF NOT EXISTS i_query ON messages (contentTopic, pubsubTopic, storedAt, id); diff --git a/migrations/message_store_postgres/content_script_version_5.nim b/migrations/message_store_postgres/content_script_version_5.nim index a59b2da87..8210be4ac 100644 --- a/migrations/message_store_postgres/content_script_version_5.nim +++ b/migrations/message_store_postgres/content_script_version_5.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_5* = - """ +const ContentScriptVersion_5* = """ CREATE INDEX IF NOT EXISTS i_query_storedAt ON messages (storedAt, id); UPDATE version SET version = 5 WHERE version = 4; diff --git a/migrations/message_store_postgres/content_script_version_6.nim b/migrations/message_store_postgres/content_script_version_6.nim index 126ec6da1..88e92ed34 100644 --- a/migrations/message_store_postgres/content_script_version_6.nim +++ b/migrations/message_store_postgres/content_script_version_6.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_6* = - """ +const ContentScriptVersion_6* = """ -- we can drop the timestamp column because this data is also kept in the storedAt column ALTER TABLE messages DROP COLUMN timestamp; diff --git a/migrations/message_store_postgres/content_script_version_7.nim b/migrations/message_store_postgres/content_script_version_7.nim index 01d7ad84e..e2aba854d 100644 --- a/migrations/message_store_postgres/content_script_version_7.nim +++ b/migrations/message_store_postgres/content_script_version_7.nim @@ -1,5 +1,4 @@ -const ContentScriptVersion_7* = - """ +const ContentScriptVersion_7* = """ -- Create lookup table CREATE TABLE IF NOT EXISTS messages_lookup ( diff --git a/migrations/message_store_postgres/pg_migration_manager.nim b/migrations/message_store_postgres/pg_migration_manager.nim index 051ac9e79..89a443609 100644 --- a/migrations/message_store_postgres/pg_migration_manager.nim +++ b/migrations/message_store_postgres/pg_migration_manager.nim @@ -10,16 +10,15 @@ type MigrationScript* = object proc init*(T: type MigrationScript, targetVersion: int, scriptContent: string): T = return MigrationScript(targetVersion: targetVersion, scriptContent: scriptContent) -const PgMigrationScripts* = - @[ - MigrationScript(version: 1, scriptContent: ContentScriptVersion_1), - MigrationScript(version: 2, scriptContent: ContentScriptVersion_2), - MigrationScript(version: 3, scriptContent: ContentScriptVersion_3), - MigrationScript(version: 4, scriptContent: ContentScriptVersion_4), - MigrationScript(version: 5, scriptContent: ContentScriptVersion_5), - MigrationScript(version: 6, scriptContent: ContentScriptVersion_6), - MigrationScript(version: 7, scriptContent: ContentScriptVersion_7), - ] +const PgMigrationScripts* = @[ + MigrationScript(version: 1, scriptContent: ContentScriptVersion_1), + MigrationScript(version: 2, scriptContent: ContentScriptVersion_2), + MigrationScript(version: 3, scriptContent: ContentScriptVersion_3), + MigrationScript(version: 4, scriptContent: ContentScriptVersion_4), + MigrationScript(version: 5, scriptContent: ContentScriptVersion_5), + MigrationScript(version: 6, scriptContent: ContentScriptVersion_6), + MigrationScript(version: 7, scriptContent: ContentScriptVersion_7), +] proc getMigrationScripts*(currentVersion: int64, targetVersion: int64): seq[string] = var ret = newSeq[string]() diff --git a/nimble.lock b/nimble.lock new file mode 100644 index 000000000..7a36d72c4 --- /dev/null +++ b/nimble.lock @@ -0,0 +1,659 @@ +{ + "version": 2, + "packages": { + "nim": { + "version": "2.2.4", + "vcsRevision": "911e0dbb1f76de61fa0215ab1bb85af5334cc9a8", + "url": "https://github.com/nim-lang/Nim.git", + "downloadMethod": "git", + "dependencies": [], + "checksums": { + "sha1": "68bb85cbfb1832ce4db43943911b046c3af3caab" + } + }, + "unittest2": { + "version": "0.2.5", + "vcsRevision": "26f2ef3ae0ec72a2a75bfe557e02e88f6a31c189", + "url": "https://github.com/status-im/nim-unittest2", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "02bb3751ba9ddc3c17bfd89f2e41cb6bfb8fc0c9" + } + }, + "bearssl": { + "version": "0.2.8", + "vcsRevision": "22c6a76ce015bc07e011562bdcfc51d9446c1e82", + "url": "https://github.com/status-im/nim-bearssl", + "downloadMethod": "git", + "dependencies": [ + "nim", + "unittest2" + ], + "checksums": { + "sha1": "da4dd7ae96d536bdaf42dca9c85d7aed024b6a86" + } + }, + "bearssl_pkey_decoder": { + "version": "#21dd3710df9345ed2ad8bf8f882761e07863b8e0", + "vcsRevision": "21dd3710df9345ed2ad8bf8f882761e07863b8e0", + "url": "https://github.com/vacp2p/bearssl_pkey_decoder", + "downloadMethod": "git", + "dependencies": [ + "nim", + "bearssl" + ], + "checksums": { + "sha1": "21b42e2e6ddca6c875d3fc50f36a5115abf51714" + } + }, + "jwt": { + "version": "#18f8378de52b241f321c1f9ea905456e89b95c6f", + "vcsRevision": "18f8378de52b241f321c1f9ea905456e89b95c6f", + "url": "https://github.com/vacp2p/nim-jwt.git", + "downloadMethod": "git", + "dependencies": [ + "nim", + "bearssl", + "bearssl_pkey_decoder" + ], + "checksums": { + "sha1": "bcfd6fc9c5e10a52b87117219b7ab5c98136bc8e" + } + }, + "testutils": { + "version": "0.8.1", + "vcsRevision": "6ce5e5e2301ccbc04b09d27ff78741ff4d352b4d", + "url": "https://github.com/status-im/nim-testutils", + "downloadMethod": "git", + "dependencies": [ + "nim", + "unittest2" + ], + "checksums": { + "sha1": "96a11cf8b84fa9bd12d4a553afa1cc4b7f9df4e3" + } + }, + "db_connector": { + "version": "0.1.0", + "vcsRevision": "29450a2063970712422e1ab857695c12d80112a6", + "url": "https://github.com/nim-lang/db_connector", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "4f2e67d0e4b61af9ac5575509305660b473f01a4" + } + }, + "results": { + "version": "0.5.1", + "vcsRevision": "df8113dda4c2d74d460a8fa98252b0b771bf1f27", + "url": "https://github.com/arnetheduck/nim-results", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "a9c011f74bc9ed5c91103917b9f382b12e82a9e7" + } + }, + "nat_traversal": { + "version": "0.0.1", + "vcsRevision": "860e18c37667b5dd005b94c63264560c35d88004", + "url": "https://github.com/status-im/nim-nat-traversal", + "downloadMethod": "git", + "dependencies": [ + "nim", + "results" + ], + "checksums": { + "sha1": "1a376d3e710590ef2c48748a546369755f0a7c97" + } + }, + "stew": { + "version": "0.5.0", + "vcsRevision": "4382b18f04b3c43c8409bfcd6b62063773b2bbaa", + "url": "https://github.com/status-im/nim-stew", + "downloadMethod": "git", + "dependencies": [ + "nim", + "results", + "unittest2" + ], + "checksums": { + "sha1": "db22942939773ab7d5a0f2b2668c237240c67dd6" + } + }, + "zlib": { + "version": "0.1.0", + "vcsRevision": "e680f269fb01af2c34a2ba879ff281795a5258fe", + "url": "https://github.com/status-im/nim-zlib", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "results" + ], + "checksums": { + "sha1": "bbde4f5a97a84b450fef7d107461e5f35cf2b47f" + } + }, + "httputils": { + "version": "0.4.1", + "vcsRevision": "f142cb2e8bd812dd002a6493b6082827bb248592", + "url": "https://github.com/status-im/nim-http-utils", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "results", + "unittest2" + ], + "checksums": { + "sha1": "016774ab31c3afff9a423f7d80584905ee59c570" + } + }, + "chronos": { + "version": "4.2.2", + "vcsRevision": "45f43a9ad8bd8bcf5903b42f365c1c879bd54240", + "url": "https://github.com/status-im/nim-chronos", + "downloadMethod": "git", + "dependencies": [ + "nim", + "results", + "stew", + "bearssl", + "httputils", + "unittest2" + ], + "checksums": { + "sha1": "3a4c9477df8cef20a04e4f1b54a2d74fdfc2a3d0" + } + }, + "metrics": { + "version": "0.2.1", + "vcsRevision": "a1296caf3ebb5f30f51a5feae7749a30df2824c2", + "url": "https://github.com/status-im/nim-metrics", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "results", + "stew" + ], + "checksums": { + "sha1": "84bb09873d7677c06046f391c7b473cd2fcff8a2" + } + }, + "faststreams": { + "version": "0.5.0", + "vcsRevision": "ce27581a3e881f782f482cb66dc5b07a02bd615e", + "url": "https://github.com/status-im/nim-faststreams", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "unittest2" + ], + "checksums": { + "sha1": "ee61e507b805ae1df7ec936f03f2d101b0d72383" + } + }, + "snappy": { + "version": "0.1.0", + "vcsRevision": "00bfcef94f8ef6981df5d5b994897f6695badfb2", + "url": "https://github.com/status-im/nim-snappy", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "unittest2", + "results", + "stew" + ], + "checksums": { + "sha1": "e572d60d6a3178c5b1cde2400c51ad771812cd3d" + } + }, + "serialization": { + "version": "0.5.2", + "vcsRevision": "b0f2fa32960ea532a184394b0f27be37bd80248b", + "url": "https://github.com/status-im/nim-serialization", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "unittest2", + "stew" + ], + "checksums": { + "sha1": "fa35c1bb76a0a02a2379fe86eaae0957c7527cb8" + } + }, + "toml_serialization": { + "version": "0.2.18", + "vcsRevision": "b5b387e6fb2a7cc75d54a269b07cc6218361bd46", + "url": "https://github.com/status-im/nim-toml-serialization", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "serialization", + "stew" + ], + "checksums": { + "sha1": "76ae1c2af5dd092849b41750ff29217980dc9ca3" + } + }, + "confutils": { + "version": "0.1.0", + "vcsRevision": "7728f6bd81a1eedcfe277d02ea85fdb805bcc05a", + "url": "https://github.com/status-im/nim-confutils", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "serialization", + "results" + ], + "checksums": { + "sha1": "8bc8c30b107fdba73b677e5f257c6c42ae1cdc8e" + } + }, + "cbor_serialization": { + "version": "0.3.0", + "vcsRevision": "1664160e04d153573373afddc552b9cbf6fbe4dc", + "url": "https://github.com/vacp2p/nim-cbor-serialization", + "downloadMethod": "git", + "dependencies": [ + "nim", + "serialization", + "stew", + "results" + ], + "checksums": { + "sha1": "ab126eae09a6e39c72972a6a0b83cb06a2ffe8f0" + } + }, + "json_serialization": { + "version": "0.4.4", + "vcsRevision": "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44", + "url": "https://github.com/status-im/nim-json-serialization", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "serialization", + "stew", + "results" + ], + "checksums": { + "sha1": "8b3115354104858a0ac9019356fb29720529c2bd" + } + }, + "chronicles": { + "version": "0.12.2", + "vcsRevision": "27ec507429a4eb81edc20f28292ee8ec420be05b", + "url": "https://github.com/status-im/nim-chronicles", + "downloadMethod": "git", + "dependencies": [ + "nim", + "faststreams", + "serialization", + "json_serialization", + "testutils" + ], + "checksums": { + "sha1": "02febb20d088120b2836d3306cfa21f434f88f65" + } + }, + "presto": { + "version": "0.1.1", + "vcsRevision": "d66043dd7ede146442e6c39720c76a20bde5225f", + "url": "https://github.com/status-im/nim-presto", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "chronicles", + "metrics", + "results", + "stew" + ], + "checksums": { + "sha1": "8df97c45683abe2337bdff43b844c4fbcc124ca2" + } + }, + "brokers": { + "version": "#v2.0.1", + "vcsRevision": "2093ca4d50e581adda73fee7fd16231f990f4cbe", + "url": "https://github.com/NagyZoltanPeter/nim-brokers.git", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "results", + "chronicles", + "testutils", + "cbor_serialization" + ], + "checksums": { + "sha1": "cc74c987af94537e9d44d1b0143aa417299040c5" + } + }, + "stint": { + "version": "0.8.2", + "vcsRevision": "470b7892561b5179ab20bd389a69217d6213fe58", + "url": "https://github.com/status-im/nim-stint", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "unittest2" + ], + "checksums": { + "sha1": "d8f871fd617e7857192d4609fe003b48942a8ae5" + } + }, + "minilru": { + "version": "0.1.0", + "vcsRevision": "6dd93feb60f4cded3c05e7af7209cf63fb677893", + "url": "https://github.com/status-im/nim-minilru", + "downloadMethod": "git", + "dependencies": [ + "nim", + "results", + "unittest2" + ], + "checksums": { + "sha1": "0be03a5da29fdd4409ea74a60fd0ccce882601b4" + } + }, + "sqlite3_abi": { + "version": "3.53.0.0", + "vcsRevision": "8240e8e2819dfce1b67fa2733135d01b5cc80ae0", + "url": "https://github.com/arnetheduck/nim-sqlite3-abi", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "fb7a6e6f36fc4eb4dfa6634dbcbf5cd0dfd0ebf0" + } + }, + "dnsclient": { + "version": "0.3.4", + "vcsRevision": "23214235d4784d24aceed99bbfe153379ea557c8", + "url": "https://github.com/ba0f3/dnsclient.nim", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "65262c7e533ff49d6aca5539da4bc6c6ce132f40" + } + }, + "unicodedb": { + "version": "0.13.2", + "vcsRevision": "66f2458710dc641dd4640368f9483c8a0ec70561", + "url": "https://github.com/nitely/nim-unicodedb", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "739102d885d99bb4571b1955f5f12aee423c935b" + } + }, + "regex": { + "version": "0.26.3", + "vcsRevision": "4593305ed1e49731fc75af1dc572dd2559aad19c", + "url": "https://github.com/nitely/nim-regex", + "downloadMethod": "git", + "dependencies": [ + "nim", + "unicodedb" + ], + "checksums": { + "sha1": "4d24e7d7441137cd202e16f2359a5807ddbdc31f" + } + }, + "nimcrypto": { + "version": "0.6.4", + "vcsRevision": "721fb99ee099b632eb86dfad1f0d96ee87583774", + "url": "https://github.com/cheatfate/nimcrypto", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "f9ab24fa940ed03d0fb09729a7303feb50b7eaec" + } + }, + "websock": { + "version": "0.3.0", + "vcsRevision": "c105d98e6522e0e2cbe3dfa11b07a273e9fd0e7b", + "url": "https://github.com/status-im/nim-websock", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "httputils", + "chronicles", + "stew", + "nimcrypto", + "bearssl", + "results", + "zlib" + ], + "checksums": { + "sha1": "1294a66520fa4541e261dec8a6a84f774fb8c0ac" + } + }, + "json_rpc": { + "version": "#43bbf499143eb45046c83ac9794c9e3280a2b8e7", + "vcsRevision": "43bbf499143eb45046c83ac9794c9e3280a2b8e7", + "url": "https://github.com/status-im/nim-json-rpc.git", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "nimcrypto", + "stint", + "chronos", + "httputils", + "chronicles", + "websock", + "serialization", + "json_serialization", + "unittest2" + ], + "checksums": { + "sha1": "30ff6ead115b88c79862c5c7e37b1c9852eea59f" + } + }, + "lsquic": { + "version": "0.0.1", + "vcsRevision": "4fb03ee7bfb39aecb3316889fdcb60bec3d0936f", + "url": "https://github.com/vacp2p/nim-lsquic", + "downloadMethod": "git", + "dependencies": [ + "nim", + "zlib", + "stew", + "chronos", + "nimcrypto", + "unittest2", + "chronicles" + ], + "checksums": { + "sha1": "f465fa994346490d0924d162f53d9b5aec62f948" + } + }, + "secp256k1": { + "version": "0.6.0.3.2", + "vcsRevision": "d8f1288b7c72f00be5fc2c5ea72bf5cae1eafb15", + "url": "https://github.com/status-im/nim-secp256k1", + "downloadMethod": "git", + "dependencies": [ + "nim", + "stew", + "results", + "nimcrypto" + ], + "checksums": { + "sha1": "6618ef9de17121846a8c1d0317026b0ce8584e10" + } + }, + "eth": { + "version": "0.9.0", + "vcsRevision": "d9135e6c3c5d6d819afdfb566aa8d958756b73a8", + "url": "https://github.com/status-im/nim-eth", + "downloadMethod": "git", + "dependencies": [ + "nim", + "nimcrypto", + "stint", + "secp256k1", + "chronos", + "chronicles", + "stew", + "nat_traversal", + "metrics", + "sqlite3_abi", + "confutils", + "testutils", + "unittest2", + "results", + "minilru", + "snappy" + ], + "checksums": { + "sha1": "2e01b0cfff9523d110562af70d19948280f8013e" + } + }, + "web3": { + "version": "0.8.0", + "vcsRevision": "cdfe5601d2812a58e54faf53ee634452d01e5918", + "url": "https://github.com/status-im/nim-web3", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronicles", + "chronos", + "bearssl", + "eth", + "faststreams", + "json_rpc", + "serialization", + "json_serialization", + "nimcrypto", + "stew", + "stint", + "results" + ], + "checksums": { + "sha1": "26a112af032ef1536f97da2ca7364af618a11b80" + } + }, + "dnsdisc": { + "version": "0.1.0", + "vcsRevision": "38f2e0f52c0a8f032ef4530835e519d550706d9e", + "url": "https://github.com/status-im/nim-dnsdisc", + "downloadMethod": "git", + "dependencies": [ + "nim", + "bearssl", + "chronicles", + "chronos", + "eth", + "secp256k1", + "stew", + "testutils", + "unittest2", + "nimcrypto", + "results" + ], + "checksums": { + "sha1": "055b882a0f6b1d1e57a25a7af99d2e5ac6268154" + } + }, + "libp2p": { + "version": "#ff8d51857b4b79a68468e7bcc27b2026cca02996", + "vcsRevision": "ff8d51857b4b79a68468e7bcc27b2026cca02996", + "url": "https://github.com/vacp2p/nim-libp2p.git", + "downloadMethod": "git", + "dependencies": [ + "nim", + "nimcrypto", + "dnsclient", + "bearssl", + "chronicles", + "chronos", + "metrics", + "secp256k1", + "stew", + "websock", + "unittest2", + "results", + "serialization", + "lsquic", + "jwt" + ], + "checksums": { + "sha1": "fa2a7552c6ec860717b77ce34cf0b7afe4570234" + } + }, + "taskpools": { + "version": "0.1.0", + "vcsRevision": "9e8ccc754631ac55ac2fd495e167e74e86293edb", + "url": "https://github.com/status-im/nim-taskpools", + "downloadMethod": "git", + "dependencies": [ + "nim" + ], + "checksums": { + "sha1": "09e1b2fdad55b973724d61227971afc0df0b7a81" + } + }, + "sds": { + "version": "#2e9a7683f0e180bf112135fae3a3803eed8490d4", + "vcsRevision": "2e9a7683f0e180bf112135fae3a3803eed8490d4", + "url": "https://github.com/logos-messaging/nim-sds.git", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "libp2p", + "chronicles", + "stew", + "stint", + "metrics", + "results", + "taskpools" + ], + "checksums": { + "sha1": "d13f1bf8d1b90b27e9edfc063b043831242cda19" + } + }, + "ffi": { + "version": "0.1.3", + "vcsRevision": "06111de155253b34e47ed2aaed1d61d08d62cc1b", + "url": "https://github.com/logos-messaging/nim-ffi", + "downloadMethod": "git", + "dependencies": [ + "nim", + "chronos", + "chronicles", + "taskpools" + ], + "checksums": { + "sha1": "6f9d49375ea1dc71add55c72ac80a808f238e5b0" + } + } + }, + "tasks": {} +} diff --git a/nix/atlas.nix b/nix/atlas.nix deleted file mode 100644 index 43336e07a..000000000 --- a/nix/atlas.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ pkgs ? import { } }: - -let - tools = pkgs.callPackage ./tools.nix {}; - sourceFile = ../vendor/nimbus-build-system/vendor/Nim/koch.nim; -in pkgs.fetchFromGitHub { - owner = "nim-lang"; - repo = "atlas"; - rev = tools.findKeyValue "^ +AtlasStableCommit = \"([a-f0-9]+)\"$" sourceFile; - # WARNING: Requires manual updates when Nim compiler version changes. - hash = "sha256-G1TZdgbRPSgxXZ3VsBP2+XFCLHXVb3an65MuQx67o/k="; -} \ No newline at end of file diff --git a/nix/checksums.nix b/nix/checksums.nix index d79345d24..c9c9f3d45 100644 --- a/nix/checksums.nix +++ b/nix/checksums.nix @@ -6,7 +6,7 @@ let in pkgs.fetchFromGitHub { owner = "nim-lang"; repo = "checksums"; - rev = tools.findKeyValue "^ +ChecksumsStableCommit = \"([a-f0-9]+)\"$" sourceFile; + rev = tools.findKeyValue "^ +ChecksumsStableCommit = \"([a-f0-9]+)\".*$" sourceFile; # WARNING: Requires manual updates when Nim compiler version changes. - hash = "sha256-Bm5iJoT2kAvcTexiLMFBa9oU5gf7d4rWjo3OiN7obWQ="; + hash = "sha256-JZhWqn4SrAgNw/HLzBK0rrj3WzvJ3Tv1nuDMn83KoYY="; } diff --git a/nix/default.nix b/nix/default.nix index 29eec844d..7b7989e1a 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -1,116 +1,138 @@ -{ - config ? {}, - pkgs ? import { }, - src ? ../., - targets ? ["libwaku-android-arm64"], - verbosity ? 2, - useSystemNim ? true, - quickAndDirty ? true, - stableSystems ? [ - "x86_64-linux" "aarch64-linux" - ], - androidArch, - abidir, - zerokitPkg, +{ pkgs +, src +, zerokitRln +, gitVersion ? "n/a" +, enablePostgres ? true +, enableNimDebugDlOpen ? true +, chroniclesLogLevel ? null }: -assert pkgs.lib.assertMsg ((src.submodules or true) == true) - "Unable to build without submodules. Append '?submodules=1#' to the URI."; - let - inherit (pkgs) stdenv lib writeScriptBin callPackage; + deps = import ./deps.nix { inherit pkgs; }; - revision = lib.substring 0 8 (src.rev or "dirty"); + nimDefineArgs = pkgs.lib.concatStringsSep " \\\n " ( + [ "--define:disable_libbacktrace" + "--define:git_version=${gitVersion}" ] + ++ pkgs.lib.optional enablePostgres "--define:postgres" + ++ pkgs.lib.optional enableNimDebugDlOpen "--define:nimDebugDlOpen" + ++ pkgs.lib.optional (chroniclesLogLevel != null) + "--define:chronicles_log_level=${toString chroniclesLogLevel}" + ); -in stdenv.mkDerivation rec { + # nat_traversal is excluded from the static pathArgs; it is handled + # separately in buildPhase (its bundled C libs must be compiled first). + otherDeps = builtins.removeAttrs deps [ "nat_traversal" ]; - pname = "nwaku"; + # Some packages (e.g. regex, unicodedb) put their .nim files under src/ + # while others use the repo root. Pass both so the compiler finds either layout. + pathArgs = + builtins.concatStringsSep " " + (builtins.concatMap (p: [ "--path:${p}" "--path:${p}/src" ]) + (builtins.attrValues otherDeps)); - version = "1.0.0-${revision}"; + libExt = + if pkgs.stdenv.hostPlatform.isWindows then "dll" + else if pkgs.stdenv.hostPlatform.isDarwin then "dylib" + else "so"; +in +pkgs.stdenv.mkDerivation { + pname = "liblogosdelivery"; + version = "dev"; inherit src; - buildInputs = with pkgs; [ - openssl - gmp - zip - ]; + nativeBuildInputs = with pkgs; [ + nim-2_2 + git + gnumake + which + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ pkgs.darwin.cctools ]; - # Dependencies that should only exist in the build environment. - nativeBuildInputs = let - # Fix for Nim compiler calling 'git rev-parse' and 'lsb_release'. - fakeGit = writeScriptBin "git" "echo ${version}"; - # Fix for the zerokit package that is built with cargo/rustup/cross. - fakeCargo = writeScriptBin "cargo" "echo ${version}"; - # Fix for the zerokit package that is built with cargo/rustup/cross. - fakeRustup = writeScriptBin "rustup" "echo ${version}"; - # Fix for the zerokit package that is built with cargo/rustup/cross. - fakeCross = writeScriptBin "cross" "echo ${version}"; - in - with pkgs; [ - cmake - which - lsb-release - zerokitPkg - nim-unwrapped-2_0 - fakeGit - fakeCargo - fakeRustup - fakeCross - ]; + buildInputs = [ zerokitRln ] + ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.stdenv.cc.cc.lib ]; - # Environment variables required for Android builds - ANDROID_SDK_ROOT="${pkgs.androidPkgs.sdk}"; - ANDROID_NDK_HOME="${pkgs.androidPkgs.ndk}"; - NIMFLAGS = "-d:disableMarchNative -d:git_revision_override=${revision}"; - XDG_CACHE_HOME = "/tmp"; - androidManifest = ""; + buildPhase = '' + export HOME=$TMPDIR + export XDG_CACHE_HOME=$TMPDIR/.cache + export NIMBLE_DIR=$TMPDIR/.nimble + export NIMCACHE=$TMPDIR/nimcache - makeFlags = targets ++ [ - "V=${toString verbosity}" - "QUICK_AND_DIRTY_COMPILER=${if quickAndDirty then "1" else "0"}" - "QUICK_AND_DIRTY_NIMBLE=${if quickAndDirty then "1" else "0"}" - "USE_SYSTEM_NIM=${if useSystemNim then "1" else "0"}" - ]; + mkdir -p build $NIMCACHE - configurePhase = '' - patchShebangs . vendor/nimbus-build-system > /dev/null - make nimbus-build-system-paths - make nimbus-build-system-nimble-dir - ''; + # nat_traversal bundles C sub-libraries that must be compiled before linking. + # Copy the fetchgit store path to a writable tmpdir, build, then pass to nim. + NAT_TRAV=$TMPDIR/nat_traversal + cp -r ${deps.nat_traversal} $NAT_TRAV + chmod -R +w $NAT_TRAV - preBuild = '' - ln -s waku.nimble waku.nims - pushd vendor/nimbus-build-system/vendor/Nim - mkdir dist - cp -r ${callPackage ./nimble.nix {}} dist/nimble - chmod 777 -R dist/nimble - mkdir -p dist/nimble/dist - cp -r ${callPackage ./checksums.nix {}} dist/checksums # need both - cp -r ${callPackage ./checksums.nix {}} dist/nimble/dist/checksums - cp -r ${callPackage ./atlas.nix {}} dist/atlas - chmod 777 -R dist/atlas - mkdir dist/atlas/dist - cp -r ${callPackage ./sat.nix {}} dist/nimble/dist/sat - cp -r ${callPackage ./sat.nix {}} dist/atlas/dist/sat - cp -r ${callPackage ./csources.nix {}} csources_v2 - chmod 777 -R dist/nimble csources_v2 - popd - mkdir -p vendor/zerokit/target/${androidArch}/release - cp ${zerokitPkg}/librln.so vendor/zerokit/target/${androidArch}/release/ + make -C $NAT_TRAV/vendor/miniupnp/miniupnpc \ + CFLAGS="-Os -fPIC" build/libminiupnpc.a + + make -C $NAT_TRAV/vendor/libnatpmp-upstream \ + CFLAGS="-Wall -Os -fPIC -DENABLE_STRNATPMPERR -DNATPMP_MAX_RETRIES=4" libnatpmp.a + + echo "== Building liblogosdelivery (dynamic) ==" + nim c \ + --noNimblePath \ + ${pathArgs} \ + --path:$NAT_TRAV \ + --path:$NAT_TRAV/src \ + --passL:"-L${zerokitRln}/lib -lrln${pkgs.lib.optionalString pkgs.stdenv.isLinux " -lstdc++"}" \ + ${nimDefineArgs} \ + --out:build/liblogosdelivery.${libExt} \ + --app:lib \ + --threads:on \ + --opt:size \ + --noMain \ + --mm:refc \ + --header \ + --nimMainPrefix:liblogosdelivery \ + --nimcache:$NIMCACHE \ + liblogosdelivery/liblogosdelivery.nim + + echo "== Building liblogosdelivery (static) ==" + nim c \ + --noNimblePath \ + ${pathArgs} \ + --path:$NAT_TRAV \ + --path:$NAT_TRAV/src \ + --passL:"-L${zerokitRln}/lib -lrln${pkgs.lib.optionalString pkgs.stdenv.isLinux " -lstdc++"}" \ + ${nimDefineArgs} \ + --out:build/liblogosdelivery.a \ + --app:staticlib \ + --threads:on \ + --opt:size \ + --noMain \ + --mm:refc \ + --nimMainPrefix:liblogosdelivery \ + --nimcache:$NIMCACHE \ + liblogosdelivery/liblogosdelivery.nim ''; installPhase = '' - mkdir -p $out/jni - cp -r ./build/android/${abidir}/* $out/jni/ - echo '${androidManifest}' > $out/jni/AndroidManifest.xml - cd $out && zip -r libwaku.aar * + runHook preInstall + mkdir -p $out/lib $out/include + cp build/liblogosdelivery.${libExt} $out/lib/ 2>/dev/null || true + cp build/liblogosdelivery.a $out/lib/ 2>/dev/null || true + cp liblogosdelivery/liblogosdelivery.h $out/include/ 2>/dev/null || true + runHook postInstall ''; - meta = with pkgs.lib; { - description = "NWaku derivation to build libwaku for mobile targets using Android NDK and Rust."; - homepage = "https://github.com/status-im/nwaku"; - license = licenses.mit; - platforms = stableSystems; - }; + # Bundle librln alongside liblogosdelivery so the output is self-contained. + # Use --add-rpath (not --set-rpath) so fixupPhase's stdenv RUNPATH injection + # for libstdc++ is preserved. + postInstall = + pkgs.lib.optionalString pkgs.stdenv.isDarwin '' + cp ${zerokitRln}/lib/librln.dylib $out/lib/ + chmod +w $out/lib/librln.dylib $out/lib/liblogosdelivery.dylib + install_name_tool -id @rpath/liblogosdelivery.dylib $out/lib/liblogosdelivery.dylib + install_name_tool -id @rpath/librln.dylib $out/lib/librln.dylib + old=$(otool -L $out/lib/liblogosdelivery.dylib | awk 'NR>1{print $1}' | grep librln) + install_name_tool -change "$old" @rpath/librln.dylib $out/lib/liblogosdelivery.dylib + install_name_tool -add_rpath @loader_path $out/lib/liblogosdelivery.dylib + '' + + pkgs.lib.optionalString pkgs.stdenv.isLinux '' + cp ${zerokitRln}/lib/librln.so $out/lib/ + patchelf --add-rpath '$ORIGIN' $out/lib/liblogosdelivery.so + ''; } diff --git a/nix/deps.nix b/nix/deps.nix new file mode 100644 index 000000000..63eeb597a --- /dev/null +++ b/nix/deps.nix @@ -0,0 +1,293 @@ +# AUTOGENERATED from nimble.lock — do not edit manually. +# Regenerate with: ./tools/gen-nix-deps.sh nimble.lock nix/deps.nix +{ pkgs }: + +{ + unittest2 = pkgs.fetchgit { + url = "https://github.com/status-im/nim-unittest2"; + rev = "26f2ef3ae0ec72a2a75bfe557e02e88f6a31c189"; + sha256 = "1n8n36kad50m97b64y7bzzknz9n7szffxhp0bqpk3g2v7zpda8sw"; + fetchSubmodules = true; + }; + + bearssl = pkgs.fetchgit { + url = "https://github.com/status-im/nim-bearssl"; + rev = "22c6a76ce015bc07e011562bdcfc51d9446c1e82"; + sha256 = "1cvdd7lfrpa6asmc39al3g4py5nqhpqmvypc36r5qyv7p5arc8a3"; + fetchSubmodules = true; + }; + + bearssl_pkey_decoder = pkgs.fetchgit { + url = "https://github.com/vacp2p/bearssl_pkey_decoder"; + rev = "21dd3710df9345ed2ad8bf8f882761e07863b8e0"; + sha256 = "0bl3f147zmkazbhdkr4cj1nipf9rqiw3g4hh1j424k9hpl55zdpg"; + fetchSubmodules = true; + }; + + jwt = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-jwt.git"; + rev = "18f8378de52b241f321c1f9ea905456e89b95c6f"; + sha256 = "1986czmszdxj6g9yr7xn1fx8y2y9mwpb3f1bn9nc6973qawsdm0p"; + fetchSubmodules = true; + }; + + testutils = pkgs.fetchgit { + url = "https://github.com/status-im/nim-testutils"; + rev = "6ce5e5e2301ccbc04b09d27ff78741ff4d352b4d"; + sha256 = "1vbkr6i5yxhc2ai3b7rbglhmyc98f99x874fqdp6a152a6kqgwxy"; + fetchSubmodules = true; + }; + + db_connector = pkgs.fetchgit { + url = "https://github.com/nim-lang/db_connector"; + rev = "29450a2063970712422e1ab857695c12d80112a6"; + sha256 = "11dna09ccdhj3pzpqa04j7a95ibx907z6n1ff33yf0n92qa4x59z"; + fetchSubmodules = true; + }; + + results = pkgs.fetchgit { + url = "https://github.com/arnetheduck/nim-results"; + rev = "df8113dda4c2d74d460a8fa98252b0b771bf1f27"; + sha256 = "1h7amas16sbhlr7zb7n3jb5434k98ji375vzw72k1fsc86vnmcr9"; + fetchSubmodules = true; + }; + + nat_traversal = pkgs.fetchgit { + url = "https://github.com/status-im/nim-nat-traversal"; + rev = "860e18c37667b5dd005b94c63264560c35d88004"; + sha256 = "0319k5bbl468phwfnvlrh7725sc80rnf7m9gyj0i3cb5hb9q78bs"; + fetchSubmodules = true; + }; + + stew = pkgs.fetchgit { + url = "https://github.com/status-im/nim-stew"; + rev = "4382b18f04b3c43c8409bfcd6b62063773b2bbaa"; + sha256 = "0mx9g5m636h3sk5pllcpylk51brf7lx91izx3gc23k3ih3hrxyk2"; + fetchSubmodules = true; + }; + + zlib = pkgs.fetchgit { + url = "https://github.com/status-im/nim-zlib"; + rev = "e680f269fb01af2c34a2ba879ff281795a5258fe"; + sha256 = "1xw9f1gjsgqihdg7kdkbaq1wankgnx2vn9l3ihc6nqk2jzv5bvk5"; + fetchSubmodules = true; + }; + + httputils = pkgs.fetchgit { + url = "https://github.com/status-im/nim-http-utils"; + rev = "f142cb2e8bd812dd002a6493b6082827bb248592"; + sha256 = "03msj4zdxraz4qx9cidb17g7v0asazxv91nng6xxbzjxz0qaqxw6"; + fetchSubmodules = true; + }; + + chronos = pkgs.fetchgit { + url = "https://github.com/status-im/nim-chronos"; + rev = "45f43a9ad8bd8bcf5903b42f365c1c879bd54240"; + sha256 = "1v1n59zfzznp97pvwgs9kf136bqmv4x2s2y9f24msspa7qv27w39"; + fetchSubmodules = true; + }; + + metrics = pkgs.fetchgit { + url = "https://github.com/status-im/nim-metrics"; + rev = "a1296caf3ebb5f30f51a5feae7749a30df2824c2"; + sha256 = "02vxqy20g8012ks939ac25ksc25k727q84si0p2cmihy5bw1a3qm"; + fetchSubmodules = true; + }; + + faststreams = pkgs.fetchgit { + url = "https://github.com/status-im/nim-faststreams"; + rev = "ce27581a3e881f782f482cb66dc5b07a02bd615e"; + sha256 = "0y6bw2scnmr8cxj4fg18w7f34l2bh9qwg5nhlgd84m9fpr5bqarn"; + fetchSubmodules = true; + }; + + snappy = pkgs.fetchgit { + url = "https://github.com/status-im/nim-snappy"; + rev = "00bfcef94f8ef6981df5d5b994897f6695badfb2"; + sha256 = "117mam97mkjjj1hs8svc07679k5ayww9yigi74yq8dyqm6fpbl6l"; + fetchSubmodules = true; + }; + + serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-serialization"; + rev = "b0f2fa32960ea532a184394b0f27be37bd80248b"; + sha256 = "0wip1fjx7ka39ck1g1xvmyarzq1p5dlngpqil6zff8k8z5skiz27"; + fetchSubmodules = true; + }; + + toml_serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-toml-serialization"; + rev = "b5b387e6fb2a7cc75d54a269b07cc6218361bd46"; + sha256 = "175swdj01rz57h1hvflkyaz4x76qbfn0174ysrk3qk385i1zlg5z"; + fetchSubmodules = true; + }; + + confutils = pkgs.fetchgit { + url = "https://github.com/status-im/nim-confutils"; + rev = "7728f6bd81a1eedcfe277d02ea85fdb805bcc05a"; + sha256 = "18bj1ilx10jm2vmqx2wy2xl9rzy7alymi2m4n9jgpa4sbxnfh0x3"; + fetchSubmodules = true; + }; + + cbor_serialization = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-cbor-serialization"; + rev = "1664160e04d153573373afddc552b9cbf6fbe4dc"; + sha256 = "0c1rj4fk0fcqvsf0yqhxvm8h10aww75gi4yfsjhlczh88ypywii2"; + fetchSubmodules = true; + }; + + json_serialization = pkgs.fetchgit { + url = "https://github.com/status-im/nim-json-serialization"; + rev = "c343b0e243d9e17e2c40f3a8a24340f7c4a71d44"; + sha256 = "0i8sq51nqj8lshf6bfixaz9a7sq0ahsbvq3chkxdvv4khsqvam91"; + fetchSubmodules = true; + }; + + chronicles = pkgs.fetchgit { + url = "https://github.com/status-im/nim-chronicles"; + rev = "27ec507429a4eb81edc20f28292ee8ec420be05b"; + sha256 = "1xx9fcfwgcaizq3s7i3s03mclz253r5j8va38l9ycl19fcbc96z9"; + fetchSubmodules = true; + }; + + presto = pkgs.fetchgit { + url = "https://github.com/status-im/nim-presto"; + rev = "d66043dd7ede146442e6c39720c76a20bde5225f"; + sha256 = "1hrppcak32aigrdv3mqk124w81yy9jv1prs57vqqhfj83gl930vi"; + fetchSubmodules = true; + }; + + brokers = pkgs.fetchgit { + url = "https://github.com/NagyZoltanPeter/nim-brokers.git"; + rev = "2093ca4d50e581adda73fee7fd16231f990f4cbe"; + sha256 = "0a4ix2q6riqfrd0hfnajisy159qdmk5imwzymppj23rwc8n7d2dx"; + fetchSubmodules = true; + }; + + stint = pkgs.fetchgit { + url = "https://github.com/status-im/nim-stint"; + rev = "470b7892561b5179ab20bd389a69217d6213fe58"; + sha256 = "1isfwmbj98qfi5pm9acy0yyvq0vlz38nxp30xl43jx2mmaga2w22"; + fetchSubmodules = true; + }; + + minilru = pkgs.fetchgit { + url = "https://github.com/status-im/nim-minilru"; + rev = "6dd93feb60f4cded3c05e7af7209cf63fb677893"; + sha256 = "1xgx4j56ais3hk8b51zhnfs9q85g2afkp3y1j9ky5iziqvcs2sml"; + fetchSubmodules = true; + }; + + sqlite3_abi = pkgs.fetchgit { + url = "https://github.com/arnetheduck/nim-sqlite3-abi"; + rev = "8240e8e2819dfce1b67fa2733135d01b5cc80ae0"; + sha256 = "0g8bc0kiwxxh3h5w06ksa23cw81hnx87rdn93v64m2f053nb6bcm"; + fetchSubmodules = true; + }; + + dnsclient = pkgs.fetchgit { + url = "https://github.com/ba0f3/dnsclient.nim"; + rev = "23214235d4784d24aceed99bbfe153379ea557c8"; + sha256 = "03mf3lw5c0m5nq9ppa49nylrl8ibkv2zzlc0wyhqg7w09kz6hks6"; + fetchSubmodules = true; + }; + + unicodedb = pkgs.fetchgit { + url = "https://github.com/nitely/nim-unicodedb"; + rev = "66f2458710dc641dd4640368f9483c8a0ec70561"; + sha256 = "092z3glgdb7rmwajm7dmqzvralkm7ixighixk8ycf8sf17zm72ck"; + fetchSubmodules = true; + }; + + regex = pkgs.fetchgit { + url = "https://github.com/nitely/nim-regex"; + rev = "4593305ed1e49731fc75af1dc572dd2559aad19c"; + sha256 = "1b666qws5sva3n5allin0ycvnqlzdjd7xzprpdvv632ccqddzcl9"; + fetchSubmodules = true; + }; + + nimcrypto = pkgs.fetchgit { + url = "https://github.com/cheatfate/nimcrypto"; + rev = "721fb99ee099b632eb86dfad1f0d96ee87583774"; + sha256 = "178vzb3q8wzjq295ik2pd25rrqf32w381ck76hm5x2d8qnzfmkkc"; + fetchSubmodules = true; + }; + + websock = pkgs.fetchgit { + url = "https://github.com/status-im/nim-websock"; + rev = "c105d98e6522e0e2cbe3dfa11b07a273e9fd0e7b"; + sha256 = "1zrigw27nwcmg7mw9867581ipcp3ckrqq3cwl2snabcjhkp5dm2c"; + fetchSubmodules = true; + }; + + json_rpc = pkgs.fetchgit { + url = "https://github.com/status-im/nim-json-rpc.git"; + rev = "43bbf499143eb45046c83ac9794c9e3280a2b8e7"; + sha256 = "1c1msxg958jm2ggvs875b6wh6n829d3lh7x4ch6dcxawda16qf95"; + fetchSubmodules = true; + }; + + lsquic = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-lsquic"; + rev = "4fb03ee7bfb39aecb3316889fdcb60bec3d0936f"; + sha256 = "0qdhcd4hyp185szc9sv3jvwdwc9zp3j0syy7glxv13k9bchfmkfg"; + fetchSubmodules = true; + }; + + secp256k1 = pkgs.fetchgit { + url = "https://github.com/status-im/nim-secp256k1"; + rev = "d8f1288b7c72f00be5fc2c5ea72bf5cae1eafb15"; + sha256 = "1qjrmwbngb73f6r1fznvig53nyal7wj41d1cmqfksrmivk2sgrn2"; + fetchSubmodules = true; + }; + + eth = pkgs.fetchgit { + url = "https://github.com/status-im/nim-eth"; + rev = "d9135e6c3c5d6d819afdfb566aa8d958756b73a8"; + sha256 = "15r6aszalnbk6mkyfbv5rnz5vcf1mmgj6yg332wry53xsd2ipg7r"; + fetchSubmodules = true; + }; + + web3 = pkgs.fetchgit { + url = "https://github.com/status-im/nim-web3"; + rev = "cdfe5601d2812a58e54faf53ee634452d01e5918"; + sha256 = "1j52vcqw868qs40bh4wzfw5cvvnywp2q0dnzhfajh31xws98jc27"; + fetchSubmodules = true; + }; + + dnsdisc = pkgs.fetchgit { + url = "https://github.com/status-im/nim-dnsdisc"; + rev = "38f2e0f52c0a8f032ef4530835e519d550706d9e"; + sha256 = "0dk787ny49n41bmzhlrvm87giwajr01gwdw9nlmphch89rdqpxxn"; + fetchSubmodules = true; + }; + + libp2p = pkgs.fetchgit { + url = "https://github.com/vacp2p/nim-libp2p.git"; + rev = "ff8d51857b4b79a68468e7bcc27b2026cca02996"; + sha256 = "08y4s0zhqzsd780bwaixfqbi79km0mcq5g8nyw7awfvcbjqsa53l"; + fetchSubmodules = true; + }; + + taskpools = pkgs.fetchgit { + url = "https://github.com/status-im/nim-taskpools"; + rev = "9e8ccc754631ac55ac2fd495e167e74e86293edb"; + sha256 = "1y78l33vdjxmb9dkr455pbphxa73rgdsh8m9gpkf4d9b1wm1yivy"; + fetchSubmodules = true; + }; + + sds = pkgs.fetchgit { + url = "https://github.com/logos-messaging/nim-sds.git"; + rev = "2e9a7683f0e180bf112135fae3a3803eed8490d4"; + sha256 = "1dbpvp3zhvdlfxdyggz5waga1vg3b6ndd3acfzhnx8k1wdr01c6f"; + fetchSubmodules = true; + }; + + ffi = pkgs.fetchgit { + url = "https://github.com/logos-messaging/nim-ffi"; + rev = "06111de155253b34e47ed2aaed1d61d08d62cc1b"; + sha256 = "0rb0d2i519amgsp7q0bn6m5465z1vwj4rab89529pyiivh3fgh8j"; + fetchSubmodules = true; + }; + +} diff --git a/nix/nimble.nix b/nix/nimble.nix deleted file mode 100644 index 5bd7b0f32..000000000 --- a/nix/nimble.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ pkgs ? import { } }: - -let - tools = pkgs.callPackage ./tools.nix {}; - sourceFile = ../vendor/nimbus-build-system/vendor/Nim/koch.nim; -in pkgs.fetchFromGitHub { - owner = "nim-lang"; - repo = "nimble"; - rev = tools.findKeyValue "^ +NimbleStableCommit = \"([a-f0-9]+)\".+" sourceFile; - # WARNING: Requires manual updates when Nim compiler version changes. - hash = "sha256-MVHf19UbOWk8Zba2scj06PxdYYOJA6OXrVyDQ9Ku6Us="; -} \ No newline at end of file diff --git a/nix/pkgs/android-sdk/compose.nix b/nix/pkgs/android-sdk/compose.nix index c73aaee43..9a8536ddb 100644 --- a/nix/pkgs/android-sdk/compose.nix +++ b/nix/pkgs/android-sdk/compose.nix @@ -5,19 +5,16 @@ { androidenv, lib, stdenv }: -assert lib.assertMsg (stdenv.system != "aarch64-darwin") - "aarch64-darwin not supported for Android SDK. Use: NIXPKGS_SYSTEM_OVERRIDE=x86_64-darwin"; - # The "android-sdk-license" license is accepted # by setting android_sdk.accept_license = true. androidenv.composeAndroidPackages { cmdLineToolsVersion = "9.0"; toolsVersion = "26.1.1"; - platformToolsVersion = "33.0.3"; + platformToolsVersion = "34.0.5"; buildToolsVersions = [ "34.0.0" ]; platformVersions = [ "34" ]; cmakeVersions = [ "3.22.1" ]; - ndkVersion = "25.2.9519653"; + ndkVersion = "27.2.12479018"; includeNDK = true; includeExtras = [ "extras;android;m2repository" diff --git a/nix/sat.nix b/nix/sat.nix index 31f264468..92db58a2e 100644 --- a/nix/sat.nix +++ b/nix/sat.nix @@ -6,7 +6,8 @@ let in pkgs.fetchFromGitHub { owner = "nim-lang"; repo = "sat"; - rev = tools.findKeyValue "^ +SatStableCommit = \"([a-f0-9]+)\"$" sourceFile; + rev = tools.findKeyValue "^ +SatStableCommit = \"([a-f0-9]+)\".*$" sourceFile; + # WARNING: Requires manual updates when Nim compiler version changes. # WARNING: Requires manual updates when Nim compiler version changes. hash = "sha256-JFrrSV+mehG0gP7NiQ8hYthL0cjh44HNbXfuxQNhq7c="; -} \ No newline at end of file +} diff --git a/nix/shell.nix b/nix/shell.nix index 0db73dc25..edff468ae 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -1,23 +1,31 @@ -{ - pkgs ? import { }, -}: +{ pkgs }: + let - optionalDarwinDeps = pkgs.lib.optionals pkgs.stdenv.isDarwin [ - pkgs.libiconv - pkgs.darwin.apple_sdk.frameworks.Security - ]; + nimble = pkgs.nimble.overrideAttrs (_: { + version = "0.22.3"; + src = pkgs.fetchFromGitHub { + owner = "nim-lang"; + repo = "nimble"; + rev = "v0.22.3"; + sha256 = "sha256-f7DYpRGVUeSi6basK1lfu5AxZpMFOSJ3oYsy+urYErg="; + }; + }); in + pkgs.mkShell { inputsFrom = [ pkgs.androidShell - ] ++ optionalDarwinDeps; + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ + pkgs.libiconv + pkgs.darwin.apple_sdk.frameworks.Security + ]; - buildInputs = with pkgs; [ + buildInputs = (with pkgs; [ git cargo rustup + rustc cmake - nim-unwrapped-2_0 - ]; - + nim-2_2 + ]) ++ [ nimble ]; # nimble pinned to 0.22.3 via let binding above } diff --git a/nix/submodules.json b/nix/submodules.json new file mode 100644 index 000000000..2f94e5f2b --- /dev/null +++ b/nix/submodules.json @@ -0,0 +1,247 @@ +[ + { + "path": "vendor/db_connector", + "url": "https://github.com/nim-lang/db_connector.git", + "rev": "74aef399e5c232f95c9fc5c987cebac846f09d62" + } + , + { + "path": "vendor/dnsclient.nim", + "url": "https://github.com/ba0f3/dnsclient.nim.git", + "rev": "23214235d4784d24aceed99bbfe153379ea557c8" + } + , + { + "path": "vendor/nim-bearssl", + "url": "https://github.com/status-im/nim-bearssl.git", + "rev": "11e798b62b8e6beabe958e048e9e24c7e0f9ee63" + } + , + { + "path": "vendor/nim-chronicles", + "url": "https://github.com/status-im/nim-chronicles.git", + "rev": "54f5b726025e8c7385e3a6529d3aa27454c6e6ff" + } + , + { + "path": "vendor/nim-chronos", + "url": "https://github.com/status-im/nim-chronos.git", + "rev": "85af4db764ecd3573c4704139560df3943216cf1" + } + , + { + "path": "vendor/nim-confutils", + "url": "https://github.com/status-im/nim-confutils.git", + "rev": "e214b3992a31acece6a9aada7d0a1ad37c928f3b" + } + , + { + "path": "vendor/nim-dnsdisc", + "url": "https://github.com/status-im/nim-dnsdisc.git", + "rev": "b71d029f4da4ec56974d54c04518bada00e1b623" + } + , + { + "path": "vendor/nim-eth", + "url": "https://github.com/status-im/nim-eth.git", + "rev": "d9135e6c3c5d6d819afdfb566aa8d958756b73a8" + } + , + { + "path": "vendor/nim-faststreams", + "url": "https://github.com/status-im/nim-faststreams.git", + "rev": "c3ac3f639ed1d62f59d3077d376a29c63ac9750c" + } + , + { + "path": "vendor/nim-ffi", + "url": "https://github.com/logos-messaging/nim-ffi", + "rev": "06111de155253b34e47ed2aaed1d61d08d62cc1b" + } + , + { + "path": "vendor/nim-http-utils", + "url": "https://github.com/status-im/nim-http-utils.git", + "rev": "79cbab1460f4c0cdde2084589d017c43a3d7b4f1" + } + , + { + "path": "vendor/nim-json-rpc", + "url": "https://github.com/status-im/nim-json-rpc.git", + "rev": "9665c265035f49f5ff94bbffdeadde68e19d6221" + } + , + { + "path": "vendor/nim-json-serialization", + "url": "https://github.com/status-im/nim-json-serialization.git", + "rev": "b65fd6a7e64c864dabe40e7dfd6c7d07db0014ac" + } + , + { + "path": "vendor/nim-jwt", + "url": "https://github.com/vacp2p/nim-jwt.git", + "rev": "18f8378de52b241f321c1f9ea905456e89b95c6f" + } + , + { + "path": "vendor/nim-libbacktrace", + "url": "https://github.com/status-im/nim-libbacktrace.git", + "rev": "d8bd4ce5c46bb6d2f984f6b3f3d7380897d95ecb" + } + , + { + "path": "vendor/nim-libp2p", + "url": "https://github.com/vacp2p/nim-libp2p.git", + "rev": "eb7e6ff89889e41b57515f891ba82986c54809fb" + } + , + { + "path": "vendor/nim-lsquic", + "url": "https://github.com/vacp2p/nim-lsquic", + "rev": "f3fe33462601ea34eb2e8e9c357c92e61f8d121b" + } + , + { + "path": "vendor/nim-metrics", + "url": "https://github.com/status-im/nim-metrics.git", + "rev": "ecf64c6078d1276d3b7d9b3d931fbdb70004db11" + } + , + { + "path": "vendor/nim-minilru", + "url": "https://github.com/status-im/nim-minilru.git", + "rev": "0c4b2bce959591f0a862e9b541ba43c6d0cf3476" + } + , + { + "path": "vendor/nim-nat-traversal", + "url": "https://github.com/status-im/nim-nat-traversal.git", + "rev": "860e18c37667b5dd005b94c63264560c35d88004" + } + , + { + "path": "vendor/nim-presto", + "url": "https://github.com/status-im/nim-presto.git", + "rev": "92b1c7ff141e6920e1f8a98a14c35c1fa098e3be" + } + , + { + "path": "vendor/nim-regex", + "url": "https://github.com/nitely/nim-regex.git", + "rev": "4593305ed1e49731fc75af1dc572dd2559aad19c" + } + , + { + "path": "vendor/nim-results", + "url": "https://github.com/arnetheduck/nim-results.git", + "rev": "df8113dda4c2d74d460a8fa98252b0b771bf1f27" + } + , + { + "path": "vendor/nim-secp256k1", + "url": "https://github.com/status-im/nim-secp256k1.git", + "rev": "9dd3df62124aae79d564da636bb22627c53c7676" + } + , + { + "path": "vendor/nim-serialization", + "url": "https://github.com/status-im/nim-serialization.git", + "rev": "6f525d5447d97256750ca7856faead03e562ed20" + } + , + { + "path": "vendor/nim-sqlite3-abi", + "url": "https://github.com/arnetheduck/nim-sqlite3-abi.git", + "rev": "bdf01cf4236fb40788f0733466cdf6708783cbac" + } + , + { + "path": "vendor/nim-stew", + "url": "https://github.com/status-im/nim-stew.git", + "rev": "e5740014961438610d336cd81706582dbf2c96f0" + } + , + { + "path": "vendor/nim-stint", + "url": "https://github.com/status-im/nim-stint.git", + "rev": "470b7892561b5179ab20bd389a69217d6213fe58" + } + , + { + "path": "vendor/nim-taskpools", + "url": "https://github.com/status-im/nim-taskpools.git", + "rev": "9e8ccc754631ac55ac2fd495e167e74e86293edb" + } + , + { + "path": "vendor/nim-testutils", + "url": "https://github.com/status-im/nim-testutils.git", + "rev": "94d68e796c045d5b37cabc6be32d7bfa168f8857" + } + , + { + "path": "vendor/nim-toml-serialization", + "url": "https://github.com/status-im/nim-toml-serialization.git", + "rev": "fea85b27f0badcf617033ca1bc05444b5fd8aa7a" + } + , + { + "path": "vendor/nim-unicodedb", + "url": "https://github.com/nitely/nim-unicodedb.git", + "rev": "66f2458710dc641dd4640368f9483c8a0ec70561" + } + , + { + "path": "vendor/nim-unittest2", + "url": "https://github.com/status-im/nim-unittest2.git", + "rev": "8b51e99b4a57fcfb31689230e75595f024543024" + } + , + { + "path": "vendor/nim-web3", + "url": "https://github.com/status-im/nim-web3.git", + "rev": "81ee8ce479d86acb73be7c4f365328e238d9b4a3" + } + , + { + "path": "vendor/nim-websock", + "url": "https://github.com/status-im/nim-websock.git", + "rev": "ebe308a79a7b440a11dfbe74f352be86a3883508" + } + , + { + "path": "vendor/nim-zlib", + "url": "https://github.com/status-im/nim-zlib.git", + "rev": "daa8723fd32299d4ca621c837430c29a5a11e19a" + } + , + { + "path": "vendor/nimbus-build-system", + "url": "https://github.com/status-im/nimbus-build-system.git", + "rev": "e6c2c9da39c2d368d9cf420ac22692e99715d22c" + } + , + { + "path": "vendor/nimcrypto", + "url": "https://github.com/cheatfate/nimcrypto.git", + "rev": "721fb99ee099b632eb86dfad1f0d96ee87583774" + } + , + { + "path": "vendor/nph", + "url": "https://github.com/arnetheduck/nph.git", + "rev": "c6e03162dc2820d3088660f644818d7040e95791" + } + , + { + "path": "vendor/waku-rlnv2-contract", + "url": "https://github.com/logos-messaging/waku-rlnv2-contract.git", + "rev": "8a338f354481e8a3f3d64a72e38fad4c62e32dcd" + } + , + { + "path": "vendor/zerokit", + "url": "https://github.com/vacp2p/zerokit.git", + "rev": "70c79fbc989d4f87d9352b2f4bddcb60ebe55b19" + } +] diff --git a/nix/zippy.nix b/nix/zippy.nix new file mode 100644 index 000000000..ec59dfc07 --- /dev/null +++ b/nix/zippy.nix @@ -0,0 +1,9 @@ +{ pkgs }: + +pkgs.fetchFromGitHub { + owner = "guzba"; + repo = "zippy"; + rev = "a99f6a7d8a8e3e0213b3cad0daf0ea974bf58e3f"; + # WARNING: Requires manual updates when Nim compiler version changes. + hash = "sha256-e2ma2Oyp0dlNx8pJsdZl5o5KnaoAX87tqfY0RLG3DZs="; +} \ No newline at end of file diff --git a/scripts/build_rln.sh b/scripts/build_rln.sh index 5e1b0caa5..35b5b8953 100755 --- a/scripts/build_rln.sh +++ b/scripts/build_rln.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash -# This script is used to build the rln library for the current platform, or download it from the -# release page if it is available. +# This script is used to build the rln library for the current platform. +# Previously downloaded prebuilt binaries, but due to compatibility issues +# we now always build from source. set -e @@ -14,41 +15,34 @@ output_filename=$3 [[ -z "${rln_version}" ]] && { echo "No rln version specified"; exit 1; } [[ -z "${output_filename}" ]] && { echo "No output filename specified"; exit 1; } -# Get the host triplet -host_triplet=$(rustc --version --verbose | awk '/host:/{print $2}') +echo "Building RLN library from source (version ${rln_version})..." -tarball="${host_triplet}" +# Check if submodule version = version in Makefile +cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" -tarball+="-rln.tar.gz" - -# Download the prebuilt rln library if it is available -if curl --silent --fail-with-body -L \ - "https://github.com/vacp2p/zerokit/releases/download/$rln_version/$tarball" \ - -o "${tarball}"; -then - echo "Downloaded ${tarball}" - tar -xzf "${tarball}" - mv "release/librln.a" "${output_filename}" - rm -rf "${tarball}" release +detected_OS=$(uname -s) +if [[ "$detected_OS" == MINGW* || "$detected_OS" == MSYS* ]]; then + submodule_version=$(cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" | sed -n 's/.*"name":"rln","version":"\([^"]*\)".*/\1/p') else - echo "Failed to download ${tarball}" - # Build rln instead - # first, check if submodule version = version in Makefile - cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" - - detected_OS=$(uname -s) - if [[ "$detected_OS" == MINGW* || "$detected_OS" == MSYS* ]]; then - submodule_version=$(cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" | sed -n 's/.*"name":"rln","version":"\([^"]*\)".*/\1/p') - else - submodule_version=$(cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" | jq -r '.packages[] | select(.name == "rln") | .version') - fi - - if [[ "v${submodule_version}" != "${rln_version}" ]]; then - echo "Submodule version (v${submodule_version}) does not match version in Makefile (${rln_version})" - echo "Please update the submodule to ${rln_version}" - exit 1 - fi - # if submodule version = version in Makefile, build rln - cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml" - cp "${build_dir}/target/release/librln.a" "${output_filename}" + submodule_version=$(cargo metadata --format-version=1 --no-deps --manifest-path "${build_dir}/rln/Cargo.toml" | jq -r '.packages[] | select(.name == "rln") | .version') fi + +if [[ "v${submodule_version}" != "${rln_version}" ]]; then + echo "Submodule version (v${submodule_version}) does not match version in Makefile (${rln_version})" + echo "Please update the submodule to ${rln_version}" + exit 1 +fi + +# Build rln from source. +# `stateless` feature: logos-delivery does not maintain a local Merkle tree +# (post-PR #3312); the contract is the source of truth and the path is fetched +# via getMerkleProof(index). The stateless build compiles out tree code. +# +# --no-default-features is required because zerokit's default features include +# `pmtree-ft` (a Merkle tree backend); `stateless` and any Merkle-tree feature +# are mutually exclusive (rln/src/lib.rs:32 compile_error). +cargo build --release -p rln --manifest-path "${build_dir}/rln/Cargo.toml" \ + --no-default-features --features stateless +cp "${build_dir}/target/release/librln.a" "${output_filename}" + +echo "Successfully built ${output_filename}" diff --git a/scripts/build_rln_android.sh b/scripts/build_rln_android.sh index 93a8c47ff..15b81ce9c 100755 --- a/scripts/build_rln_android.sh +++ b/scripts/build_rln_android.sh @@ -25,4 +25,3 @@ cargo clean cross rustc --release --lib --target=${android_arch} --crate-type=cdylib cp ../target/${android_arch}/release/librln.so ${output_dir}/. popd - diff --git a/scripts/generate_nix_submodules.sh b/scripts/generate_nix_submodules.sh new file mode 100755 index 000000000..51073294c --- /dev/null +++ b/scripts/generate_nix_submodules.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +# Generates nix/submodules.json from .gitmodules and git ls-tree. +# This allows Nix to fetch all git submodules without requiring +# locally initialized submodules or the '?submodules=1' URI flag. +# +# Usage: ./scripts/generate_nix_submodules.sh +# +# Run this script after: +# - Adding/removing submodules +# - Updating submodule commits (e.g. after 'make update') +# - Any change to .gitmodules +# +# Compatible with macOS bash 3.x (no associative arrays). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +OUTPUT="${REPO_ROOT}/nix/submodules.json" + +cd "$REPO_ROOT" + +TMP_URLS=$(mktemp) +TMP_REVS=$(mktemp) +trap 'rm -f "$TMP_URLS" "$TMP_REVS"' EXIT + +# Parse .gitmodules: extract (path, url) pairs +current_path="" +while IFS= read -r line; do + case "$line" in + *"path = "*) + current_path="${line#*path = }" + ;; + *"url = "*) + if [ -n "$current_path" ]; then + url="${line#*url = }" + url="${url%/}" + printf '%s\t%s\n' "$current_path" "$url" >> "$TMP_URLS" + current_path="" + fi + ;; + esac +done < .gitmodules + +# Get pinned commit hashes from git tree +git ls-tree HEAD vendor/ | while IFS= read -r tree_line; do + mode=$(echo "$tree_line" | awk '{print $1}') + type=$(echo "$tree_line" | awk '{print $2}') + hash=$(echo "$tree_line" | awk '{print $3}') + path=$(echo "$tree_line" | awk '{print $4}') + if [ "$type" = "commit" ]; then + path="${path%/}" + printf '%s\t%s\n' "$path" "$hash" >> "$TMP_REVS" + fi +done + +# Generate JSON by joining urls and revs on path +printf '[\n' > "$OUTPUT" +first=true + +sort "$TMP_URLS" | while IFS="$(printf '\t')" read -r path url; do + rev=$(grep "^${path} " "$TMP_REVS" | cut -f2 || true) + + if [ -z "$rev" ]; then + echo "WARNING: No commit hash found for submodule '$path', skipping" >&2 + continue + fi + + if [ "$first" = true ]; then + first=false + else + printf ' ,\n' >> "$OUTPUT" + fi + + printf ' {\n "path": "%s",\n "url": "%s",\n "rev": "%s"\n }\n' \ + "$path" "$url" "$rev" >> "$OUTPUT" +done + +printf ']\n' >> "$OUTPUT" + +count=$(grep -c '"path"' "$OUTPUT" || echo 0) +echo "Generated $OUTPUT with $count submodule entries" diff --git a/scripts/install_anvil.sh b/scripts/install_anvil.sh index 1bf4bd7b1..c573ac31c 100755 --- a/scripts/install_anvil.sh +++ b/scripts/install_anvil.sh @@ -2,14 +2,51 @@ # Install Anvil -if ! command -v anvil &> /dev/null; then +REQUIRED_FOUNDRY_VERSION="$1" + +if command -v anvil &> /dev/null; then + # Foundry is already installed; check the current version. + CURRENT_FOUNDRY_VERSION=$(anvil --version 2>/dev/null | awk '{print $2}') + + if [ -n "$CURRENT_FOUNDRY_VERSION" ]; then + # Compare CURRENT_FOUNDRY_VERSION < REQUIRED_FOUNDRY_VERSION using sort -V + lower_version=$(printf '%s\n%s\n' "$CURRENT_FOUNDRY_VERSION" "$REQUIRED_FOUNDRY_VERSION" | sort -V | head -n1) + + if [ "$lower_version" != "$REQUIRED_FOUNDRY_VERSION" ]; then + echo "Anvil is already installed with version $CURRENT_FOUNDRY_VERSION, which is older than the required $REQUIRED_FOUNDRY_VERSION. Please update Foundry manually if needed." + fi + fi +else BASE_DIR="${XDG_CONFIG_HOME:-$HOME}" FOUNDRY_DIR="${FOUNDRY_DIR:-"$BASE_DIR/.foundry"}" FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin" + echo "Installing Foundry..." curl -L https://foundry.paradigm.xyz | bash - # Extract the source path from the download result - echo "foundryup_path: $FOUNDRY_BIN_DIR" - # run foundryup - $FOUNDRY_BIN_DIR/foundryup + + # Add Foundry to PATH for this script session + export PATH="$FOUNDRY_BIN_DIR:$PATH" + + # Verify foundryup is available + if ! command -v foundryup >/dev/null 2>&1; then + echo "Error: foundryup installation failed or not found in $FOUNDRY_BIN_DIR" + exit 1 + fi + + # Run foundryup to install the required version + if [ -n "$REQUIRED_FOUNDRY_VERSION" ]; then + echo "Installing Foundry tools version $REQUIRED_FOUNDRY_VERSION..." + foundryup --install "$REQUIRED_FOUNDRY_VERSION" + else + echo "Installing latest Foundry tools..." + foundryup + fi + + # Verify anvil was installed + if ! command -v anvil >/dev/null 2>&1; then + echo "Error: anvil installation failed" + exit 1 + fi + + echo "Anvil successfully installed: $(anvil --version)" fi \ No newline at end of file diff --git a/scripts/install_nasm_in_windows.sh b/scripts/install_nasm_in_windows.sh new file mode 100644 index 000000000..2bba5ecd4 --- /dev/null +++ b/scripts/install_nasm_in_windows.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env sh +set -e + +NASM_VERSION="2.16.01" +NASM_ZIP="nasm-${NASM_VERSION}-win64.zip" +NASM_URL="https://www.nasm.us/pub/nasm/releasebuilds/${NASM_VERSION}/win64/${NASM_ZIP}" + +INSTALL_DIR="$HOME/.local/nasm" +BIN_DIR="$INSTALL_DIR/bin" + +echo "Installing NASM ${NASM_VERSION}..." + +# Create directories +mkdir -p "$BIN_DIR" +cd "$INSTALL_DIR" + +# Download +if [ ! -f "$NASM_ZIP" ]; then + echo "Downloading NASM..." + curl -LO "$NASM_URL" +fi + +# Extract +echo "Extracting..." +unzip -o "$NASM_ZIP" + +# Move binaries +cp nasm-*/nasm.exe "$BIN_DIR/" +cp nasm-*/ndisasm.exe "$BIN_DIR/" + +# Add to PATH in bashrc (idempotent) +if ! grep -q 'nasm/bin' "$HOME/.bashrc"; then + echo '' >> "$HOME/.bashrc" + echo '# NASM' >> "$HOME/.bashrc" + echo 'export PATH="$HOME/.local/nasm/bin:$PATH"' >> "$HOME/.bashrc" +fi + diff --git a/scripts/install_nim.sh b/scripts/install_nim.sh new file mode 100755 index 000000000..c8d0f439d --- /dev/null +++ b/scripts/install_nim.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Installs a specific Nim version. +# Usage: install_nim.sh +# +# Installs to ~/.nim/nim-/ and symlinks binaries into ~/.nimble/bin/, +# which is the idiomatic Nim location already on PATH. +# +# Pre-built binaries are downloaded from nim-lang.org when available. +# Falls back to building from source otherwise (e.g. macOS on older releases). + +set -e + +NIM_VERSION="${1:-}" + +if [ -z "${NIM_VERSION}" ]; then + echo "Usage: $0 " >&2 + exit 1 +fi + +# Check if the right version is already installed +nim_ver=$(nim --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true) +if [ "${nim_ver}" = "${NIM_VERSION}" ]; then + echo "Nim ${NIM_VERSION} already installed, skipping." + exit 0 +fi + +if [ -n "${nim_ver}" ]; then + newer=$(printf '%s\n%s\n' "${NIM_VERSION}" "${nim_ver}" | sort -V | tail -1) + if [ "${newer}" = "${nim_ver}" ]; then + echo "WARNING: Nim ${nim_ver} is installed; this repo is validated against ${NIM_VERSION}." >&2 + echo "WARNING: The build will proceed but may behave differently." >&2 + exit 0 + fi +fi + +OS=$(uname -s | tr 'A-Z' 'a-z' | sed 's/darwin/macosx/') +ARCH=$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/') + +NIM_DEST="${HOME}/.nim/nim-${NIM_VERSION}" +BINARY_URL="https://nim-lang.org/download/nim-${NIM_VERSION}-${OS}_${ARCH}.tar.xz" +WORK_DIR=$(mktemp -d) +trap 'rm -rf "${WORK_DIR}"' EXIT + +echo "Checking for pre-built Nim ${NIM_VERSION} (${OS}_${ARCH})..." +HTTP_STATUS=$(curl -sI "${BINARY_URL}" | head -1 | grep -oE '[0-9]{3}' || true) + +if [ "${HTTP_STATUS}" = "200" ]; then + echo "Downloading pre-built binary from ${BINARY_URL}..." + curl -fL "${BINARY_URL}" -o "${WORK_DIR}/nim.tar.xz" + tar -xJf "${WORK_DIR}/nim.tar.xz" -C "${WORK_DIR}" + rm -rf "${NIM_DEST}" + mkdir -p "${HOME}/.nim" + cp -r "${WORK_DIR}/nim-${NIM_VERSION}" "${NIM_DEST}" +else + echo "No pre-built binary found for ${OS}_${ARCH}. Building from source..." + SRC_URL="https://github.com/nim-lang/Nim/archive/refs/tags/v${NIM_VERSION}.tar.gz" + curl -fL "${SRC_URL}" -o "${WORK_DIR}/nim-src.tar.gz" + tar -xzf "${WORK_DIR}/nim-src.tar.gz" -C "${WORK_DIR}" + cd "${WORK_DIR}/Nim-${NIM_VERSION}" + sh build_all.sh + rm -rf "${NIM_DEST}" + mkdir -p "${HOME}/.nim" + cp -r "${WORK_DIR}/Nim-${NIM_VERSION}" "${NIM_DEST}" +fi + +mkdir -p "${HOME}/.nimble/bin" +for bin_path in "${NIM_DEST}/bin/"*; do + ln -sf "${bin_path}" "${HOME}/.nimble/bin/$(basename "${bin_path}")" +done + +echo "Nim ${NIM_VERSION} installed to ${NIM_DEST}" +echo "Binaries symlinked in ~/.nimble/bin — ensure it is in your PATH." diff --git a/scripts/install_pnpm.sh b/scripts/install_pnpm.sh index 34ba47b07..fcfc82ccd 100755 --- a/scripts/install_pnpm.sh +++ b/scripts/install_pnpm.sh @@ -1,8 +1,37 @@ #!/usr/bin/env bash # Install pnpm -if ! command -v pnpm &> /dev/null; then - echo "pnpm is not installed, installing it now..." - npm i pnpm --global + +REQUIRED_PNPM_VERSION="$1" + +if command -v pnpm &> /dev/null; then + # pnpm is already installed; check the current version. + CURRENT_PNPM_VERSION=$(pnpm --version 2>/dev/null) + + if [ -n "$CURRENT_PNPM_VERSION" ]; then + # Compare CURRENT_PNPM_VERSION < REQUIRED_PNPM_VERSION using sort -V + lower_version=$(printf '%s\n%s\n' "$CURRENT_PNPM_VERSION" "$REQUIRED_PNPM_VERSION" | sort -V | head -n1) + + if [ "$lower_version" != "$REQUIRED_PNPM_VERSION" ]; then + echo "pnpm is already installed with version $CURRENT_PNPM_VERSION, which is older than the required $REQUIRED_PNPM_VERSION. Please update pnpm manually if needed." + fi + fi +else + # Install pnpm using npm + if [ -n "$REQUIRED_PNPM_VERSION" ]; then + echo "Installing pnpm version $REQUIRED_PNPM_VERSION..." + npm install -g pnpm@$REQUIRED_PNPM_VERSION + else + echo "Installing latest pnpm..." + npm install -g pnpm + fi + + # Verify pnpm was installed + if ! command -v pnpm >/dev/null 2>&1; then + echo "Error: pnpm installation failed" + exit 1 + fi + + echo "pnpm successfully installed: $(pnpm --version)" fi diff --git a/scripts/install_rln_tests_dependencies.sh b/scripts/install_rln_tests_dependencies.sh index e19e0ef3c..c8c083b54 100755 --- a/scripts/install_rln_tests_dependencies.sh +++ b/scripts/install_rln_tests_dependencies.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash # Install Anvil -./scripts/install_anvil.sh +FOUNDRY_VERSION="$1" +./scripts/install_anvil.sh "$FOUNDRY_VERSION" -#Install pnpm -./scripts/install_pnpm.sh \ No newline at end of file +# Install pnpm +PNPM_VERSION="$2" +./scripts/install_pnpm.sh "$PNPM_VERSION" \ No newline at end of file diff --git a/scripts/libwaku_windows_setup.mk b/scripts/libwaku_windows_setup.mk new file mode 100644 index 000000000..503d0c405 --- /dev/null +++ b/scripts/libwaku_windows_setup.mk @@ -0,0 +1,53 @@ +# --------------------------------------------------------- +# Windows Setup Makefile +# --------------------------------------------------------- + +# Extend PATH (Make preserves environment variables) +export PATH := /c/msys64/usr/bin:/c/msys64/mingw64/bin:/c/msys64/usr/lib:/c/msys64/mingw64/lib:$(PATH) + +# Tools required +DEPS = gcc g++ make cmake cargo upx rustc python + +# Default target +.PHONY: windows-setup +windows-setup: check-deps update-submodules create-tmp libunwind miniupnpc libnatpmp + @echo "Windows setup completed successfully!" + +.PHONY: check-deps +check-deps: + @echo "Checking libwaku build dependencies..." + @for dep in $(DEPS); do \ + if ! which $$dep >/dev/null 2>&1; then \ + echo "✗ Missing dependency: $$dep"; \ + exit 1; \ + else \ + echo "✓ Found: $$dep"; \ + fi; \ + done + +.PHONY: update-submodules +update-submodules: + @echo "Updating libwaku git submodules..." + git submodule update --init --recursive + +.PHONY: create-tmp +create-tmp: + @echo "Creating tmp directory..." + mkdir -p tmp + +.PHONY: libunwind +libunwind: + @echo "Building libunwind..." + cd vendor/nim-libbacktrace && make all V=1 + +.PHONY: miniupnpc +miniupnpc: + @echo "Building miniupnpc..." + cd vendor/nim-nat-traversal/vendor/miniupnp/miniupnpc && \ + make -f Makefile.mingw CC=gcc CXX=g++ libminiupnpc.a V=1 + +.PHONY: libnatpmp +libnatpmp: + @echo "Building libnatpmp..." + cd vendor/nim-nat-traversal/vendor/libnatpmp-upstream && \ + make CC="gcc -fPIC -D_WIN32_WINNT=0x0600 -DNATPMP_STATICLIB" libnatpmp.a V=1 diff --git a/scripts/regenerate_anvil_state.sh b/scripts/regenerate_anvil_state.sh new file mode 100755 index 000000000..9474591d9 --- /dev/null +++ b/scripts/regenerate_anvil_state.sh @@ -0,0 +1,104 @@ +#!/usr/bin/env bash + +# Simple script to regenerate the Anvil state file +# This creates a state file compatible with the current Foundry version + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +STATE_DIR="$PROJECT_ROOT/tests/waku_rln_relay/anvil_state" +STATE_FILE="$STATE_DIR/state-deployed-contracts-mint-and-approved.json" +STATE_FILE_GZ="${STATE_FILE}.gz" + +echo "===================================" +echo "Anvil State File Regeneration Tool" +echo "===================================" +echo "" + +# Check if Foundry is installed +if ! command -v anvil &> /dev/null; then + echo "ERROR: anvil is not installed!" + echo "Please run: make rln-deps" + exit 1 +fi + +ANVIL_VERSION=$(anvil --version 2>/dev/null | head -n1) +echo "Using Foundry: $ANVIL_VERSION" +echo "" + +# Backup existing state file +if [ -f "$STATE_FILE_GZ" ]; then + BACKUP_FILE="${STATE_FILE_GZ}.backup-$(date +%Y%m%d-%H%M%S)" + echo "Backing up existing state file to: $(basename $BACKUP_FILE)" + cp "$STATE_FILE_GZ" "$BACKUP_FILE" +fi + +# Remove old state files +rm -f "$STATE_FILE" "$STATE_FILE_GZ" + +echo "" +echo "Running test to generate fresh state file..." +echo "This will:" +echo " 1. Build RLN library" +echo " 2. Start Anvil with state dump enabled" +echo " 3. Deploy contracts" +echo " 4. Save state and compress it" +echo "" + +cd "$PROJECT_ROOT" + +# Run a single test that deploys contracts +# The test framework will handle state dump +make test tests/waku_rln_relay/test_rln_group_manager_onchain.nim "RLN instances" || { + echo "" + echo "Test execution completed (exit status: $?)" + echo "Checking if state file was generated..." +} + +# Check if state file was created +if [ -f "$STATE_FILE" ]; then + echo "" + echo "✓ State file generated: $STATE_FILE" + + # Compress it + gzip -c "$STATE_FILE" > "$STATE_FILE_GZ" + echo "✓ Compressed: $STATE_FILE_GZ" + + # File sizes + STATE_SIZE=$(du -h "$STATE_FILE" | cut -f1) + GZ_SIZE=$(du -h "$STATE_FILE_GZ" | cut -f1) + echo "" + echo "File sizes:" + echo " Uncompressed: $STATE_SIZE" + echo " Compressed: $GZ_SIZE" + + # Optionally remove uncompressed + echo "" + read -p "Remove uncompressed state file? [y/N] " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + rm "$STATE_FILE" + echo "✓ Removed uncompressed file" + fi + + echo "" + echo "============================================" + echo "✓ SUCCESS! State file regenerated" + echo "============================================" + echo "" + echo "Next steps:" + echo " 1. Test locally: make test tests/node/test_wakunode_lightpush.nim" + echo " 2. If tests pass, commit: git add $STATE_FILE_GZ" + echo " 3. Push and verify CI passes" + echo "" +else + echo "" + echo "============================================" + echo "✗ ERROR: State file was not generated" + echo "============================================" + echo "" + echo "The state file should have been created at: $STATE_FILE" + echo "Please check the test output above for errors." + exit 1 +fi diff --git a/simulations/mixnet/config.toml b/simulations/mixnet/config.toml index 17e9242d3..5cd1aa936 100644 --- a/simulations/mixnet/config.toml +++ b/simulations/mixnet/config.toml @@ -1,16 +1,17 @@ -log-level = "INFO" +log-level = "TRACE" relay = true -#mix = true +mix = true filter = true -store = false +store = true lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9000 discv5-enr-auto-update = true +enable-kad-discovery = true rest = true rest-admin = true ports-shift = 1 @@ -18,8 +19,10 @@ num-shards-in-network = 1 shard = [0] agent-string = "nwaku-mix" nodekey = "f98e3fba96c32e8d1967d460f1b79457380e1a895f7971cecc8528abe733781a" -#mixkey = "a87db88246ec0eedda347b9b643864bee3d6933eb15ba41e6d58cb678d813258" -rendezvous = true +mixkey = "a87db88246ec0eedda347b9b643864bee3d6933eb15ba41e6d58cb678d813258" +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60001"] +ext-multiaddr-only = true ip-colocation-limit=0 diff --git a/simulations/mixnet/config1.toml b/simulations/mixnet/config1.toml index e06a527c1..73cccb8c6 100644 --- a/simulations/mixnet/config1.toml +++ b/simulations/mixnet/config1.toml @@ -1,17 +1,18 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true store = false lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9001 discv5-enr-auto-update = true discv5-bootstrap-node = ["enr:-LG4QBaAbcA921hmu3IrreLqGZ4y3VWCjBCgNN9mpX9vqkkbSrM3HJHZTXnb5iVXgc5pPtDhWLxkB6F3yY25hSwMezkEgmlkgnY0gmlwhH8AAAGKbXVsdGlhZGRyc4oACATAqEQ-BuphgnJzhQACAQAAiXNlY3AyNTZrMaEDpEW1UlUGHRJg6g_zGuCddKWmIUBGZCQX13xGfh9J6KiDdGNwguphg3VkcIIjKYV3YWt1Mg0"] +kad-bootstrap-node = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"] rest = true rest-admin = true ports-shift = 2 @@ -20,8 +21,10 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "09e9d134331953357bd38bbfce8edb377f4b6308b4f3bfbe85c610497053d684" mixkey = "c86029e02c05a7e25182974b519d0d52fcbafeca6fe191fbb64857fb05be1a53" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60002"] +ext-multiaddr-only = true ip-colocation-limit=0 #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config2.toml b/simulations/mixnet/config2.toml index 93822603b..c40e41103 100644 --- a/simulations/mixnet/config2.toml +++ b/simulations/mixnet/config2.toml @@ -1,17 +1,18 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true store = false lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9002 discv5-enr-auto-update = true discv5-bootstrap-node = ["enr:-LG4QBaAbcA921hmu3IrreLqGZ4y3VWCjBCgNN9mpX9vqkkbSrM3HJHZTXnb5iVXgc5pPtDhWLxkB6F3yY25hSwMezkEgmlkgnY0gmlwhH8AAAGKbXVsdGlhZGRyc4oACATAqEQ-BuphgnJzhQACAQAAiXNlY3AyNTZrMaEDpEW1UlUGHRJg6g_zGuCddKWmIUBGZCQX13xGfh9J6KiDdGNwguphg3VkcIIjKYV3YWt1Mg0"] +kad-bootstrap-node = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"] rest = false rest-admin = false ports-shift = 3 @@ -20,8 +21,10 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "ed54db994682e857d77cd6fb81be697382dc43aa5cd78e16b0ec8098549f860e" mixkey = "b858ac16bbb551c4b2973313b1c8c8f7ea469fca03f1608d200bbf58d388ec7f" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60003"] +ext-multiaddr-only = true ip-colocation-limit=0 #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config3.toml b/simulations/mixnet/config3.toml index 6f339dfff..80c19b34b 100644 --- a/simulations/mixnet/config3.toml +++ b/simulations/mixnet/config3.toml @@ -1,17 +1,18 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true store = false lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9003 discv5-enr-auto-update = true discv5-bootstrap-node = ["enr:-LG4QBaAbcA921hmu3IrreLqGZ4y3VWCjBCgNN9mpX9vqkkbSrM3HJHZTXnb5iVXgc5pPtDhWLxkB6F3yY25hSwMezkEgmlkgnY0gmlwhH8AAAGKbXVsdGlhZGRyc4oACATAqEQ-BuphgnJzhQACAQAAiXNlY3AyNTZrMaEDpEW1UlUGHRJg6g_zGuCddKWmIUBGZCQX13xGfh9J6KiDdGNwguphg3VkcIIjKYV3YWt1Mg0"] +kad-bootstrap-node = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"] rest = false rest-admin = false ports-shift = 4 @@ -20,8 +21,10 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "42f96f29f2d6670938b0864aced65a332dcf5774103b4c44ec4d0ea4ef3c47d6" mixkey = "d8bd379bb394b0f22dd236d63af9f1a9bc45266beffc3fbbe19e8b6575f2535b" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60004"] +ext-multiaddr-only = true ip-colocation-limit=0 #staticnode = ["/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o","/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu"] diff --git a/simulations/mixnet/config4.toml b/simulations/mixnet/config4.toml index 23115ac03..ed5b2dad0 100644 --- a/simulations/mixnet/config4.toml +++ b/simulations/mixnet/config4.toml @@ -1,17 +1,18 @@ -log-level = "INFO" +log-level = "TRACE" relay = true mix = true filter = true store = false lightpush = true max-connections = 150 -peer-exchange = true +peer-exchange = false metrics-logging = false cluster-id = 2 -discv5-discovery = true +discv5-discovery = false discv5-udp-port = 9004 discv5-enr-auto-update = true discv5-bootstrap-node = ["enr:-LG4QBaAbcA921hmu3IrreLqGZ4y3VWCjBCgNN9mpX9vqkkbSrM3HJHZTXnb5iVXgc5pPtDhWLxkB6F3yY25hSwMezkEgmlkgnY0gmlwhH8AAAGKbXVsdGlhZGRyc4oACATAqEQ-BuphgnJzhQACAQAAiXNlY3AyNTZrMaEDpEW1UlUGHRJg6g_zGuCddKWmIUBGZCQX13xGfh9J6KiDdGNwguphg3VkcIIjKYV3YWt1Mg0"] +kad-bootstrap-node = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o"] rest = false rest-admin = false ports-shift = 5 @@ -20,8 +21,10 @@ shard = [0] agent-string = "nwaku-mix" nodekey = "3ce887b3c34b7a92dd2868af33941ed1dbec4893b054572cd5078da09dd923d4" mixkey = "780fff09e51e98df574e266bf3266ec6a3a1ddfcf7da826a349a29c137009d49" -rendezvous = true +rendezvous = false listen-address = "127.0.0.1" nat = "extip:127.0.0.1" +ext-multiaddr = ["/ip4/127.0.0.1/tcp/60005"] +ext-multiaddr-only = true ip-colocation-limit=0 #staticnode = ["/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o", "/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA","/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f","/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF"] diff --git a/simulations/mixnet/run_chat_mix.sh b/simulations/mixnet/run_chat_mix.sh index 11a28c06b..f711c055e 100755 --- a/simulations/mixnet/run_chat_mix.sh +++ b/simulations/mixnet/run_chat_mix.sh @@ -1 +1,2 @@ -../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" +../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --kad-bootstrap-node="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" +#--mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" diff --git a/simulations/mixnet/run_chat_mix1.sh b/simulations/mixnet/run_chat_mix1.sh index 11a28c06b..7323bb3a9 100755 --- a/simulations/mixnet/run_chat_mix1.sh +++ b/simulations/mixnet/run_chat_mix1.sh @@ -1 +1,2 @@ -../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE --mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" +../../build/chat2mix --cluster-id=2 --num-shards-in-network=1 --shard=0 --servicenode="/ip4/127.0.0.1/tcp/60001/p2p/16Uiu2HAmPiEs2ozjjJF2iN2Pe2FYeMC9w4caRHKYdLdAfjgbWM6o" --log-level=TRACE +#--mixnode="/ip4/127.0.0.1/tcp/60002/p2p/16Uiu2HAmLtKaFaSWDohToWhWUZFLtqzYZGPFuXwKrojFVF6az5UF:9231e86da6432502900a84f867004ce78632ab52cd8e30b1ec322cd795710c2a" --mixnode="/ip4/127.0.0.1/tcp/60003/p2p/16Uiu2HAmTEDHwAziWUSz6ZE23h5vxG2o4Nn7GazhMor4bVuMXTrA:275cd6889e1f29ca48e5b9edb800d1a94f49f13d393a0ecf1a07af753506de6c" --mixnode="/ip4/127.0.0.1/tcp/60004/p2p/16Uiu2HAmPwRKZajXtfb1Qsv45VVfRZgK3ENdfmnqzSrVm3BczF6f:e0ed594a8d506681be075e8e23723478388fb182477f7a469309a25e7076fc18" --mixnode="/ip4/127.0.0.1/tcp/60005/p2p/16Uiu2HAmRhxmCHBYdXt1RibXrjAUNJbduAhzaTHwFCZT4qWnqZAu:8fd7a1a7c19b403d231452a9b1ea40eb1cc76f455d918ef8980e7685f9eeeb1f" diff --git a/simulations/mixnet/run_lp_service_node.sh b/simulations/mixnet/run_lp_service_node.sh deleted file mode 100755 index 1d005796e..000000000 --- a/simulations/mixnet/run_lp_service_node.sh +++ /dev/null @@ -1 +0,0 @@ -../../build/wakunode2 --config-file="config.toml" diff --git a/simulations/mixnet/run_mix_node.sh b/simulations/mixnet/run_mix_node.sh new file mode 100755 index 000000000..2b293540c --- /dev/null +++ b/simulations/mixnet/run_mix_node.sh @@ -0,0 +1 @@ +../../build/wakunode2 --config-file="config.toml" 2>&1 | tee mix_node.log diff --git a/simulations/mixnet/run_mix_node1.sh b/simulations/mixnet/run_mix_node1.sh index 024eb3f99..617312122 100755 --- a/simulations/mixnet/run_mix_node1.sh +++ b/simulations/mixnet/run_mix_node1.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config1.toml" +../../build/wakunode2 --config-file="config1.toml" 2>&1 | tee mix_node1.log diff --git a/simulations/mixnet/run_mix_node2.sh b/simulations/mixnet/run_mix_node2.sh index e55a9bac8..5fc2ef498 100755 --- a/simulations/mixnet/run_mix_node2.sh +++ b/simulations/mixnet/run_mix_node2.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config2.toml" +../../build/wakunode2 --config-file="config2.toml" 2>&1 | tee mix_node2.log diff --git a/simulations/mixnet/run_mix_node3.sh b/simulations/mixnet/run_mix_node3.sh index dca8119a3..d77d04c02 100755 --- a/simulations/mixnet/run_mix_node3.sh +++ b/simulations/mixnet/run_mix_node3.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config3.toml" +../../build/wakunode2 --config-file="config3.toml" 2>&1 | tee mix_node3.log diff --git a/simulations/mixnet/run_mix_node4.sh b/simulations/mixnet/run_mix_node4.sh index 9cf25158b..3a2b0299d 100755 --- a/simulations/mixnet/run_mix_node4.sh +++ b/simulations/mixnet/run_mix_node4.sh @@ -1 +1 @@ -../../build/wakunode2 --config-file="config4.toml" +../../build/wakunode2 --config-file="config4.toml" 2>&1 | tee mix_node4.log diff --git a/tests/all_tests_waku.nim b/tests/all_tests_waku.nim index 3d22cd9c2..e64922f4c 100644 --- a/tests/all_tests_waku.nim +++ b/tests/all_tests_waku.nim @@ -20,14 +20,7 @@ import ./waku_archive/test_driver_sqlite, ./waku_archive/test_retention_policy, ./waku_archive/test_waku_archive, - ./waku_archive/test_partition_manager, - ./waku_archive_legacy/test_driver_queue_index, - ./waku_archive_legacy/test_driver_queue_pagination, - ./waku_archive_legacy/test_driver_queue_query, - ./waku_archive_legacy/test_driver_queue, - ./waku_archive_legacy/test_driver_sqlite_query, - ./waku_archive_legacy/test_driver_sqlite, - ./waku_archive_legacy/test_waku_archive + ./waku_archive/test_partition_manager const os* {.strdefine.} = "" when os == "Linux" and @@ -37,8 +30,6 @@ when os == "Linux" and import ./waku_archive/test_driver_postgres_query, ./waku_archive/test_driver_postgres, - #./waku_archive_legacy/test_driver_postgres_query, - #./waku_archive_legacy/test_driver_postgres, ./factory/test_node_factory, ./wakunode_rest/test_rest_store, ./wakunode_rest/test_all @@ -50,20 +41,9 @@ import ./waku_store/test_waku_store, ./waku_store/test_wakunode_store -# Waku legacy store test suite -import - ./waku_store_legacy/test_client, - ./waku_store_legacy/test_rpc_codec, - ./waku_store_legacy/test_waku_store, - ./waku_store_legacy/test_wakunode_store - # Waku store sync suite import ./waku_store_sync/test_all -when defined(waku_exp_store_resume): - # TODO: Review store resume test cases (#1282) - import ./waku_store_legacy/test_resume - import ./node/test_all, ./waku_filter_v2/test_all, @@ -89,6 +69,7 @@ import ./test_waku_netconfig, ./test_waku_switch, ./test_waku_rendezvous, + ./test_waku_metadata, ./waku_discv5/test_waku_discv5 # Waku Keystore test suite @@ -104,3 +85,6 @@ import ./api/test_all # Waku tools tests import ./tools/test_all + +# Persistency library tests +import ./persistency/test_all diff --git a/tests/api/test_all.nim b/tests/api/test_all.nim index 99c1b3b4c..56be19c27 100644 --- a/tests/api/test_all.nim +++ b/tests/api/test_all.nim @@ -1,3 +1,9 @@ {.used.} -import ./test_entry_nodes, ./test_node_conf +import + ./test_entry_nodes, + ./test_node_conf, + ./test_api_send, + ./test_api_subscription, + ./test_api_receive, + ./test_api_health diff --git a/tests/api/test_api_health.nim b/tests/api/test_api_health.nim new file mode 100644 index 000000000..d949db24f --- /dev/null +++ b/tests/api/test_api_health.nim @@ -0,0 +1,295 @@ +{.used.} + +import std/[options, sequtils, times] +import chronos, testutils/unittests, stew/byteutils, libp2p/[switch, peerinfo] +import brokers/broker_context +import ../testlib/[common, wakucore, wakunode, testasync] + +import + waku, + waku/[waku_node, waku_core, waku_relay/protocol], + waku/node/health_monitor/[topic_health, health_status, protocol_health, health_report], + waku/requests/health_requests, + waku/requests/node_requests, + waku/events/health_events, + waku/common/waku_protocol, + waku/factory/waku_conf +import tools/confutils/cli_args + +const TestTimeout = chronos.seconds(10) +const DefaultShard = PubsubTopic("/waku/2/rs/3/0") +const TestContentTopic = ContentTopic("/waku/2/default-content/proto") + +proc dummyHandler( + topic: PubsubTopic, msg: WakuMessage +): Future[void] {.async, gcsafe.} = + discard + +proc waitForConnectionStatus( + brokerCtx: BrokerContext, expected: ConnectionStatus +) {.async.} = + var future = newFuture[void]("waitForConnectionStatus") + + let handler: EventConnectionStatusChangeListenerProc = proc( + e: EventConnectionStatusChange + ) {.async: (raises: []), gcsafe.} = + if not future.finished: + if e.connectionStatus == expected: + future.complete() + + let handle = EventConnectionStatusChange.listen(brokerCtx, handler).valueOr: + raiseAssert error + + try: + if not await future.withTimeout(TestTimeout): + raiseAssert "Timeout waiting for status: " & $expected + finally: + await EventConnectionStatusChange.dropListener(brokerCtx, handle) + +proc waitForShardHealthy( + brokerCtx: BrokerContext +): Future[EventShardTopicHealthChange] {.async.} = + var future = newFuture[EventShardTopicHealthChange]("waitForShardHealthy") + + let handler: EventShardTopicHealthChangeListenerProc = proc( + e: EventShardTopicHealthChange + ) {.async: (raises: []), gcsafe.} = + if not future.finished: + if e.health == TopicHealth.MINIMALLY_HEALTHY or + e.health == TopicHealth.SUFFICIENTLY_HEALTHY: + future.complete(e) + + let handle = EventShardTopicHealthChange.listen(brokerCtx, handler).valueOr: + raiseAssert error + + try: + if await future.withTimeout(TestTimeout): + return future.read() + else: + raiseAssert "Timeout waiting for shard health event" + finally: + await EventShardTopicHealthChange.dropListener(brokerCtx, handle) + +suite "LM API health checking": + var + serviceNode {.threadvar.}: WakuNode + client {.threadvar.}: Waku + servicePeerInfo {.threadvar.}: RemotePeerInfo + + asyncSetup: + lockNewGlobalBrokerContext: + serviceNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + (await serviceNode.mountRelay()).isOkOr: + raiseAssert error + serviceNode.mountMetadata(3, @[0'u16]).isOkOr: + raiseAssert error + await serviceNode.mountLibp2pPing() + await serviceNode.start() + + servicePeerInfo = serviceNode.peerInfo.toRemotePeerInfo() + serviceNode.wakuRelay.subscribe(DefaultShard, dummyHandler) + + lockNewGlobalBrokerContext: + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = Core + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = 1 + conf.rest = false + + client = (await createNode(conf)).valueOr: + raiseAssert error + (await startWaku(addr client)).isOkOr: + raiseAssert error + + asyncTeardown: + discard await client.stop() + await serviceNode.stop() + + asyncTest "RequestShardTopicsHealth, check PubsubTopic health": + client.node.wakuRelay.subscribe(DefaultShard, dummyHandler) + await client.node.connectToNodes(@[servicePeerInfo]) + + var isHealthy = false + let start = Moment.now() + while Moment.now() - start < TestTimeout: + let req = RequestShardTopicsHealth.request(client.brokerCtx, @[DefaultShard]).valueOr: + raiseAssert "RequestShardTopicsHealth failed" + + if req.topicHealth.len > 0: + let h = req.topicHealth[0].health + if h == TopicHealth.MINIMALLY_HEALTHY or h == TopicHealth.SUFFICIENTLY_HEALTHY: + isHealthy = true + break + await sleepAsync(chronos.milliseconds(100)) + + check isHealthy == true + + asyncTest "RequestShardTopicsHealth, check disconnected PubsubTopic": + const GhostShard = PubsubTopic("/waku/2/rs/1/666") + client.node.wakuRelay.subscribe(GhostShard, dummyHandler) + + let req = RequestShardTopicsHealth.request(client.brokerCtx, @[GhostShard]).valueOr: + raiseAssert "Request failed" + + check req.topicHealth.len > 0 + check req.topicHealth[0].health == TopicHealth.UNHEALTHY + + asyncTest "RequestProtocolHealth, check relay status": + await client.node.connectToNodes(@[servicePeerInfo]) + + var isReady = false + let start = Moment.now() + while Moment.now() - start < TestTimeout: + let relayReq = await RequestProtocolHealth.request( + client.brokerCtx, WakuProtocol.RelayProtocol + ) + if relayReq.isOk() and relayReq.get().healthStatus.health == HealthStatus.READY: + isReady = true + break + await sleepAsync(chronos.milliseconds(100)) + + check isReady == true + + let storeReq = + await RequestProtocolHealth.request(client.brokerCtx, WakuProtocol.StoreProtocol) + if storeReq.isOk(): + check storeReq.get().healthStatus.health != HealthStatus.READY + + asyncTest "RequestProtocolHealth, check unmounted protocol": + let req = + await RequestProtocolHealth.request(client.brokerCtx, WakuProtocol.StoreProtocol) + check req.isOk() + + let status = req.get().healthStatus + check status.health == HealthStatus.NOT_MOUNTED + check status.desc.isNone() + + asyncTest "RequestConnectionStatus, check connectivity state": + let initialReq = RequestConnectionStatus.request(client.brokerCtx).valueOr: + raiseAssert "RequestConnectionStatus failed" + check initialReq.connectionStatus == ConnectionStatus.Disconnected + + await client.node.connectToNodes(@[servicePeerInfo]) + + var isConnected = false + let start = Moment.now() + while Moment.now() - start < TestTimeout: + let req = RequestConnectionStatus.request(client.brokerCtx).valueOr: + raiseAssert "RequestConnectionStatus failed" + + if req.connectionStatus == ConnectionStatus.PartiallyConnected or + req.connectionStatus == ConnectionStatus.Connected: + isConnected = true + break + await sleepAsync(chronos.milliseconds(100)) + + check isConnected == true + + asyncTest "EventConnectionStatusChange, detect connect and disconnect": + let connectFuture = + waitForConnectionStatus(client.brokerCtx, ConnectionStatus.PartiallyConnected) + + await client.node.connectToNodes(@[servicePeerInfo]) + await connectFuture + + let disconnectFuture = + waitForConnectionStatus(client.brokerCtx, ConnectionStatus.Disconnected) + await client.node.disconnectNode(servicePeerInfo) + await disconnectFuture + + asyncTest "EventShardTopicHealthChange, detect health improvement": + client.node.wakuRelay.subscribe(DefaultShard, dummyHandler) + + let healthEventFuture = waitForShardHealthy(client.brokerCtx) + + await client.node.connectToNodes(@[servicePeerInfo]) + + let event = await healthEventFuture + check event.topic == DefaultShard + + asyncTest "RequestHealthReport, check aggregate report": + let req = await RequestHealthReport.request(client.brokerCtx) + + check req.isOk() + + let report = req.get().healthReport + check report.nodeHealth == HealthStatus.READY + check report.protocolsHealth.len > 0 + check report.protocolsHealth.anyIt(it.protocol == $WakuProtocol.RelayProtocol) + + asyncTest "RequestContentTopicsHealth, smoke test": + let fictionalTopic = ContentTopic("/waku/2/this-does-not-exist/proto") + + let req = RequestContentTopicsHealth.request(client.brokerCtx, @[fictionalTopic]) + + check req.isOk() + + let res = req.get() + check res.contentTopicHealth.len == 1 + check res.contentTopicHealth[0].topic == fictionalTopic + check res.contentTopicHealth[0].health == TopicHealth.NOT_SUBSCRIBED + + asyncTest "RequestContentTopicsHealth, core mode trivial 1-shard autosharding": + let cTopic = ContentTopic("/waku/2/my-content-topic/proto") + + let shardReq = + RequestRelayShard.request(client.brokerCtx, none(PubsubTopic), cTopic) + + check shardReq.isOk() + let targetShard = $shardReq.get().relayShard + + client.node.wakuRelay.subscribe(targetShard, dummyHandler) + serviceNode.wakuRelay.subscribe(targetShard, dummyHandler) + + await client.node.connectToNodes(@[servicePeerInfo]) + + var isHealthy = false + let start = Moment.now() + while Moment.now() - start < TestTimeout: + let req = RequestContentTopicsHealth.request(client.brokerCtx, @[cTopic]).valueOr: + raiseAssert "Request failed" + + if req.contentTopicHealth.len > 0: + let h = req.contentTopicHealth[0].health + if h == TopicHealth.MINIMALLY_HEALTHY or h == TopicHealth.SUFFICIENTLY_HEALTHY: + isHealthy = true + break + + await sleepAsync(chronos.milliseconds(100)) + + check isHealthy == true + + asyncTest "RequestProtocolHealth, edge mode smoke test": + var edgeWaku: Waku + + lockNewGlobalBrokerContext: + var edgeConf = defaultWakuNodeConf().valueOr: + raiseAssert error + edgeConf.mode = Edge + edgeConf.listenAddress = parseIpAddress("0.0.0.0") + edgeConf.tcpPort = Port(0) + edgeConf.discv5UdpPort = Port(0) + edgeConf.clusterId = 3'u16 + edgeConf.maxMessageSize = "150 KiB" + edgeConf.rest = false + + edgeWaku = (await createNode(edgeConf)).valueOr: + raiseAssert "Failed to create edge node: " & error + + (await startWaku(addr edgeWaku)).isOkOr: + raiseAssert "Failed to start edge waku: " & error + + let relayReq = await RequestProtocolHealth.request( + edgeWaku.brokerCtx, WakuProtocol.RelayProtocol + ) + check relayReq.isOk() + check relayReq.get().healthStatus.health == HealthStatus.NOT_MOUNTED + + check not edgeWaku.node.wakuFilterClient.isNil() + + discard await edgeWaku.stop() diff --git a/tests/api/test_api_receive.nim b/tests/api/test_api_receive.nim new file mode 100644 index 000000000..d6aa954a4 --- /dev/null +++ b/tests/api/test_api_receive.nim @@ -0,0 +1,199 @@ +{.used.} + +import std/[options, sequtils, net, sets] +import chronos, testutils/unittests, stew/byteutils +import libp2p/[peerid, peerinfo, crypto/crypto] +import brokers/broker_context +import ../testlib/[common, wakucore, wakunode, testasync] +import ../waku_archive/archive_utils + +import + waku, + waku/[ + waku_node, + waku_core, + events/message_events, + waku_relay/protocol, + waku_archive, + waku_archive/common as archive_common, + node/delivery_service/delivery_service, + node/delivery_service/recv_service, + ] +import waku/factory/waku_conf +import tools/confutils/cli_args + +const TestTimeout = chronos.seconds(60) + +type ReceiveEventListenerManager = ref object + brokerCtx: BrokerContext + receivedListener: MessageReceivedEventListener + receivedEvent: AsyncEvent + receivedMessages: seq[WakuMessage] + targetCount: int + +proc newReceiveEventListenerManager( + brokerCtx: BrokerContext, expectedCount: int = 1 +): ReceiveEventListenerManager = + let manager = ReceiveEventListenerManager( + brokerCtx: brokerCtx, receivedMessages: @[], targetCount: expectedCount + ) + manager.receivedEvent = newAsyncEvent() + + manager.receivedListener = MessageReceivedEvent + .listen( + brokerCtx, + proc(event: MessageReceivedEvent) {.async: (raises: []).} = + manager.receivedMessages.add(event.message) + if manager.receivedMessages.len >= manager.targetCount: + manager.receivedEvent.fire() + , + ) + .expect("Failed to listen to MessageReceivedEvent") + + return manager + +proc teardown(manager: ReceiveEventListenerManager) {.async.} = + await MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) + +proc waitForEvents( + manager: ReceiveEventListenerManager, timeout: Duration +): Future[bool] {.async.} = + return await manager.receivedEvent.wait().withTimeout(timeout) + +proc createApiNodeConf(numShards: uint16 = 1): WakuNodeConf = + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = cli_args.WakuMode.Core + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = numShards + conf.reliabilityEnabled = true + conf.rest = false + result = conf + +suite "Messaging API, Receive Service (store recovery)": + asyncTest "recv_service delivers store-recovered messages via MessageReceivedEvent": + ## Message gets archived before subscriber exists, checkStore() recovers it. + ## This is a regression test: it proves that messages recovered via store by + ## the RecvService (instead of receiving via a live relay sub) are actually + ## delivered via the MessageReceivedEvent API. + + let numShards: uint16 = 1 + let shards = @[PubsubTopic("/waku/2/rs/3/0")] + let shard = shards[0] + let testTopic = ContentTopic("/waku/2/recv-test/proto") + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + # store node has archive, store, relay + # it archives messages from relay and serves them to the + # subscriber's store client when it comes up (later) + var storeNode: WakuNode + lockNewGlobalBrokerContext: + storeNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + storeNode.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on storeNode" + ) + (await storeNode.mountRelay()).expect("Failed to mount relay on storeNode") + let archiveDriver = newSqliteArchiveDriver() + storeNode.mountArchive(archiveDriver).expect("Failed to mount archive") + await storeNode.mountStore() + await storeNode.mountLibp2pPing() + await storeNode.start() + + for s in shards: + storeNode.subscribe((kind: PubsubSub, topic: s), dummyHandler).expect( + "Failed to sub storeNode" + ) + + let storeNodePeerInfo = storeNode.peerInfo.toRemotePeerInfo() + + # publisher node (relay) + var publisher: WakuNode + lockNewGlobalBrokerContext: + publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on publisher" + ) + (await publisher.mountRelay()).expect("Failed to mount relay on publisher") + await publisher.mountLibp2pPing() + await publisher.start() + + for s in shards: + publisher.subscribe((kind: PubsubSub, topic: s), dummyHandler).expect( + "Failed to sub publisher" + ) + + # connect publisher to store so messages get archived + await publisher.connectToNodes(@[storeNodePeerInfo]) + + # wait for relay mesh + for _ in 0 ..< 50: + if publisher.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0: + break + await sleepAsync(100.milliseconds) + + # create the subscriber before publishing. + # RecvService captures startTimeToCheck at construction time; the + # message's timestamp must land after that point to fall inside + # checkStore's time window. + var subscriber: Waku + lockNewGlobalBrokerContext: + subscriber = (await createNode(createApiNodeConf(numShards))).expect( + "Failed to create subscriber" + ) + (await startWaku(addr subscriber)).expect("Failed to start subscriber") + + # publish after the subscriber exists but before it connects to the + # store; the message reaches the archive but the subscriber doesn't + # see it via live relay. + let missedPayload = "This message was missed".toBytes() + let missedMsg = WakuMessage( + payload: missedPayload, contentTopic: testTopic, version: 0, timestamp: now() + ) + discard (await publisher.publish(some(shard), missedMsg)).expect( + "Publish missed msg failed" + ) + + # wait for archive + block waitArchive: + for _ in 0 ..< 50: + let query = archive_common.ArchiveQuery( + includeData: false, contentTopics: @[testTopic], pubsubTopic: some(shard) + ) + let res = await storeNode.wakuArchive.findMessages(query) + if res.isOk() and res.get().hashes.len > 0: + break waitArchive + await sleepAsync(100.milliseconds) + raiseAssert "Message was not archived in time" + + # connect subscriber to store after the message is already archived so + # gossipsub doesn't replay it via the live path + await subscriber.node.connectToNodes(@[storeNodePeerInfo]) + + # subscribe to content topic + (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") + + # listen before triggering store check + let eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + # trigger store check, should recover and deliver via MessageReceivedEvent + await subscriber.deliveryService.recvService.checkStore() + + let received = await eventManager.waitForEvents(TestTimeout) + check received + check eventManager.receivedMessages.len == 1 + if eventManager.receivedMessages.len > 0: + check eventManager.receivedMessages[0].payload == missedPayload + + # cleanup + (await subscriber.stop()).expect("Failed to stop subscriber") + await publisher.stop() + await storeNode.stop() diff --git a/tests/api/test_api_send.nim b/tests/api/test_api_send.nim new file mode 100644 index 000000000..084119041 --- /dev/null +++ b/tests/api/test_api_send.nim @@ -0,0 +1,437 @@ +{.used.} + +import std/strutils +import chronos, testutils/unittests, stew/byteutils, libp2p/[switch, peerinfo] +import brokers/broker_context +import ../testlib/[common, wakucore, wakunode, testasync] +import ../waku_archive/archive_utils +import waku, waku/[waku_node, waku_core, waku_relay/protocol] +import waku/factory/waku_conf +import tools/confutils/cli_args + +type SendEventOutcome {.pure.} = enum + Sent + Propagated + Error + +type SendEventListenerManager = ref object + brokerCtx: BrokerContext + sentListener: MessageSentEventListener + errorListener: MessageErrorEventListener + propagatedListener: MessagePropagatedEventListener + sentFuture: Future[void] + errorFuture: Future[void] + propagatedFuture: Future[void] + sentCount: int + errorCount: int + propagatedCount: int + sentRequestIds: seq[RequestId] + errorRequestIds: seq[RequestId] + propagatedRequestIds: seq[RequestId] + +proc newSendEventListenerManager(brokerCtx: BrokerContext): SendEventListenerManager = + let manager = SendEventListenerManager(brokerCtx: brokerCtx) + manager.sentFuture = newFuture[void]("sentEvent") + manager.errorFuture = newFuture[void]("errorEvent") + manager.propagatedFuture = newFuture[void]("propagatedEvent") + + manager.sentListener = MessageSentEvent.listen( + brokerCtx, + proc(event: MessageSentEvent) {.async: (raises: []).} = + inc manager.sentCount + manager.sentRequestIds.add(event.requestId) + echo "SENT EVENT TRIGGERED (#", + manager.sentCount, "): requestId=", event.requestId + if not manager.sentFuture.finished(): + manager.sentFuture.complete() + , + ).valueOr: + raiseAssert error + + manager.errorListener = MessageErrorEvent.listen( + brokerCtx, + proc(event: MessageErrorEvent) {.async: (raises: []).} = + inc manager.errorCount + manager.errorRequestIds.add(event.requestId) + echo "ERROR EVENT TRIGGERED (#", manager.errorCount, "): ", event.error + if not manager.errorFuture.finished(): + manager.errorFuture.fail( + newException(CatchableError, "Error event triggered: " & event.error) + ) + , + ).valueOr: + raiseAssert error + + manager.propagatedListener = MessagePropagatedEvent.listen( + brokerCtx, + proc(event: MessagePropagatedEvent) {.async: (raises: []).} = + inc manager.propagatedCount + manager.propagatedRequestIds.add(event.requestId) + echo "PROPAGATED EVENT TRIGGERED (#", + manager.propagatedCount, "): requestId=", event.requestId + if not manager.propagatedFuture.finished(): + manager.propagatedFuture.complete() + , + ).valueOr: + raiseAssert error + + return manager + +proc teardown(manager: SendEventListenerManager) {.async.} = + await MessageSentEvent.dropListener(manager.brokerCtx, manager.sentListener) + await MessageErrorEvent.dropListener(manager.brokerCtx, manager.errorListener) + await MessagePropagatedEvent.dropListener( + manager.brokerCtx, manager.propagatedListener + ) + +proc waitForEvents( + manager: SendEventListenerManager, timeout: Duration +): Future[bool] {.async.} = + return await allFutures( + manager.sentFuture, manager.propagatedFuture, manager.errorFuture + ) + .withTimeout(timeout) + +proc outcomes(manager: SendEventListenerManager): set[SendEventOutcome] = + if manager.sentFuture.completed(): + result.incl(SendEventOutcome.Sent) + if manager.propagatedFuture.completed(): + result.incl(SendEventOutcome.Propagated) + if manager.errorFuture.failed(): + result.incl(SendEventOutcome.Error) + +proc validate(manager: SendEventListenerManager, expected: set[SendEventOutcome]) = + echo "EVENT COUNTS: sent=", + manager.sentCount, ", propagated=", manager.propagatedCount, ", error=", + manager.errorCount + check manager.outcomes() == expected + +proc validate( + manager: SendEventListenerManager, + expected: set[SendEventOutcome], + expectedRequestId: RequestId, +) = + manager.validate(expected) + for requestId in manager.sentRequestIds: + check requestId == expectedRequestId + for requestId in manager.propagatedRequestIds: + check requestId == expectedRequestId + for requestId in manager.errorRequestIds: + check requestId == expectedRequestId + +proc createApiNodeConf(mode: cli_args.WakuMode = cli_args.WakuMode.Core): WakuNodeConf = + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = mode + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = 1 + conf.reliabilityEnabled = true + conf.rest = false + result = conf + +suite "Waku API - Send": + var + relayNode1 {.threadvar.}: WakuNode + relayNode1PeerInfo {.threadvar.}: RemotePeerInfo + relayNode1PeerId {.threadvar.}: PeerId + + relayNode2 {.threadvar.}: WakuNode + relayNode2PeerInfo {.threadvar.}: RemotePeerInfo + relayNode2PeerId {.threadvar.}: PeerId + + lightpushNode {.threadvar.}: WakuNode + lightpushNodePeerInfo {.threadvar.}: RemotePeerInfo + lightpushNodePeerId {.threadvar.}: PeerId + + storeNode {.threadvar.}: WakuNode + storeNodePeerInfo {.threadvar.}: RemotePeerInfo + storeNodePeerId {.threadvar.}: PeerId + + asyncSetup: + lockNewGlobalBrokerContext: + relayNode1 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + relayNode1.mountMetadata(3, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await relayNode1.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + await relayNode1.mountLibp2pPing() + await relayNode1.start() + + lockNewGlobalBrokerContext: + relayNode2 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + relayNode2.mountMetadata(3, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await relayNode2.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + await relayNode2.mountLibp2pPing() + await relayNode2.start() + + lockNewGlobalBrokerContext: + lightpushNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + lightpushNode.mountMetadata(3, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await lightpushNode.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + (await lightpushNode.mountLightPush()).isOkOr: + raiseAssert "Failed to mount lightpush" + await lightpushNode.mountLibp2pPing() + await lightpushNode.start() + + lockNewGlobalBrokerContext: + storeNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + storeNode.mountMetadata(3, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await storeNode.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + # Mount archive so store can persist messages + let archiveDriver = newSqliteArchiveDriver() + storeNode.mountArchive(archiveDriver).isOkOr: + raiseAssert "Failed to mount archive: " & error + await storeNode.mountStore() + await storeNode.mountLibp2pPing() + await storeNode.start() + + relayNode1PeerInfo = relayNode1.peerInfo.toRemotePeerInfo() + relayNode1PeerId = relayNode1.peerInfo.peerId + + relayNode2PeerInfo = relayNode2.peerInfo.toRemotePeerInfo() + relayNode2PeerId = relayNode2.peerInfo.peerId + + lightpushNodePeerInfo = lightpushNode.peerInfo.toRemotePeerInfo() + lightpushNodePeerId = lightpushNode.peerInfo.peerId + + storeNodePeerInfo = storeNode.peerInfo.toRemotePeerInfo() + storeNodePeerId = storeNode.peerInfo.peerId + + # Subscribe all relay nodes to the default shard topic + const testPubsubTopic = PubsubTopic("/waku/2/rs/3/0") + proc dummyHandler( + topic: PubsubTopic, msg: WakuMessage + ): Future[void] {.async, gcsafe.} = + discard + + relayNode1.subscribe((kind: PubsubSub, topic: testPubsubTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe relayNode1: " & error + relayNode2.subscribe((kind: PubsubSub, topic: testPubsubTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe relayNode2: " & error + + lightpushNode.subscribe((kind: PubsubSub, topic: testPubsubTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe lightpushNode: " & error + storeNode.subscribe((kind: PubsubSub, topic: testPubsubTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe storeNode: " & error + + # Subscribe all relay nodes to the default shard topic + await relayNode1.connectToNodes(@[relayNode2PeerInfo, storeNodePeerInfo]) + await lightpushNode.connectToNodes(@[relayNode2PeerInfo]) + + asyncTeardown: + await allFutures( + relayNode1.stop(), relayNode2.stop(), lightpushNode.stop(), storeNode.stop() + ) + + asyncTest "Check API availability (unhealthy node)": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + # node is not connected ! + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let sendResult = await node.send(envelope) + + # TODO: The API is not enforcing a health check before the send, + # so currently this test cannot successfully fail to send. + check sendResult.isOk() + + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send fully validated": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes( + @[relayNode1PeerInfo, lightpushNodePeerInfo, storeNodePeerInfo] + ) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + await eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + # Wait for events with timeout + const eventTimeout = 10.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate( + {SendEventOutcome.Sent, SendEventOutcome.Propagated}, requestId + ) + + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send only propagates": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes(@[relayNode1PeerInfo]) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + await eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + # Wait for events with timeout + const eventTimeout = 10.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate({SendEventOutcome.Propagated}, requestId) + + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send only propagates fallback to lightpush": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes(@[lightpushNodePeerInfo]) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + await eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + # Wait for events with timeout + const eventTimeout = 10.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate({SendEventOutcome.Propagated}, requestId) + + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send fully validates fallback to lightpush": + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf())).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes(@[lightpushNodePeerInfo, storeNodePeerInfo]) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + await eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + # Wait for events with timeout + const eventTimeout = 10.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate( + {SendEventOutcome.Propagated, SendEventOutcome.Sent}, requestId + ) + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error + + asyncTest "Send fails with event": + var fakeLightpushNode: WakuNode + lockNewGlobalBrokerContext: + fakeLightpushNode = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + fakeLightpushNode.mountMetadata(3, @[0'u16]).isOkOr: + raiseAssert "Failed to mount metadata: " & error + (await fakeLightpushNode.mountRelay()).isOkOr: + raiseAssert "Failed to mount relay" + (await fakeLightpushNode.mountLightPush()).isOkOr: + raiseAssert "Failed to mount lightpush" + await fakeLightpushNode.mountLibp2pPing() + await fakeLightpushNode.start() + let fakeLightpushNodePeerInfo = fakeLightpushNode.peerInfo.toRemotePeerInfo() + proc dummyHandler( + topic: PubsubTopic, msg: WakuMessage + ): Future[void] {.async, gcsafe.} = + discard + + fakeLightpushNode.subscribe( + (kind: PubsubSub, topic: PubsubTopic("/waku/2/rs/3/0")), dummyHandler + ).isOkOr: + raiseAssert "Failed to subscribe fakeLightpushNode: " & error + + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(createApiNodeConf(cli_args.WakuMode.Edge))).valueOr: + raiseAssert error + (await startWaku(addr node)).isOkOr: + raiseAssert "Failed to start Waku node: " & error + + await node.node.connectToNodes(@[fakeLightpushNodePeerInfo]) + + let eventManager = newSendEventListenerManager(node.brokerCtx) + defer: + await eventManager.teardown() + + let envelope = MessageEnvelope.init( + ContentTopic("/waku/2/default-content/proto"), "test payload" + ) + + let requestId = (await node.send(envelope)).valueOr: + raiseAssert error + + echo "Sent message with requestId=", requestId + # Wait for events with timeout + const eventTimeout = 62.seconds + discard await eventManager.waitForEvents(eventTimeout) + + eventManager.validate({SendEventOutcome.Error}, requestId) + (await node.stop()).isOkOr: + raiseAssert "Failed to stop node: " & error diff --git a/tests/api/test_api_subscription.nim b/tests/api/test_api_subscription.nim new file mode 100644 index 000000000..32d4e742f --- /dev/null +++ b/tests/api/test_api_subscription.nim @@ -0,0 +1,809 @@ +{.used.} + +import std/[strutils, sequtils, net, options, sets, tables] +import chronos, testutils/unittests, stew/byteutils +import libp2p/[peerid, peerinfo, multiaddress, crypto/crypto] +import brokers/broker_context +import ../testlib/[common, wakucore, wakunode, testasync] + +import + waku, + waku/[ + waku_node, + waku_core, + events/message_events, + waku_relay/protocol, + node/kernel_api/filter, + node/delivery_service/subscription_manager, + ] +import waku/factory/waku_conf +import tools/confutils/cli_args + +const TestTimeout = chronos.seconds(10) +const NegativeTestTimeout = chronos.seconds(2) + +type ReceiveEventListenerManager = ref object + brokerCtx: BrokerContext + receivedListener: MessageReceivedEventListener + receivedEvent: AsyncEvent + receivedMessages: seq[WakuMessage] + targetCount: int + +proc newReceiveEventListenerManager( + brokerCtx: BrokerContext, expectedCount: int = 1 +): ReceiveEventListenerManager = + let manager = ReceiveEventListenerManager( + brokerCtx: brokerCtx, receivedMessages: @[], targetCount: expectedCount + ) + manager.receivedEvent = newAsyncEvent() + + manager.receivedListener = MessageReceivedEvent + .listen( + brokerCtx, + proc(event: MessageReceivedEvent) {.async: (raises: []).} = + manager.receivedMessages.add(event.message) + + if manager.receivedMessages.len >= manager.targetCount: + manager.receivedEvent.fire() + , + ) + .expect("Failed to listen to MessageReceivedEvent") + + return manager + +proc teardown(manager: ReceiveEventListenerManager) {.async.} = + await MessageReceivedEvent.dropListener(manager.brokerCtx, manager.receivedListener) + +proc waitForEvents( + manager: ReceiveEventListenerManager, timeout: Duration +): Future[bool] {.async.} = + return await manager.receivedEvent.wait().withTimeout(timeout) + +type TestNetwork = ref object + publisher: WakuNode # Relay node that publishes messages in tests. + meshBuddy: WakuNode # Extra relay peer for publisher's mesh (Edge tests only). + subscriber: Waku + # The receiver node in tests. Edge node in edge tests, Core node in relay tests. + publisherPeerInfo: RemotePeerInfo + +proc createApiNodeConf( + mode: cli_args.WakuMode = cli_args.WakuMode.Core, numShards: uint16 = 1 +): WakuNodeConf = + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = mode + conf.listenAddress = parseIpAddress("0.0.0.0") + conf.tcpPort = Port(0) + conf.discv5UdpPort = Port(0) + conf.clusterId = 3'u16 + conf.numShardsInNetwork = numShards + conf.reliabilityEnabled = true + conf.rest = false + result = conf + +proc setupSubscriberNode(conf: WakuNodeConf): Future[Waku] {.async.} = + var node: Waku + lockNewGlobalBrokerContext: + node = (await createNode(conf)).expect("Failed to create subscriber node") + (await startWaku(addr node)).expect("Failed to start subscriber node") + return node + +proc setupNetwork( + numShards: uint16 = 1, mode: cli_args.WakuMode = cli_args.WakuMode.Core +): Future[TestNetwork] {.async.} = + var net = TestNetwork() + + lockNewGlobalBrokerContext: + net.publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + net.publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata" + ) + (await net.publisher.mountRelay()).expect("Failed to mount relay") + if mode == cli_args.WakuMode.Edge: + await net.publisher.mountFilter() + await net.publisher.mountLibp2pPing() + await net.publisher.start() + + net.publisherPeerInfo = net.publisher.peerInfo.toRemotePeerInfo() + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + var shards: seq[PubsubTopic] + for i in 0 ..< numShards.int: + shards.add(PubsubTopic("/waku/2/rs/3/" & $i)) + + for shard in shards: + net.publisher.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub publisher" + ) + + if mode == cli_args.WakuMode.Edge: + lockNewGlobalBrokerContext: + net.meshBuddy = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + net.meshBuddy.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on meshBuddy" + ) + (await net.meshBuddy.mountRelay()).expect("Failed to mount relay on meshBuddy") + await net.meshBuddy.start() + + for shard in shards: + net.meshBuddy.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub meshBuddy" + ) + + await net.meshBuddy.connectToNodes(@[net.publisherPeerInfo]) + + net.subscriber = await setupSubscriberNode(createApiNodeConf(mode, numShards)) + + await net.subscriber.node.connectToNodes(@[net.publisherPeerInfo]) + + return net + +proc teardown(net: TestNetwork) {.async.} = + if not isNil(net.subscriber): + (await net.subscriber.stop()).expect("Failed to stop subscriber node") + net.subscriber = nil + + if not isNil(net.meshBuddy): + await net.meshBuddy.stop() + net.meshBuddy = nil + + if not isNil(net.publisher): + await net.publisher.stop() + net.publisher = nil + +proc getRelayShard(node: WakuNode, contentTopic: ContentTopic): PubsubTopic = + let autoSharding = node.wakuAutoSharding.get() + let shardObj = autoSharding.getShard(contentTopic).expect("Failed to get shard") + return PubsubTopic($shardObj) + +proc waitForMesh(node: WakuNode, shard: PubsubTopic) {.async.} = + for _ in 0 ..< 50: + if node.wakuRelay.getNumPeersInMesh(shard).valueOr(0) > 0: + return + await sleepAsync(100.milliseconds) + raise newException(ValueError, "GossipSub Mesh failed to stabilize on " & shard) + +proc waitForEdgeSubs(w: Waku, shard: PubsubTopic) {.async.} = + let sm = w.deliveryService.subscriptionManager + for _ in 0 ..< 50: + if sm.edgeFilterPeerCount(shard) > 0: + return + await sleepAsync(100.milliseconds) + raise newException(ValueError, "Edge filter subscription failed on " & shard) + +proc publishToMesh( + net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte] +): Future[Result[int, string]] {.async.} = + # Publishes a message from "publisher" via relay into the gossipsub mesh. + let shard = net.subscriber.node.getRelayShard(contentTopic) + await waitForMesh(net.publisher, shard) + let msg = WakuMessage( + payload: payload, contentTopic: contentTopic, version: 0, timestamp: now() + ) + return await net.publisher.publish(some(shard), msg) + +proc publishToMeshAfterEdgeReady( + net: TestNetwork, contentTopic: ContentTopic, payload: seq[byte] +): Future[Result[int, string]] {.async.} = + # First, ensure "subscriber" node (an edge node) is subscribed and ready to receive. + # Afterwards, "publisher" (relay node) sends the message in the gossipsub network. + let shard = net.subscriber.node.getRelayShard(contentTopic) + await waitForEdgeSubs(net.subscriber, shard) + return await net.publishToMesh(contentTopic, payload) + +suite "Messaging API, SubscriptionManager": + asyncTest "Subscription API, relay node auto subscribe and receive message": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/test-content/proto") + (await net.subscriber.subscribe(testTopic)).expect( + "subscriberNode failed to subscribe" + ) + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + discard (await net.publishToMesh(testTopic, "Hello, world!".toBytes())).expect( + "Publish failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 1 + check eventManager.receivedMessages[0].contentTopic == testTopic + + asyncTest "Subscription API, relay node ignores unsubscribed content topics on same shard": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let subbedTopic = ContentTopic("/waku/2/subbed-topic/proto") + let ignoredTopic = ContentTopic("/waku/2/ignored-topic/proto") + (await net.subscriber.subscribe(subbedTopic)).expect("failed to subscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + discard (await net.publishToMesh(ignoredTopic, "Ghost Msg".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, relay node unsubscribe stops message receipt": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/unsub-test/proto") + + (await net.subscriber.subscribe(testTopic)).expect("failed to subscribe") + net.subscriber.unsubscribe(testTopic).expect("failed to unsubscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + discard (await net.publishToMesh(testTopic, "Should be dropped".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, overlapping topics on same shard maintain correct isolation": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let topicA = ContentTopic("/waku/2/topic-a/proto") + let topicB = ContentTopic("/waku/2/topic-b/proto") + (await net.subscriber.subscribe(topicA)).expect("failed to sub A") + (await net.subscriber.subscribe(topicB)).expect("failed to sub B") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + net.subscriber.unsubscribe(topicA).expect("failed to unsub A") + + discard (await net.publishToMesh(topicA, "Dropped Message".toBytes())).expect( + "Publish A failed" + ) + discard + (await net.publishToMesh(topicB, "Kept Msg".toBytes())).expect("Publish B failed") + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 1 + check eventManager.receivedMessages[0].contentTopic == topicB + + asyncTest "Subscription API, redundant subs tolerated and subs are removed": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let glitchTopic = ContentTopic("/waku/2/glitch/proto") + + (await net.subscriber.subscribe(glitchTopic)).expect("failed to sub") + (await net.subscriber.subscribe(glitchTopic)).expect("failed to double sub") + net.subscriber.unsubscribe(glitchTopic).expect("failed to unsub") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + discard (await net.publishToMesh(glitchTopic, "Ghost Msg".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, resubscribe to an unsubscribed topic": + let net = await setupNetwork(1) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/resub-test/proto") + + # Subscribe + (await net.subscriber.subscribe(testTopic)).expect("Initial sub failed") + + var eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + discard + (await net.publishToMesh(testTopic, "Msg 1".toBytes())).expect("Pub 1 failed") + + require await eventManager.waitForEvents(TestTimeout) + await eventManager.teardown() + + # Unsubscribe and verify teardown + net.subscriber.unsubscribe(testTopic).expect("Unsub failed") + eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + + discard + (await net.publishToMesh(testTopic, "Ghost".toBytes())).expect("Ghost pub failed") + + check not await eventManager.waitForEvents(NegativeTestTimeout) + await eventManager.teardown() + + # Resubscribe + (await net.subscriber.subscribe(testTopic)).expect("Resub failed") + eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + + discard + (await net.publishToMesh(testTopic, "Msg 2".toBytes())).expect("Pub 2 failed") + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "Msg 2".toBytes() + + asyncTest "Subscription API, two content topics in different shards": + let net = await setupNetwork(8) + defer: + await net.teardown() + + var topicA = ContentTopic("/appA/2/shard-test-a/proto") + var topicB = ContentTopic("/appB/2/shard-test-b/proto") + + # generate two content topics that land in two different shards + var i = 0 + while net.subscriber.node.getRelayShard(topicA) == + net.subscriber.node.getRelayShard(topicB): + topicB = ContentTopic("/appB" & $i & "/2/shard-test-b/proto") + inc i + + (await net.subscriber.subscribe(topicA)).expect("failed to sub A") + (await net.subscriber.subscribe(topicB)).expect("failed to sub B") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 2) + defer: + await eventManager.teardown() + + discard (await net.publishToMesh(topicA, "Msg on Shard A".toBytes())).expect( + "Publish A failed" + ) + discard (await net.publishToMesh(topicB, "Msg on Shard B".toBytes())).expect( + "Publish B failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 2 + + asyncTest "Subscription API, many content topics in many shards": + let net = await setupNetwork(8) + defer: + await net.teardown() + + var allTopics: seq[ContentTopic] + for i in 0 ..< 100: + allTopics.add(ContentTopic("/stress-app-" & $i & "/2/state-test/proto")) + + var activeSubs: seq[ContentTopic] + + proc verifyNetworkState(expected: seq[ContentTopic]) {.async.} = + let eventManager = + newReceiveEventListenerManager(net.subscriber.brokerCtx, expected.len) + + for topic in allTopics: + discard (await net.publishToMesh(topic, "Stress Payload".toBytes())).expect( + "publish failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + + # here we just give a chance for any messages that we don't expect to arrive + await sleepAsync(1.seconds) + await eventManager.teardown() + + # weak check (but catches most bugs) + require eventManager.receivedMessages.len == expected.len + + # strict expected receipt test + var receivedTopics = initHashSet[ContentTopic]() + for msg in eventManager.receivedMessages: + receivedTopics.incl(msg.contentTopic) + var expectedTopics = initHashSet[ContentTopic]() + for t in expected: + expectedTopics.incl(t) + + check receivedTopics == expectedTopics + + # subscribe to all content topics we generated + for t in allTopics: + (await net.subscriber.subscribe(t)).expect("sub failed") + activeSubs.add(t) + + await verifyNetworkState(activeSubs) + + # unsubscribe from some content topics + for i in 0 ..< 50: + let t = allTopics[i] + net.subscriber.unsubscribe(t).expect("unsub failed") + + let idx = activeSubs.find(t) + if idx >= 0: + activeSubs.del(idx) + + await verifyNetworkState(activeSubs) + + # re-subscribe to some content topics + for i in 0 ..< 25: + let t = allTopics[i] + (await net.subscriber.subscribe(t)).expect("resub failed") + activeSubs.add(t) + + await verifyNetworkState(activeSubs) + + asyncTest "Subscription API, edge node subscribe and receive message": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/test-content/proto") + (await net.subscriber.subscribe(testTopic)).expect("failed to subscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + discard (await net.publishToMeshAfterEdgeReady(testTopic, "Hello, edge!".toBytes())).expect( + "Publish failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 1 + check eventManager.receivedMessages[0].contentTopic == testTopic + + asyncTest "Subscription API, edge node ignores unsubscribed content topics": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let subbedTopic = ContentTopic("/waku/2/subbed-topic/proto") + let ignoredTopic = ContentTopic("/waku/2/ignored-topic/proto") + (await net.subscriber.subscribe(subbedTopic)).expect("failed to subscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + discard (await net.publishToMesh(ignoredTopic, "Ghost Msg".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, edge node unsubscribe stops message receipt": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/unsub-test/proto") + + (await net.subscriber.subscribe(testTopic)).expect("failed to subscribe") + net.subscriber.unsubscribe(testTopic).expect("failed to unsubscribe") + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + discard (await net.publishToMesh(testTopic, "Should be dropped".toBytes())).expect( + "Publish failed" + ) + + check not await eventManager.waitForEvents(NegativeTestTimeout) + check eventManager.receivedMessages.len == 0 + + asyncTest "Subscription API, edge node overlapping topics isolation": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let topicA = ContentTopic("/waku/2/topic-a/proto") + let topicB = ContentTopic("/waku/2/topic-b/proto") + (await net.subscriber.subscribe(topicA)).expect("failed to sub A") + (await net.subscriber.subscribe(topicB)).expect("failed to sub B") + + let shard = net.subscriber.node.getRelayShard(topicA) + await waitForEdgeSubs(net.subscriber, shard) + + let eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + defer: + await eventManager.teardown() + + net.subscriber.unsubscribe(topicA).expect("failed to unsub A") + + discard (await net.publishToMesh(topicA, "Dropped Message".toBytes())).expect( + "Publish A failed" + ) + discard + (await net.publishToMesh(topicB, "Kept Msg".toBytes())).expect("Publish B failed") + + require await eventManager.waitForEvents(TestTimeout) + require eventManager.receivedMessages.len == 1 + check eventManager.receivedMessages[0].contentTopic == topicB + + asyncTest "Subscription API, edge node resubscribe after unsubscribe": + let net = await setupNetwork(1, cli_args.WakuMode.Edge) + defer: + await net.teardown() + + let testTopic = ContentTopic("/waku/2/resub-test/proto") + + (await net.subscriber.subscribe(testTopic)).expect("Initial sub failed") + + var eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + discard (await net.publishToMeshAfterEdgeReady(testTopic, "Msg 1".toBytes())).expect( + "Pub 1 failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + await eventManager.teardown() + + net.subscriber.unsubscribe(testTopic).expect("Unsub failed") + eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + + discard + (await net.publishToMesh(testTopic, "Ghost".toBytes())).expect("Ghost pub failed") + + check not await eventManager.waitForEvents(NegativeTestTimeout) + await eventManager.teardown() + + (await net.subscriber.subscribe(testTopic)).expect("Resub failed") + eventManager = newReceiveEventListenerManager(net.subscriber.brokerCtx, 1) + + discard (await net.publishToMeshAfterEdgeReady(testTopic, "Msg 2".toBytes())).expect( + "Pub 2 failed" + ) + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "Msg 2".toBytes() + + asyncTest "Subscription API, edge node failover after service peer dies": + # NOTE: This test is a bit more verbose because it defines a custom topology. + # It doesn't use the shared TestNetwork helper. + # This mounts two service peers for the edge node then fails one. + let numShards: uint16 = 1 + let shards = @[PubsubTopic("/waku/2/rs/3/0")] + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + var publisher: WakuNode + lockNewGlobalBrokerContext: + publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on publisher" + ) + (await publisher.mountRelay()).expect("Failed to mount relay on publisher") + await publisher.mountFilter() + await publisher.mountLibp2pPing() + await publisher.start() + + for shard in shards: + publisher.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub publisher" + ) + + let publisherPeerInfo = publisher.peerInfo.toRemotePeerInfo() + + var meshBuddy: WakuNode + lockNewGlobalBrokerContext: + meshBuddy = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + meshBuddy.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on meshBuddy" + ) + (await meshBuddy.mountRelay()).expect("Failed to mount relay on meshBuddy") + await meshBuddy.mountFilter() + await meshBuddy.mountLibp2pPing() + await meshBuddy.start() + + for shard in shards: + meshBuddy.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub meshBuddy" + ) + + let meshBuddyPeerInfo = meshBuddy.peerInfo.toRemotePeerInfo() + + await meshBuddy.connectToNodes(@[publisherPeerInfo]) + + let conf = createApiNodeConf(cli_args.WakuMode.Edge, numShards) + var subscriber: Waku + lockNewGlobalBrokerContext: + subscriber = (await createNode(conf)).expect("Failed to create edge subscriber") + (await startWaku(addr subscriber)).expect("Failed to start edge subscriber") + + # Connect edge subscriber to both filter servers so selectPeers finds both + await subscriber.node.connectToNodes(@[publisherPeerInfo, meshBuddyPeerInfo]) + + let testTopic = ContentTopic("/waku/2/failover-test/proto") + let shard = subscriber.node.getRelayShard(testTopic) + + (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") + + # Wait for dialing both filter servers (HealthyThreshold = 2) + for _ in 0 ..< 100: + if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: + break + await sleepAsync(100.milliseconds) + + check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2 + + # Verify message delivery with both servers alive + await waitForMesh(publisher, shard) + + var eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + let msg1 = WakuMessage( + payload: "Before failover".toBytes(), + contentTopic: testTopic, + version: 0, + timestamp: now(), + ) + discard (await publisher.publish(some(shard), msg1)).expect("Publish 1 failed") + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "Before failover".toBytes() + await eventManager.teardown() + + # Disconnect meshBuddy from edge (keeps relay mesh alive for publishing) + await subscriber.node.disconnectNode(meshBuddyPeerInfo) + + # Wait for the dead peer to be pruned + for _ in 0 ..< 50: + if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) < 2: + break + await sleepAsync(100.milliseconds) + + check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 1 + + # Verify messages still arrive through the surviving filter server (publisher) + eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + let msg2 = WakuMessage( + payload: "After failover".toBytes(), + contentTopic: testTopic, + version: 0, + timestamp: now(), + ) + discard (await publisher.publish(some(shard), msg2)).expect("Publish 2 failed") + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "After failover".toBytes() + await eventManager.teardown() + + (await subscriber.stop()).expect("Failed to stop subscriber") + await meshBuddy.stop() + await publisher.stop() + + asyncTest "Subscription API, edge node dials replacement after peer eviction": + # 3 service peers: publisher, meshBuddy, sparePeer. Edge subscribes and + # confirms 2 (HealthyThreshold). After one is disconnected, the sub loop + # should detect the loss and dial the spare to recover back to threshold. + let numShards: uint16 = 1 + let shards = @[PubsubTopic("/waku/2/rs/3/0")] + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + discard + + var publisher: WakuNode + lockNewGlobalBrokerContext: + publisher = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + publisher.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on publisher" + ) + (await publisher.mountRelay()).expect("Failed to mount relay on publisher") + await publisher.mountFilter() + await publisher.mountLibp2pPing() + await publisher.start() + + for shard in shards: + publisher.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub publisher" + ) + + let publisherPeerInfo = publisher.peerInfo.toRemotePeerInfo() + + var meshBuddy: WakuNode + lockNewGlobalBrokerContext: + meshBuddy = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + meshBuddy.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on meshBuddy" + ) + (await meshBuddy.mountRelay()).expect("Failed to mount relay on meshBuddy") + await meshBuddy.mountFilter() + await meshBuddy.mountLibp2pPing() + await meshBuddy.start() + + for shard in shards: + meshBuddy.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub meshBuddy" + ) + + let meshBuddyPeerInfo = meshBuddy.peerInfo.toRemotePeerInfo() + + var sparePeer: WakuNode + lockNewGlobalBrokerContext: + sparePeer = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + sparePeer.mountMetadata(3, toSeq(0'u16 ..< numShards)).expect( + "Failed to mount metadata on sparePeer" + ) + (await sparePeer.mountRelay()).expect("Failed to mount relay on sparePeer") + await sparePeer.mountFilter() + await sparePeer.mountLibp2pPing() + await sparePeer.start() + + for shard in shards: + sparePeer.subscribe((kind: PubsubSub, topic: shard), dummyHandler).expect( + "Failed to sub sparePeer" + ) + + let sparePeerInfo = sparePeer.peerInfo.toRemotePeerInfo() + + await meshBuddy.connectToNodes(@[publisherPeerInfo]) + await sparePeer.connectToNodes(@[publisherPeerInfo]) + + let conf = createApiNodeConf(cli_args.WakuMode.Edge, numShards) + var subscriber: Waku + lockNewGlobalBrokerContext: + subscriber = (await createNode(conf)).expect("Failed to create edge subscriber") + (await startWaku(addr subscriber)).expect("Failed to start edge subscriber") + + await subscriber.node.connectToNodes( + @[publisherPeerInfo, meshBuddyPeerInfo, sparePeerInfo] + ) + + let testTopic = ContentTopic("/waku/2/replacement-test/proto") + let shard = subscriber.node.getRelayShard(testTopic) + + (await subscriber.subscribe(testTopic)).expect("Failed to subscribe") + + # Wait for 2 confirmed peers (HealthyThreshold). The 3rd is available but not dialed. + for _ in 0 ..< 100: + if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: + break + await sleepAsync(100.milliseconds) + + require subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) == + 2 + + await subscriber.node.disconnectNode(meshBuddyPeerInfo) + + # Wait for the sub loop to detect the loss and dial a replacement + for _ in 0 ..< 100: + if subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2: + break + await sleepAsync(100.milliseconds) + + check subscriber.deliveryService.subscriptionManager.edgeFilterPeerCount(shard) >= 2 + + await waitForMesh(publisher, shard) + + var eventManager = newReceiveEventListenerManager(subscriber.brokerCtx, 1) + let msg = WakuMessage( + payload: "After replacement".toBytes(), + contentTopic: testTopic, + version: 0, + timestamp: now(), + ) + discard (await publisher.publish(some(shard), msg)).expect("Publish failed") + + require await eventManager.waitForEvents(TestTimeout) + check eventManager.receivedMessages[0].payload == "After replacement".toBytes() + await eventManager.teardown() + + (await subscriber.stop()).expect("Failed to stop subscriber") + await sparePeer.stop() + await meshBuddy.stop() + await publisher.stop() diff --git a/tests/api/test_entry_nodes.nim b/tests/api/test_entry_nodes.nim index 136a49b2b..a6faf58c8 100644 --- a/tests/api/test_entry_nodes.nim +++ b/tests/api/test_entry_nodes.nim @@ -2,7 +2,7 @@ import std/options, results, testutils/unittests -import waku/api/entry_nodes +import tools/confutils/entry_nodes # Since classifyEntryNode is internal, we test it indirectly through processEntryNodes behavior # The enum is exported so we can test against it @@ -126,12 +126,11 @@ suite "Entry Nodes Classification": suite "Entry Nodes Processing": test "Process mixed entry nodes": - let entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", - ] + let entryNodes = @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", + ] let result = processEntryNodes(entryNodes) check: @@ -147,11 +146,10 @@ suite "Entry Nodes Processing": staticNodes[0] == entryNodes[1] # multiaddr added to static test "Process only ENRTree nodes": - let entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "enrtree://ANOTHER_TREE@example.com", - ] + let entryNodes = @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "enrtree://ANOTHER_TREE@example.com", + ] let result = processEntryNodes(entryNodes) check: @@ -165,11 +163,10 @@ suite "Entry Nodes Processing": enrTreeUrls == entryNodes test "Process only multiaddresses": - let entryNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "/ip4/192.168.1.1/tcp/60001/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", - ] + let entryNodes = @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "/ip4/192.168.1.1/tcp/60001/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", + ] let result = processEntryNodes(entryNodes) check: @@ -183,11 +180,10 @@ suite "Entry Nodes Processing": staticNodes == entryNodes test "Process only ENR nodes": - let entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", - "enr:-QEkuECnZ3IbVAgkOzv-QLnKC4dRKAPRY80m1-R7G8jZ7yfT3ipEfBrhKN7ARcQgQ-vg-h40AQzyvAkPYlHPaFKk6u9MBgmlkgnY0iXNlY3AyNTZrMaEDk49D8JjMSns4p1XVNBvJquOUzT4PENSJknkROspfAFGg3RjcIJ2X4N1ZHCCd2g", - ] + let entryNodes = @[ + "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", + "enr:-QEkuECnZ3IbVAgkOzv-QLnKC4dRKAPRY80m1-R7G8jZ7yfT3ipEfBrhKN7ARcQgQ-vg-h40AQzyvAkPYlHPaFKk6u9MBgmlkgnY0iXNlY3AyNTZrMaEDk49D8JjMSns4p1XVNBvJquOUzT4PENSJknkROspfAFGg3RjcIJ2X4N1ZHCCd2g", + ] let result = processEntryNodes(entryNodes) check: @@ -224,13 +220,12 @@ suite "Entry Nodes Processing": "Entry node error: Unrecognized entry node format. Must start with 'enrtree:', 'enr:', or '/'" test "Process different multiaddr formats": - let entryNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "/ip6/::1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", - "/dns4/example.com/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYe", - "/dns/node.example.org/tcp/443/wss/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYf", - ] + let entryNodes = @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "/ip6/::1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", + "/dns4/example.com/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYe", + "/dns/node.example.org/tcp/443/wss/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYf", + ] let result = processEntryNodes(entryNodes) check: @@ -244,13 +239,12 @@ suite "Entry Nodes Processing": staticNodes == entryNodes test "Process with duplicate entries": - let entryNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - ] + let entryNodes = @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + ] let result = processEntryNodes(entryNodes) check: diff --git a/tests/api/test_node_conf.nim b/tests/api/test_node_conf.nim index 232ffc7d2..8798c5cc5 100644 --- a/tests/api/test_node_conf.nim +++ b/tests/api/test_node_conf.nim @@ -1,17 +1,356 @@ {.used.} -import std/options, results, stint, testutils/unittests -import waku/api/api_conf, waku/factory/waku_conf, waku/factory/networks_config +import std/[options, json, strutils], results, stint, testutils/unittests +import json_serialization, confutils, confutils/std/net +import + tools/confutils/cli_args, + waku/api/api_conf, + waku/factory/waku_conf, + waku/factory/networks_config, + waku/factory/conf_builder/conf_builder, + waku/common/logging -suite "LibWaku Conf - toWakuConf": - test "Minimal configuration": +# Helper: parse JSON into WakuNodeConf using fieldPairs (same as liblogosdelivery) +proc parseWakuNodeConfFromJson(jsonStr: string): Result[WakuNodeConf, string] = + var conf = defaultWakuNodeConf().valueOr: + return err(error) + var jsonNode: JsonNode + try: + jsonNode = parseJson(jsonStr) + except Exception: + return err("JSON parse error: " & getCurrentExceptionMsg()) + for confField, confValue in fieldPairs(conf): + if jsonNode.contains(confField): + let formattedString = ($jsonNode[confField]).strip(chars = {'\"'}) + try: + confValue = parseCmdArg(typeof(confValue), formattedString) + except Exception: + return err( + "Field '" & confField & "' parse error: " & getCurrentExceptionMsg() & + ". Value: " & formattedString + ) + return ok(conf) + +suite "WakuNodeConf - mode-driven toWakuConf": + test "Core mode enables service protocols": ## Given - let nodeConfig = NodeConfig.init(ethRpcEndpoints = @["http://someaddress"]) + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = Core + conf.clusterId = 1 ## When - let wakuConfRes = toWakuConf(nodeConfig) + let wakuConfRes = conf.toWakuConf() ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == true + wakuConf.lightPush == true + wakuConf.peerExchangeService == true + wakuConf.rendezvous == true + wakuConf.clusterId == 1 + + test "Edge mode disables service protocols": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = Edge + conf.clusterId = 1 + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == false + wakuConf.lightPush == false + wakuConf.filterServiceConf.isSome() == false + wakuConf.storeServiceConf.isSome() == false + wakuConf.peerExchangeService == true + + test "noMode uses explicit CLI flags as-is": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = cli_args.WakuMode.noMode + conf.relay = true + conf.lightpush = false + conf.clusterId = 5 + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == true + wakuConf.lightPush == false + wakuConf.clusterId == 5 + + test "Core mode overrides individual protocol flags": + ## Given - user sets relay=false but mode=Core should override + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.mode = Core + conf.relay = false # will be overridden by Core mode + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == true # mode overrides + +suite "WakuNodeConf - JSON parsing with fieldPairs": + test "Empty JSON produces valid default conf": + ## Given / When + let confRes = parseWakuNodeConfFromJson("{}") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.mode == cli_args.WakuMode.noMode + conf.clusterId == 0 + conf.logLevel == logging.LogLevel.INFO + + test "JSON with mode and clusterId": + ## Given / When + let confRes = parseWakuNodeConfFromJson("""{"mode": "Core", "clusterId": 42}""") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.mode == Core + conf.clusterId == 42 + + test "JSON with Edge mode": + ## Given / When + let confRes = parseWakuNodeConfFromJson("""{"mode": "Edge"}""") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.mode == Edge + + test "JSON with logLevel": + ## Given / When + let confRes = parseWakuNodeConfFromJson("""{"logLevel": "DEBUG"}""") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.logLevel == logging.LogLevel.DEBUG + + test "JSON with sharding config": + ## Given / When + let confRes = + parseWakuNodeConfFromJson("""{"clusterId": 99, "numShardsInNetwork": 16}""") + + ## Then + require confRes.isOk() + let conf = confRes.get() + check: + conf.clusterId == 99 + conf.numShardsInNetwork == 16 + + test "JSON with unknown fields is silently ignored": + ## Given / When + let confRes = + parseWakuNodeConfFromJson("""{"unknownField": true, "clusterId": 5}""") + + ## Then - unknown fields are just ignored (not in fieldPairs) + require confRes.isOk() + let conf = confRes.get() + check: + conf.clusterId == 5 + + test "Invalid JSON syntax returns error": + ## Given / When + let confRes = parseWakuNodeConfFromJson("{ not valid json }") + + ## Then + check confRes.isErr() + +suite "WakuNodeConf - preset integration": + test "TWN preset applies TheWakuNetworkConf": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.preset = "twn" + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.clusterId == 1 + + test "LogosDev preset applies LogosDevConf": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.preset = "logosdev" + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.clusterId == 2 + + test "LogosTest preset applies LogosTestConf": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.preset = "logostest" + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.clusterId == 2 + + test "Invalid preset returns error": + ## Given + var conf = defaultWakuNodeConf().valueOr: + raiseAssert error + conf.preset = "nonexistent" + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + check wakuConfRes.isErr() + +suite "WakuNodeConf JSON -> WakuConf integration": + test "Core mode JSON config produces valid WakuConf": + ## Given + let confRes = parseWakuNodeConfFromJson( + """{"mode": "Core", "clusterId": 55, "numShardsInNetwork": 6}""" + ) + require confRes.isOk() + let conf = confRes.get() + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == true + wakuConf.lightPush == true + wakuConf.peerExchangeService == true + wakuConf.clusterId == 55 + wakuConf.shardingConf.numShardsInCluster == 6 + + test "Edge mode JSON config produces valid WakuConf": + ## Given + let confRes = parseWakuNodeConfFromJson("""{"mode": "Edge", "clusterId": 1}""") + require confRes.isOk() + let conf = confRes.get() + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == false + wakuConf.lightPush == false + wakuConf.peerExchangeService == true + + test "JSON with preset produces valid WakuConf": + ## Given + let confRes = + parseWakuNodeConfFromJson("""{"mode": "Core", "preset": "logosdev"}""") + require confRes.isOk() + let conf = confRes.get() + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.clusterId == 2 + wakuConf.relay == true + + test "JSON with static nodes": + ## Given + let confRes = parseWakuNodeConfFromJson( + """{"mode": "Core", "clusterId": 42, "staticnodes": ["/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc"]}""" + ) + require confRes.isOk() + let conf = confRes.get() + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.staticNodes.len == 1 + + test "JSON with max message size": + ## Given + let confRes = + parseWakuNodeConfFromJson("""{"clusterId": 42, "maxMessageSize": "100KiB"}""") + require confRes.isOk() + let conf = confRes.get() + + ## When + let wakuConfRes = conf.toWakuConf() + + ## Then + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.maxMessageSizeBytes == 100'u64 * 1024'u64 + +# ---- Deprecated NodeConfig tests (kept for backward compatibility) ---- + +{.push warning[Deprecated]: off.} + +import waku/api/api_conf + +suite "NodeConfig (deprecated) - toWakuConf": + test "Minimal configuration": + let nodeConfig = NodeConfig.init(ethRpcEndpoints = @["http://someaddress"]) + let wakuConfRes = api_conf.toWakuConf(nodeConfig) let wakuConf = wakuConfRes.valueOr: raiseAssert error wakuConf.validate().isOkOr: @@ -21,16 +360,24 @@ suite "LibWaku Conf - toWakuConf": wakuConf.shardingConf.numShardsInCluster == 8 wakuConf.staticNodes.len == 0 - test "Core mode configuration": - ## Given + test "Edge mode configuration": let protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) + let nodeConfig = + NodeConfig.init(mode = api_conf.WakuMode.Edge, protocolsConfig = protocolsConfig) + let wakuConfRes = api_conf.toWakuConf(nodeConfig) + require wakuConfRes.isOk() + let wakuConf = wakuConfRes.get() + require wakuConf.validate().isOk() + check: + wakuConf.relay == false + wakuConf.lightPush == false + wakuConf.peerExchangeService == true - let nodeConfig = NodeConfig.init(mode = Core, protocolsConfig = protocolsConfig) - - ## When - let wakuConfRes = toWakuConf(nodeConfig) - - ## Then + test "Core mode configuration": + let protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) + let nodeConfig = + NodeConfig.init(mode = api_conf.WakuMode.Core, protocolsConfig = protocolsConfig) + let wakuConfRes = api_conf.toWakuConf(nodeConfig) require wakuConfRes.isOk() let wakuConf = wakuConfRes.get() require wakuConf.validate().isOk() @@ -38,242 +385,64 @@ suite "LibWaku Conf - toWakuConf": wakuConf.relay == true wakuConf.lightPush == true wakuConf.peerExchangeService == true - wakuConf.clusterId == 1 - test "Auto-sharding configuration": +{.pop.} + +suite "WakuConfBuilder - store retention policies": + test "Multiple retention policies": ## Given - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - staticStoreNodes = @[], - clusterId = 42, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 16), - ), + var b = WakuConfBuilder.init() + b.withP2pTcpPort(0'u16) + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies( + "time:86400 ; capacity:10000; size : 50GB" ) ## When - let wakuConfRes = toWakuConf(nodeConfig) - - ## Then - require wakuConfRes.isOk() - let wakuConf = wakuConfRes.get() - require wakuConf.validate().isOk() - check: - wakuConf.clusterId == 42 - wakuConf.shardingConf.numShardsInCluster == 16 - - test "Bootstrap nodes configuration": - ## Given - let entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g", - "enr:-QEkuECnZ3IbVAgkOzv-QLnKC4dRKAPRY80m1-R7G8jZ7yfT3ipEfBrhKN7ARcQgQ-vg-h40AQzyvAkPYlHPaFKk6u9MBgmlkgnY0iXNlY3AyNTZrMaEDk49D8JjMSns4p1XVNBvJquOUzT4PENSJknkROspfAFGg3RjcIJ2X4N1ZHCCd2g", - ] - let libConf = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = entryNodes, staticStoreNodes = @[], clusterId = 1 - ), - ) - - ## When - let wakuConfRes = toWakuConf(libConf) - - ## Then - require wakuConfRes.isOk() - let wakuConf = wakuConfRes.get() - require wakuConf.validate().isOk() - require wakuConf.discv5Conf.isSome() - check: - wakuConf.discv5Conf.get().bootstrapNodes == entryNodes - - test "Static store nodes configuration": - ## Given - let staticStoreNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - "/ip4/192.168.1.1/tcp/60001/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYd", - ] - let nodeConf = NodeConfig.init( - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], staticStoreNodes = staticStoreNodes, clusterId = 1 - ) - ) - - ## When - let wakuConfRes = toWakuConf(nodeConf) - - ## Then - require wakuConfRes.isOk() - let wakuConf = wakuConfRes.get() - require wakuConf.validate().isOk() - check: - wakuConf.staticNodes == staticStoreNodes - - test "Message validation with max message size": - ## Given - let nodeConfig = NodeConfig.init( - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - staticStoreNodes = @[], - clusterId = 1, - messageValidation = - MessageValidation(maxMessageSize: "100KiB", rlnConfig: none(RlnConfig)), - ) - ) - - ## When - let wakuConfRes = toWakuConf(nodeConfig) - - ## Then - require wakuConfRes.isOk() - let wakuConf = wakuConfRes.get() - require wakuConf.validate().isOk() - check: - wakuConf.maxMessageSizeBytes == 100'u64 * 1024'u64 - - test "Message validation with RLN config": - ## Given - let nodeConfig = NodeConfig.init( - protocolsConfig = ProtocolsConfig.init( - entryNodes = @[], - clusterId = 1, - messageValidation = MessageValidation( - maxMessageSize: "150 KiB", - rlnConfig: some( - RlnConfig( - contractAddress: "0x1234567890123456789012345678901234567890", - chainId: 1'u, - epochSizeSec: 600'u64, - ) - ), - ), - ), - ethRpcEndpoints = @["http://127.0.0.1:1111"], - ) - - ## When - let wakuConf = toWakuConf(nodeConfig).valueOr: + let wakuConf = b.build().valueOr: raiseAssert error - wakuConf.validate().isOkOr: - raiseAssert error + ## Then + require wakuConf.storeServiceConf.isSome() + let storeConf = wakuConf.storeServiceConf.get() + check storeConf.retentionPolicies == @["time:86400", "capacity:10000", "size:50GB"] - check: - wakuConf.maxMessageSizeBytes == 150'u64 * 1024'u64 - - require wakuConf.rlnRelayConf.isSome() - let rlnConf = wakuConf.rlnRelayConf.get() - check: - rlnConf.dynamic == true - rlnConf.ethContractAddress == "0x1234567890123456789012345678901234567890" - rlnConf.chainId == 1'u256 - rlnConf.epochSizeSec == 600'u64 - - test "Full Core mode configuration with all fields": + test "Duplicated retention policies returns error": ## Given - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g" - ], - staticStoreNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - ], - clusterId = 99, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 12), - messageValidation = MessageValidation( - maxMessageSize: "512KiB", - rlnConfig: some( - RlnConfig( - contractAddress: "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - chainId: 5'u, # Goerli - epochSizeSec: 300'u64, - ) - ), - ), - ), - ethRpcEndpoints = @["https://127.0.0.1:8333"], - ) + var b = WakuConfBuilder.init() + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies("time:86400;time:800;capacity:10000") ## When - let wakuConfRes = toWakuConf(nodeConfig) + let wakuConfRes = b.build() + check wakuConfRes.isErr() + check wakuConfRes.error.contains("duplicated retention policy type") - ## Then - let wakuConf = wakuConfRes.valueOr: - raiseAssert error - wakuConf.validate().isOkOr: - raiseAssert error - - # Check basic settings - check: - wakuConf.relay == true - wakuConf.lightPush == true - wakuConf.peerExchangeService == true - wakuConf.rendezvous == true - wakuConf.clusterId == 99 - - # Check sharding - check: - wakuConf.shardingConf.numShardsInCluster == 12 - - # Check bootstrap nodes - require wakuConf.discv5Conf.isSome() - check: - wakuConf.discv5Conf.get().bootstrapNodes.len == 1 - - # Check static nodes - check: - wakuConf.staticNodes.len == 1 - wakuConf.staticNodes[0] == - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - - # Check message validation - check: - wakuConf.maxMessageSizeBytes == 512'u64 * 1024'u64 - - # Check RLN config - require wakuConf.rlnRelayConf.isSome() - let rlnConf = wakuConf.rlnRelayConf.get() - check: - rlnConf.dynamic == true - rlnConf.ethContractAddress == "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" - rlnConf.chainId == 5'u256 - rlnConf.epochSizeSec == 300'u64 - - test "NodeConfig with mixed entry nodes (integration test)": + test "Incorrect retention policy type returns error": ## Given - let entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - ] - - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = entryNodes, staticStoreNodes = @[], clusterId = 1 - ), - ) + var b = WakuConfBuilder.init() + b.storeServiceConf.withEnabled(true) + b.storeServiceConf.withDbUrl("sqlite://test.db") + b.storeServiceConf.withRetentionPolicies("capaity:10000") ## When - let wakuConfRes = toWakuConf(nodeConfig) + let wakuConfRes = b.build() ## Then - require wakuConfRes.isOk() - let wakuConf = wakuConfRes.get() - require wakuConf.validate().isOk() + check wakuConfRes.isErr() + check wakuConfRes.error.contains("unknown retention policy type") - # Check that ENRTree went to DNS discovery - require wakuConf.dnsDiscoveryConf.isSome() - check: - wakuConf.dnsDiscoveryConf.get().enrTreeUrl == entryNodes[0] + test "Store disabled - no retention policy applied": + ## Given + var b = WakuConfBuilder.init() + b.withP2pTcpPort(0'u16) + # storeServiceConf not enabled - # Check that multiaddr went to static nodes - check: - wakuConf.staticNodes.len == 1 - wakuConf.staticNodes[0] == entryNodes[1] + ## When + let wakuConf = b.build().valueOr: + raiseAssert error + + ## Then + check wakuConf.storeServiceConf.isNone() diff --git a/tests/common/test_all.nim b/tests/common/test_all.nim index 5b4515093..1070c34e4 100644 --- a/tests/common/test_all.nim +++ b/tests/common/test_all.nim @@ -6,7 +6,6 @@ import ./test_protobuf_validation, ./test_sqlite_migrations, ./test_parse_size, - ./test_tokenbucket, ./test_requestratelimiter, ./test_ratelimit_setting, ./test_timed_map diff --git a/tests/common/test_base64_codec.nim b/tests/common/test_base64_codec.nim index 1c2d04c45..b9ac9e464 100644 --- a/tests/common/test_base64_codec.nim +++ b/tests/common/test_base64_codec.nim @@ -4,23 +4,22 @@ import std/strutils, results, stew/byteutils, testutils/unittests import waku/common/base64 suite "Waku Common - stew base64 wrapper": - const TestData = - @[ - # Test vectors from RFC 4648 - # See: https://datatracker.ietf.org/doc/html/rfc4648#section-10 - ("", Base64String("")), - ("f", Base64String("Zg==")), - ("fo", Base64String("Zm8=")), - ("foo", Base64String("Zm9v")), - ("foob", Base64String("Zm9vYg==")), - ("fooba", Base64String("Zm9vYmE=")), - ("foobar", Base64String("Zm9vYmFy")), + const TestData = @[ + # Test vectors from RFC 4648 + # See: https://datatracker.ietf.org/doc/html/rfc4648#section-10 + ("", Base64String("")), + ("f", Base64String("Zg==")), + ("fo", Base64String("Zm8=")), + ("foo", Base64String("Zm9v")), + ("foob", Base64String("Zm9vYg==")), + ("fooba", Base64String("Zm9vYmE=")), + ("foobar", Base64String("Zm9vYmFy")), - # Custom test vectors - ("\x01", Base64String("AQ==")), - ("\x13", Base64String("Ew==")), - ("\x01\x02\x03\x04", Base64String("AQIDBA==")), - ] + # Custom test vectors + ("\x01", Base64String("AQ==")), + ("\x13", Base64String("Ew==")), + ("\x01\x02\x03\x04", Base64String("AQIDBA==")), + ] for (plaintext, encoded) in TestData: test "encode into base64 (" & escape(plaintext) & " -> \"" & string(encoded) & "\")": diff --git a/tests/common/test_ratelimit_setting.nim b/tests/common/test_ratelimit_setting.nim index 97d69e06a..2bc95fbfb 100644 --- a/tests/common/test_ratelimit_setting.nim +++ b/tests/common/test_ratelimit_setting.nim @@ -25,7 +25,6 @@ suite "RateLimitSetting": test "Parse rate limit setting - ok": let test1 = "10/2m" let test2 = " store : 10 /1h" - let test2a = "storev2 : 10 /1h" let test2b = "storeV3: 12 /1s" let test3 = "LIGHTPUSH: 10/ 1m" let test4 = "px:10/2 s " @@ -34,7 +33,6 @@ suite "RateLimitSetting": let expU = UnlimitedRateLimit let exp1: RateLimitSetting = (10, 2.minutes) let exp2: RateLimitSetting = (10, 1.hours) - let exp2a: RateLimitSetting = (10, 1.hours) let exp2b: RateLimitSetting = (12, 1.seconds) let exp3: RateLimitSetting = (10, 1.minutes) let exp4: RateLimitSetting = (10, 2.seconds) @@ -42,7 +40,6 @@ suite "RateLimitSetting": let res1 = ProtocolRateLimitSettings.parse(@[test1]) let res2 = ProtocolRateLimitSettings.parse(@[test2]) - let res2a = ProtocolRateLimitSettings.parse(@[test2a]) let res2b = ProtocolRateLimitSettings.parse(@[test2b]) let res3 = ProtocolRateLimitSettings.parse(@[test3]) let res4 = ProtocolRateLimitSettings.parse(@[test4]) @@ -53,15 +50,7 @@ suite "RateLimitSetting": res1.get() == {GLOBAL: exp1, FILTER: FilterDefaultPerPeerRateLimit}.toTable() res2.isOk() res2.get() == - { - GLOBAL: expU, - FILTER: FilterDefaultPerPeerRateLimit, - STOREV2: exp2, - STOREV3: exp2, - }.toTable() - res2a.isOk() - res2a.get() == - {GLOBAL: expU, FILTER: FilterDefaultPerPeerRateLimit, STOREV2: exp2a}.toTable() + {GLOBAL: expU, FILTER: FilterDefaultPerPeerRateLimit, STOREV3: exp2}.toTable() res2b.isOk() res2b.get() == {GLOBAL: expU, FILTER: FilterDefaultPerPeerRateLimit, STOREV3: exp2b}.toTable() @@ -77,7 +66,6 @@ suite "RateLimitSetting": test "Parse rate limit setting - err": let test1 = "10/2d" let test2 = " stre : 10 /1h" - let test2a = "storev2 10 /1h" let test2b = "storev3: 12 1s" let test3 = "somethingelse: 10/ 1m" let test4 = ":px:10/2 s " @@ -85,7 +73,6 @@ suite "RateLimitSetting": let res1 = ProtocolRateLimitSettings.parse(@[test1]) let res2 = ProtocolRateLimitSettings.parse(@[test2]) - let res2a = ProtocolRateLimitSettings.parse(@[test2a]) let res2b = ProtocolRateLimitSettings.parse(@[test2b]) let res3 = ProtocolRateLimitSettings.parse(@[test3]) let res4 = ProtocolRateLimitSettings.parse(@[test4]) @@ -94,7 +81,6 @@ suite "RateLimitSetting": check: res1.isErr() res2.isErr() - res2a.isErr() res2b.isErr() res3.isErr() res4.isErr() @@ -103,13 +89,12 @@ suite "RateLimitSetting": test "Parse rate limit setting - complex": let expU = UnlimitedRateLimit - let test1 = @["lightpush:2/2ms", "10/2m", " store: 3/3s", " storev2:12/12s"] + let test1 = @["lightpush:2/2ms", "10/2m", " store: 3/3s"] let exp1 = { GLOBAL: (10, 2.minutes), FILTER: FilterDefaultPerPeerRateLimit, LIGHTPUSH: (2, 2.milliseconds), STOREV3: (3, 3.seconds), - STOREV2: (12, 12.seconds), }.toTable() let res1 = ProtocolRateLimitSettings.parse(test1) @@ -118,7 +103,6 @@ suite "RateLimitSetting": res1.isOk() res1.get() == exp1 res1.get().getSetting(PEEREXCHG) == (10, 2.minutes) - res1.get().getSetting(STOREV2) == (12, 12.seconds) res1.get().getSetting(STOREV3) == (3, 3.seconds) res1.get().getSetting(LIGHTPUSH) == (2, 2.milliseconds) @@ -127,7 +111,6 @@ suite "RateLimitSetting": GLOBAL: expU, LIGHTPUSH: (2, 2.milliseconds), STOREV3: (3, 3.seconds), - STOREV2: (3, 3.seconds), FILTER: (4, 42.milliseconds), PEEREXCHG: (10, 10.hours), }.toTable() @@ -138,13 +121,9 @@ suite "RateLimitSetting": res2.isOk() res2.get() == exp2 - let test3 = - @["storev2:1/1s", "store:3/3s", "storev3:4/42ms", "storev3:5/5s", "storev3:6/6s"] + let test3 = @["store:3/3s", "storev3:4/42ms", "storev3:5/5s", "storev3:6/6s"] let exp3 = { - GLOBAL: expU, - FILTER: FilterDefaultPerPeerRateLimit, - STOREV3: (6, 6.seconds), - STOREV2: (1, 1.seconds), + GLOBAL: expU, FILTER: FilterDefaultPerPeerRateLimit, STOREV3: (6, 6.seconds) }.toTable() let res3 = ProtocolRateLimitSettings.parse(test3) diff --git a/tests/common/test_sqlite_migrations.nim b/tests/common/test_sqlite_migrations.nim index 9e67fb9c8..2a9cae609 100644 --- a/tests/common/test_sqlite_migrations.nim +++ b/tests/common/test_sqlite_migrations.nim @@ -29,17 +29,16 @@ suite "SQLite - migrations": test "filter and order migration script file paths": ## Given - let paths = - @[ - sourceDir / "00001_valid.up.sql", - sourceDir / "00002_alsoValidWithUpperCaseExtension.UP.SQL", - sourceDir / "00007_unorderedValid.up.sql", - sourceDir / "00003_validRepeated.up.sql", - sourceDir / "00003_validRepeated.up.sql", - sourceDir / "00666_noMigrationScript.bmp", - sourceDir / "00X00_invalidVersion.down.sql", - sourceDir / "00008_notWithinVersionRange.up.sql", - ] + let paths = @[ + sourceDir / "00001_valid.up.sql", + sourceDir / "00002_alsoValidWithUpperCaseExtension.UP.SQL", + sourceDir / "00007_unorderedValid.up.sql", + sourceDir / "00003_validRepeated.up.sql", + sourceDir / "00003_validRepeated.up.sql", + sourceDir / "00666_noMigrationScript.bmp", + sourceDir / "00X00_invalidVersion.down.sql", + sourceDir / "00008_notWithinVersionRange.up.sql", + ] let lowerVersion = 0 @@ -64,16 +63,14 @@ suite "SQLite - migrations": test "break migration scripts into queries": ## Given - let statement1 = - """CREATE TABLE contacts1 ( + let statement1 = """CREATE TABLE contacts1 ( contact_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, phone TEXT NOT NULL UNIQUE );""" - let statement2 = - """CREATE TABLE contacts2 ( + let statement2 = """CREATE TABLE contacts2 ( contact_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, @@ -91,16 +88,14 @@ suite "SQLite - migrations": test "break statements script into queries - empty statements": ## Given - let statement1 = - """CREATE TABLE contacts1 ( + let statement1 = """CREATE TABLE contacts1 ( contact_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, email TEXT NOT NULL UNIQUE, phone TEXT NOT NULL UNIQUE );""" - let statement2 = - """CREATE TABLE contacts2 ( + let statement2 = """CREATE TABLE contacts2 ( contact_id INTEGER PRIMARY KEY, first_name TEXT NOT NULL, last_name TEXT NOT NULL, diff --git a/tests/common/test_tokenbucket.nim b/tests/common/test_tokenbucket.nim deleted file mode 100644 index 5bc1a0583..000000000 --- a/tests/common/test_tokenbucket.nim +++ /dev/null @@ -1,69 +0,0 @@ -# Chronos Test Suite -# (c) Copyright 2022-Present -# Status Research & Development GmbH -# -# Licensed under either of -# Apache License, version 2.0, (LICENSE-APACHEv2) -# MIT license (LICENSE-MIT) - -{.used.} - -import testutils/unittests -import chronos -import ../../waku/common/rate_limit/token_bucket - -suite "Token Bucket": - test "TokenBucket Sync test - strict": - var bucket = TokenBucket.newStrict(1000, 1.milliseconds) - let - start = Moment.now() - fullTime = start + 1.milliseconds - check: - bucket.tryConsume(800, start) == true - bucket.tryConsume(200, start) == true - # Out of budget - bucket.tryConsume(100, start) == false - bucket.tryConsume(800, fullTime) == true - bucket.tryConsume(200, fullTime) == true - # Out of budget - bucket.tryConsume(100, fullTime) == false - - test "TokenBucket Sync test - compensating": - var bucket = TokenBucket.new(1000, 1.milliseconds) - let - start = Moment.now() - fullTime = start + 1.milliseconds - check: - bucket.tryConsume(800, start) == true - bucket.tryConsume(200, start) == true - # Out of budget - bucket.tryConsume(100, start) == false - bucket.tryConsume(800, fullTime) == true - bucket.tryConsume(200, fullTime) == true - # Due not using the bucket for a full period the compensation will satisfy this request - bucket.tryConsume(100, fullTime) == true - - test "TokenBucket Max compensation": - var bucket = TokenBucket.new(1000, 1.minutes) - var reqTime = Moment.now() - - check bucket.tryConsume(1000, reqTime) - check bucket.tryConsume(1, reqTime) == false - reqTime += 1.minutes - check bucket.tryConsume(500, reqTime) == true - reqTime += 1.minutes - check bucket.tryConsume(1000, reqTime) == true - reqTime += 10.seconds - # max compensation is 25% so try to consume 250 more - check bucket.tryConsume(250, reqTime) == true - reqTime += 49.seconds - # out of budget within the same period - check bucket.tryConsume(1, reqTime) == false - - test "TokenBucket Short replenish": - var bucket = TokenBucket.new(15000, 1.milliseconds) - let start = Moment.now() - check bucket.tryConsume(15000, start) - check bucket.tryConsume(1, start) == false - - check bucket.tryConsume(15000, start + 1.milliseconds) == true diff --git a/tests/factory/test_node_factory.nim b/tests/factory/test_node_factory.nim index f30e079b5..1fe242532 100644 --- a/tests/factory/test_node_factory.nim +++ b/tests/factory/test_node_factory.nim @@ -1,13 +1,23 @@ {.used.} -import testutils/unittests, chronos, libp2p/protocols/connectivity/relay/relay +import + std/[net, options, sequtils, strutils], + testutils/unittests, + chronos, + chronos/transports/[stream, datagram, common], + metrics/chronos_httpserver, + libp2p/[crypto/crypto, multiaddress, protocols/connectivity/relay/relay], + eth/p2p/discoveryv5/enr import - ../testlib/wakunode, - waku/waku_node, - waku/factory/node_factory, - waku/factory/conf_builder/conf_builder, - waku/factory/conf_builder/web_socket_conf_builder + tests/testlib/[wakunode, wakucore], + waku/[waku_node, waku_enr, net/auto_port, discovery/waku_discv5, node/waku_metrics], + waku/factory/[ + node_factory, + internal_config, + conf_builder/conf_builder, + conf_builder/web_socket_conf_builder, + ] suite "Node Factory": asynctest "Set up a node based on default configurations": @@ -38,6 +48,45 @@ suite "Node Factory": not node.wakuStore.isNil() not node.wakuArchive.isNil() + test "ENR configuration trims multiaddrs until record fits": + var conf = defaultTestWakuConf() + let bindIp = conf.endpointConf.p2pListenAddress + let bindPort = Port(30303) + + let oversizedMultiaddrs = (0 .. 11).mapIt( + MultiAddress + .init( + "/dns4/very-long-logical-hostname-" & $it & + ".example.logos.dev.status.im/tcp/30303/wss" + ) + .get() + ) + + let netConfig = NetConfig.init( + clusterId = conf.clusterId, + bindIp = bindIp, + bindPort = bindPort, + extMultiAddrs = oversizedMultiaddrs, + extMultiAddrsOnly = true, + wakuFlags = some(conf.wakuFlags), + ).valueOr: + raiseAssert error + + let record = enrConfiguration(conf, netConfig).valueOr: + raiseAssert error + + let typedRecord = record.toTyped() + require typedRecord.isOk() + + let multiaddrsOpt = typedRecord.value.multiaddrs + require multiaddrsOpt.isSome() + + let retainedMultiaddrs = multiaddrsOpt.get() + check: + retainedMultiaddrs.len < oversizedMultiaddrs.len + retainedMultiaddrs.len > 0 + retainedMultiaddrs == oversizedMultiaddrs[0 ..< retainedMultiaddrs.len] + asynctest "Set up a node with Filter enabled": var confBuilder = defaultTestWakuConfBuilder() confBuilder.filterServiceConf.withEnabled(true) @@ -68,5 +117,90 @@ asynctest "Start a node based on default test configuration": check: node.started == true + # Default conf has p2pTcpPort=0, so the OS must have assigned a real port. + var hasNonZeroTcp = false + for a in node.switch.peerInfo.listenAddrs: + let s = $a + if ("/tcp/" in s) and not ("/tcp/0" in s): + hasNonZeroTcp = true + check hasNonZeroTcp + ## Cleanup await node.stop() + +suite "Auto-port retry": + asynctest "metrics binds on free TCP port, fails on taken": + let takenPort = Port(55100) + let freePort = Port(55101) + let taken = createStreamServer(initTAddress("127.0.0.1", takenPort)) + defer: + taken.stop() + await taken.closeWait() + + proc buildMetricsConf(port: Port): MetricsServerConf = + var b = MetricsServerConfBuilder.init() + b.withEnabled(true) + b.withHttpPort(port) + b.build().value.get() + + let failRes = await startMetricsServerAndLogging(buildMetricsConf(takenPort), 0'u16) + check failRes.isErr() + + let okRes = await startMetricsServerAndLogging(buildMetricsConf(freePort), 0'u16) + check okRes.isOk() + if okRes.isOk(): + await okRes.get().server.close() + + asynctest "discv5 binds on free UDP port, fails on taken": + let takenPort = Port(55200) + let freePort = Port(55201) + + proc dummyCb( + transp: DatagramTransport, raddr: TransportAddress + ): Future[void] {.async: (raises: []).} = + discard + + let takenUdp = + newDatagramTransport(dummyCb, local = initTAddress("0.0.0.0", takenPort)) + defer: + await takenUdp.closeWait() + + let nodeKey = generateSecp256k1Key() + let node = newTestWakuNode(nodeKey, parseIpAddress("0.0.0.0"), Port(0)) + await node.start() + defer: + await node.stop() + + proc buildDiscv5Conf(port: Port): Discv5Conf = + var b = Discv5ConfBuilder.init() + b.withEnabled(true) + b.withUdpPort(port) + b.build().value.get() + + let failRes = await setupAndStartDiscv5( + node.enr, + node.peerManager, + node.topicSubscriptionQueue, + buildDiscv5Conf(takenPort), + @[], + node.rng, + nodeKey, + parseIpAddress("0.0.0.0"), + 0'u16, + ) + check failRes.isErr() + + let okRes = await setupAndStartDiscv5( + node.enr, + node.peerManager, + node.topicSubscriptionQueue, + buildDiscv5Conf(freePort), + @[], + node.rng, + nodeKey, + parseIpAddress("0.0.0.0"), + 0'u16, + ) + check okRes.isOk() + if okRes.isOk(): + await okRes.get().stop() diff --git a/tests/factory/test_waku_conf.nim b/tests/factory/test_waku_conf.nim index 3d3fec20e..885e22867 100644 --- a/tests/factory/test_waku_conf.nim +++ b/tests/factory/test_waku_conf.nim @@ -4,7 +4,7 @@ import libp2p/crypto/[crypto, secp], libp2p/multiaddress, nimcrypto/utils, - std/[options, sequtils], + std/[net, options, random, sequtils], results, testutils/unittests import @@ -22,11 +22,13 @@ suite "Waku Conf - build with cluster conf": builder.withRelayServiceRatio("50:50") # Mount all shards in network let expectedShards = toSeq[0.uint16 .. 7.uint16] + let userMessageLimit = rand(1 .. 1000).uint64 ## Given builder.rlnRelayConf.withEthClientUrls(@["https://my_eth_rpc_url/"]) builder.withNetworkConf(networkConf) builder.withRelay(true) + builder.rlnRelayConf.withUserMessageLimit(userMessageLimit) ## When let resConf = builder.build() @@ -54,7 +56,7 @@ suite "Waku Conf - build with cluster conf": check rlnRelayConf.dynamic == networkConf.rlnRelayDynamic check rlnRelayConf.chainId == networkConf.rlnRelayChainId check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec - check rlnRelayConf.userMessageLimit == networkConf.rlnRelayUserMessageLimit + check rlnRelayConf.userMessageLimit == userMessageLimit.uint test "Cluster Conf is passed, but relay is disabled": ## Setup @@ -174,11 +176,13 @@ suite "Waku Conf - build with cluster conf": # Mount all shards in network let expectedShards = toSeq[0.uint16 .. 7.uint16] let contractAddress = "0x0123456789ABCDEF" + let userMessageLimit = rand(1 .. 1000).uint64 ## Given builder.rlnRelayConf.withEthContractAddress(contractAddress) builder.withNetworkConf(networkConf) builder.withRelay(true) + builder.rlnRelayConf.withUserMessageLimit(userMessageLimit) ## When let resConf = builder.build() @@ -207,7 +211,55 @@ suite "Waku Conf - build with cluster conf": check rlnRelayConf.dynamic == networkConf.rlnRelayDynamic check rlnRelayConf.chainId == networkConf.rlnRelayChainId check rlnRelayConf.epochSizeSec == networkConf.rlnEpochSizeSec - check rlnRelayConf.userMessageLimit == networkConf.rlnRelayUserMessageLimit + check rlnRelayConf.userMessageLimit == userMessageLimit.uint + + test "num-shards-in-network > 0 overrides preset": + ## Setup + let networkConf = NetworkConf.LogosDevConf() + var builder = WakuConfBuilder.init() + + # Sanity check + check networkConf.shardingConf.kind == AutoSharding + check networkConf.shardingConf.numShardsInCluster > 1 + + ## Given: preset says >1 shards but user explicitly sets 1 + builder.withNetworkConf(networkConf) + builder.withNumShardsInCluster(1) + builder.withShardingConf(AutoSharding) + + ## When + let conf = builder.build().expect("build should succeed") + + ## Then: user value wins, not preset + conf.validate().expect("conf should validate") + check conf.shardingConf.kind == AutoSharding + check conf.shardingConf.numShardsInCluster == 1 + + test "num-shards-in-network == 0 does not override preset": + ## Passing an AutoSharding preset and trying to override with + ## --num-shards-in-network=0 (which is StaticSharding) doesn't work. + ## Note that --num-shards-in-network=0 and omitting the switch are + ## internally the same. Promoting the config to an Option[uint16] is + ## probably not worth it since overriding an AutoSharding preset with + ## StaticSharding shouldn't make any sense (that is, no use case). + + ## Given: emulate --preset=logos.dev --num-shards-in-network=0 + let networkConf = NetworkConf.LogosDevConf() + var builder = WakuConfBuilder.init() + builder.withNetworkConf(networkConf) + # Note: builder.withNumShardsInCluster() is not called when the + # value that comes from the CLI path is 0 (which means it was + # either set to 0 or was left unset). + builder.withShardingConf(StaticSharding) + + ## When + let conf = builder.build().expect("build should succeed") + + ## Then: preset wins and StaticSharding user intent is lost + conf.validate().expect("conf should validate") + check conf.shardingConf.kind == networkConf.shardingConf.kind + check conf.shardingConf.numShardsInCluster == + networkConf.shardingConf.numShardsInCluster suite "Waku Conf - node key": test "Node key is generated": diff --git a/tests/node/peer_manager/peer_store/test_waku_peer_storage.nim b/tests/node/peer_manager/peer_store/test_waku_peer_storage.nim index c0e25ec6a..bf052205b 100644 --- a/tests/node/peer_manager/peer_store/test_waku_peer_storage.nim +++ b/tests/node/peer_manager/peer_store/test_waku_peer_storage.nim @@ -10,22 +10,21 @@ import import waku/waku_core/peers, waku/node/peer_manager/peer_store/waku_peer_storage proc `==`(a, b: RemotePeerInfo): bool = - let comparisons = - @[ - a.peerId == b.peerId, - a.addrs == b.addrs, - a.enr == b.enr, - a.protocols == b.protocols, - a.agent == b.agent, - a.protoVersion == b.protoVersion, - a.publicKey == b.publicKey, - a.connectedness == b.connectedness, - a.disconnectTime == b.disconnectTime, - a.origin == b.origin, - a.direction == b.direction, - a.lastFailedConn == b.lastFailedConn, - a.numberFailedConn == b.numberFailedConn, - ] + let comparisons = @[ + a.peerId == b.peerId, + a.addrs == b.addrs, + a.enr == b.enr, + a.protocols == b.protocols, + a.agent == b.agent, + a.protoVersion == b.protoVersion, + a.publicKey == b.publicKey, + a.connectedness == b.connectedness, + a.disconnectTime == b.disconnectTime, + a.origin == b.origin, + a.direction == b.direction, + a.lastFailedConn == b.lastFailedConn, + a.numberFailedConn == b.numberFailedConn, + ] allIt(comparisons, it == true) @@ -61,18 +60,17 @@ suite "Protobuf Serialisation": suite "encode": test "simple": # Given the expected bytes representation of a valid RemotePeerInfo - let expectedBuffer: seq[byte] = - @[ - 10, 39, 0, 37, 8, 2, 18, 33, 3, 43, 246, 238, 219, 109, 147, 79, 129, 40, 145, - 217, 209, 109, 105, 185, 186, 200, 180, 203, 72, 166, 220, 196, 232, 170, 74, - 141, 125, 255, 112, 238, 204, 18, 8, 4, 192, 168, 0, 1, 6, 31, 144, 34, 95, 8, - 3, 18, 91, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, - 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 222, 61, 48, 15, 163, 106, 224, 232, 245, - 213, 48, 137, 157, 131, 171, 171, 68, 171, 243, 22, 31, 22, 42, 75, 201, 1, - 216, 230, 236, 218, 2, 14, 139, 109, 95, 141, 163, 5, 37, 231, 29, 104, 81, - 81, 12, 9, 142, 92, 71, 198, 70, 165, 151, 251, 77, 206, 192, 52, 233, 247, - 124, 64, 158, 98, 40, 0, 48, 0, - ] + let expectedBuffer: seq[byte] = @[ + 10, 39, 0, 37, 8, 2, 18, 33, 3, 43, 246, 238, 219, 109, 147, 79, 129, 40, 145, + 217, 209, 109, 105, 185, 186, 200, 180, 203, 72, 166, 220, 196, 232, 170, 74, + 141, 125, 255, 112, 238, 204, 18, 8, 4, 192, 168, 0, 1, 6, 31, 144, 34, 95, 8, + 3, 18, 91, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, + 206, 61, 3, 1, 7, 3, 66, 0, 4, 222, 61, 48, 15, 163, 106, 224, 232, 245, 213, + 48, 137, 157, 131, 171, 171, 68, 171, 243, 22, 31, 22, 42, 75, 201, 1, 216, 230, + 236, 218, 2, 14, 139, 109, 95, 141, 163, 5, 37, 231, 29, 104, 81, 81, 12, 9, + 142, 92, 71, 198, 70, 165, 151, 251, 77, 206, 192, 52, 233, 247, 124, 64, 158, + 98, 40, 0, 48, 0, + ] # When converting a valid RemotePeerInfo to a ProtoBuffer let encodedRemotePeerInfo = encode(remotePeerInfo).get() @@ -87,18 +85,17 @@ suite "Protobuf Serialisation": suite "decode": test "simple": # Given the bytes representation of a valid RemotePeerInfo - let buffer: seq[byte] = - @[ - 10, 39, 0, 37, 8, 2, 18, 33, 3, 43, 246, 238, 219, 109, 147, 79, 129, 40, 145, - 217, 209, 109, 105, 185, 186, 200, 180, 203, 72, 166, 220, 196, 232, 170, 74, - 141, 125, 255, 112, 238, 204, 18, 8, 4, 192, 168, 0, 1, 6, 31, 144, 34, 95, 8, - 3, 18, 91, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, - 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 222, 61, 48, 15, 163, 106, 224, 232, 245, - 213, 48, 137, 157, 131, 171, 171, 68, 171, 243, 22, 31, 22, 42, 75, 201, 1, - 216, 230, 236, 218, 2, 14, 139, 109, 95, 141, 163, 5, 37, 231, 29, 104, 81, - 81, 12, 9, 142, 92, 71, 198, 70, 165, 151, 251, 77, 206, 192, 52, 233, 247, - 124, 64, 158, 98, 40, 0, 48, 0, - ] + let buffer: seq[byte] = @[ + 10, 39, 0, 37, 8, 2, 18, 33, 3, 43, 246, 238, 219, 109, 147, 79, 129, 40, 145, + 217, 209, 109, 105, 185, 186, 200, 180, 203, 72, 166, 220, 196, 232, 170, 74, + 141, 125, 255, 112, 238, 204, 18, 8, 4, 192, 168, 0, 1, 6, 31, 144, 34, 95, 8, + 3, 18, 91, 48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, + 206, 61, 3, 1, 7, 3, 66, 0, 4, 222, 61, 48, 15, 163, 106, 224, 232, 245, 213, + 48, 137, 157, 131, 171, 171, 68, 171, 243, 22, 31, 22, 42, 75, 201, 1, 216, 230, + 236, 218, 2, 14, 139, 109, 95, 141, 163, 5, 37, 231, 29, 104, 81, 81, 12, 9, + 142, 92, 71, 198, 70, 165, 151, 251, 77, 206, 192, 52, 233, 247, 124, 64, 158, + 98, 40, 0, 48, 0, + ] # When converting a valid buffer to RemotePeerInfo let decodedRemotePeerInfo = RemotePeerInfo.decode(buffer).get() diff --git a/tests/node/test_all.nim b/tests/node/test_all.nim index f6e7507b7..2846fdb7f 100644 --- a/tests/node/test_all.nim +++ b/tests/node/test_all.nim @@ -6,5 +6,5 @@ import ./test_wakunode_lightpush, ./test_wakunode_peer_exchange, ./test_wakunode_store, - ./test_wakunode_legacy_store, - ./test_wakunode_peer_manager + ./test_wakunode_peer_manager, + ./test_wakunode_health_monitor diff --git a/tests/node/test_wakunode_filter.nim b/tests/node/test_wakunode_filter.nim index 04db575ab..2777b0124 100644 --- a/tests/node/test_wakunode_filter.nim +++ b/tests/node/test_wakunode_filter.nim @@ -12,7 +12,7 @@ import waku_core, node/peer_manager, node/waku_node, - node/api, + node/kernel_api, waku_filter_v2, waku_filter_v2/client, waku_filter_v2/subscriptions, diff --git a/tests/node/test_wakunode_health_monitor.nim b/tests/node/test_wakunode_health_monitor.nim new file mode 100644 index 000000000..08f641a75 --- /dev/null +++ b/tests/node/test_wakunode_health_monitor.nim @@ -0,0 +1,434 @@ +{.used.} + +import + std/[json, options, sequtils, strutils, tables], testutils/unittests, chronos, results +import brokers/broker_context + +import + waku/[ + waku_core, + common/waku_protocol, + node/waku_node, + node/peer_manager, + node/health_monitor/health_status, + node/health_monitor/connection_status, + node/health_monitor/protocol_health, + node/health_monitor/topic_health, + node/health_monitor/node_health_monitor, + node/delivery_service/delivery_service, + node/delivery_service/subscription_manager, + node/kernel_api/relay, + node/kernel_api/store, + node/kernel_api/lightpush, + node/kernel_api/filter, + events/health_events, + events/peer_events, + waku_archive, + ] + +import ../testlib/[wakunode, wakucore], ../waku_archive/archive_utils + +const MockDLow = 4 # Mocked GossipSub DLow value + +const TestConnectivityTimeLimit = 3.seconds + +proc protoHealthMock(kind: WakuProtocol, health: HealthStatus): ProtocolHealth = + var ph = ProtocolHealth.init(kind) + if health == HealthStatus.READY: + return ph.ready() + else: + return ph.notReady("mock") + +suite "Health Monitor - health state calculation": + test "Disconnected, zero peers": + let protocols = @[ + protoHealthMock(RelayProtocol, HealthStatus.NOT_READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + protoHealthMock(FilterClientProtocol, HealthStatus.NOT_READY), + protoHealthMock(LightpushClientProtocol, HealthStatus.NOT_READY), + ] + let strength = initTable[WakuProtocol, int]() + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Disconnected + + test "PartiallyConnected, weak relay": + let weakCount = MockDLow - 1 + let protocols = @[protoHealthMock(RelayProtocol, HealthStatus.READY)] + var strength = initTable[WakuProtocol, int]() + strength[RelayProtocol] = weakCount + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + # Partially connected since relay connectivity is weak (> 0, but < dLow) + check state == ConnectionStatus.PartiallyConnected + + test "Connected, robust relay": + let protocols = @[protoHealthMock(RelayProtocol, HealthStatus.READY)] + var strength = initTable[WakuProtocol, int]() + strength[RelayProtocol] = MockDLow + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + # Fully connected since relay connectivity is ideal (>= dLow) + check state == ConnectionStatus.Connected + + test "Connected, robust edge": + let protocols = @[ + protoHealthMock(RelayProtocol, HealthStatus.NOT_MOUNTED), + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[LightpushClientProtocol] = HealthyThreshold + strength[FilterClientProtocol] = HealthyThreshold + strength[StoreClientProtocol] = HealthyThreshold + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Connected + + test "Disconnected, edge missing store": + let protocols = @[ + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[LightpushClientProtocol] = HealthyThreshold + strength[FilterClientProtocol] = HealthyThreshold + strength[StoreClientProtocol] = 0 + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Disconnected + + test "PartiallyConnected, edge meets minimum failover requirement": + let weakCount = max(1, HealthyThreshold - 1) + let protocols = @[ + protoHealthMock(LightpushClientProtocol, HealthStatus.READY), + protoHealthMock(FilterClientProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[LightpushClientProtocol] = weakCount + strength[FilterClientProtocol] = weakCount + strength[StoreClientProtocol] = weakCount + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.PartiallyConnected + + test "Connected, robust relay ignores store server": + let protocols = @[ + protoHealthMock(RelayProtocol, HealthStatus.READY), + protoHealthMock(StoreProtocol, HealthStatus.READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[RelayProtocol] = MockDLow + strength[StoreProtocol] = 0 + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Connected + + test "Connected, robust relay ignores store client": + let protocols = @[ + protoHealthMock(RelayProtocol, HealthStatus.READY), + protoHealthMock(StoreProtocol, HealthStatus.READY), + protoHealthMock(StoreClientProtocol, HealthStatus.NOT_READY), + ] + var strength = initTable[WakuProtocol, int]() + strength[RelayProtocol] = MockDLow + strength[StoreProtocol] = 0 + strength[StoreClientProtocol] = 0 + let state = calculateConnectionState(protocols, strength, some(MockDLow)) + check state == ConnectionStatus.Connected + +suite "Health Monitor - events": + asyncTest "Core (relay) health update": + var nodeA: WakuNode + lockNewGlobalBrokerContext: + let nodeAKey = generateSecp256k1Key() + nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0)) + (await nodeA.mountRelay()).expect("Node A failed to mount Relay") + await nodeA.start() + + let monitorA = NodeHealthMonitor.new(nodeA) + + var + lastStatus = ConnectionStatus.Disconnected + callbackCount = 0 + healthChangeSignal = newAsyncEvent() + + monitorA.onConnectionStatusChange = proc(status: ConnectionStatus) {.async.} = + lastStatus = status + callbackCount.inc() + healthChangeSignal.fire() + + monitorA.startHealthMonitor().expect("Health monitor failed to start") + + var nodeB: WakuNode + lockNewGlobalBrokerContext: + let nodeBKey = generateSecp256k1Key() + nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0)) + let driver = newSqliteArchiveDriver() + nodeB.mountArchive(driver).expect("Node B failed to mount archive") + (await nodeB.mountRelay()).expect("Node B failed to mount relay") + await nodeB.mountStore() + await nodeB.start() + + await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) + + proc dummyHandler(topic: PubsubTopic, msg: WakuMessage): Future[void] {.async.} = + discard + + nodeA.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), dummyHandler).expect( + "Node A failed to subscribe" + ) + nodeB.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), dummyHandler).expect( + "Node B failed to subscribe" + ) + + let connectTimeLimit = Moment.now() + TestConnectivityTimeLimit + var gotConnected = false + + while Moment.now() < connectTimeLimit: + if lastStatus == ConnectionStatus.PartiallyConnected: + gotConnected = true + break + + if await healthChangeSignal.wait().withTimeout(connectTimeLimit - Moment.now()): + healthChangeSignal.clear() + + check: + gotConnected == true + callbackCount >= 1 + lastStatus == ConnectionStatus.PartiallyConnected + + healthChangeSignal.clear() + + await nodeB.stop() + await nodeA.disconnectNode(nodeB.switch.peerInfo.toRemotePeerInfo()) + + let disconnectTimeLimit = Moment.now() + TestConnectivityTimeLimit + var gotDisconnected = false + + while Moment.now() < disconnectTimeLimit: + if lastStatus == ConnectionStatus.Disconnected: + gotDisconnected = true + break + + if await healthChangeSignal.wait().withTimeout(disconnectTimeLimit - Moment.now()): + healthChangeSignal.clear() + + check: + gotDisconnected == true + + await monitorA.stopHealthMonitor() + await nodeA.stop() + + asyncTest "Edge (light client) health update": + var nodeA: WakuNode + lockNewGlobalBrokerContext: + let nodeAKey = generateSecp256k1Key() + nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0)) + nodeA.mountLightpushClient() + await nodeA.mountFilterClient() + nodeA.mountStoreClient() + require nodeA.mountAutoSharding(1, 8).isOk + nodeA.mountMetadata(1, @[0'u16]).expect("Node A failed to mount metadata") + await nodeA.start() + + let ds = + DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService") + ds.startDeliveryService().expect("Failed to start DeliveryService") + + let monitorA = NodeHealthMonitor.new(nodeA) + + var + lastStatus = ConnectionStatus.Disconnected + callbackCount = 0 + healthChangeSignal = newAsyncEvent() + + monitorA.onConnectionStatusChange = proc(status: ConnectionStatus) {.async.} = + lastStatus = status + callbackCount.inc() + healthChangeSignal.fire() + + monitorA.startHealthMonitor().expect("Health monitor failed to start") + + var nodeB: WakuNode + lockNewGlobalBrokerContext: + let nodeBKey = generateSecp256k1Key() + nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0)) + let driver = newSqliteArchiveDriver() + nodeB.mountArchive(driver).expect("Node B failed to mount archive") + (await nodeB.mountRelay()).expect("Node B failed to mount relay") + (await nodeB.mountLightpush()).expect("Node B failed to mount lightpush") + await nodeB.mountFilter() + await nodeB.mountStore() + require nodeB.mountAutoSharding(1, 8).isOk + nodeB.mountMetadata(1, toSeq(0'u16 ..< 8'u16)).expect( + "Node B failed to mount metadata" + ) + await nodeB.start() + + var metadataFut = newFuture[void]("waitForMetadata") + let metadataLis = WakuPeerEvent + .listen( + nodeA.brokerCtx, + proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} = + if not metadataFut.finished and + evt.kind == WakuPeerEventKind.EventMetadataUpdated: + metadataFut.complete() + , + ) + .expect("Failed to listen for metadata") + + await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) + + let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit) + await WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis) + require metadataOk + + let connectTimeLimit = Moment.now() + TestConnectivityTimeLimit + var gotConnected = false + + while Moment.now() < connectTimeLimit: + if lastStatus == ConnectionStatus.PartiallyConnected: + gotConnected = true + break + + if await healthChangeSignal.wait().withTimeout(connectTimeLimit - Moment.now()): + healthChangeSignal.clear() + + check: + gotConnected == true + callbackCount >= 1 + lastStatus == ConnectionStatus.PartiallyConnected + + healthChangeSignal.clear() + + await nodeB.stop() + await nodeA.disconnectNode(nodeB.switch.peerInfo.toRemotePeerInfo()) + + let disconnectTimeLimit = Moment.now() + TestConnectivityTimeLimit + var gotDisconnected = false + + while Moment.now() < disconnectTimeLimit: + if lastStatus == ConnectionStatus.Disconnected: + gotDisconnected = true + break + + if await healthChangeSignal.wait().withTimeout(disconnectTimeLimit - Moment.now()): + healthChangeSignal.clear() + + check: + gotDisconnected == true + lastStatus == ConnectionStatus.Disconnected + + await monitorA.stopHealthMonitor() + await ds.stopDeliveryService() + await nodeA.stop() + + asyncTest "Edge health driven by confirmed filter subscriptions": + var nodeA: WakuNode + lockNewGlobalBrokerContext: + let nodeAKey = generateSecp256k1Key() + nodeA = newTestWakuNode(nodeAKey, parseIpAddress("127.0.0.1"), Port(0)) + await nodeA.mountFilterClient() + nodeA.mountLightpushClient() + nodeA.mountStoreClient() + require nodeA.mountAutoSharding(1, 8).isOk + nodeA.mountMetadata(1, @[0'u16]).expect("Node A failed to mount metadata") + await nodeA.start() + + let ds = + DeliveryService.new(false, nodeA).expect("Failed to create DeliveryService") + ds.startDeliveryService().expect("Failed to start DeliveryService") + let subMgr = ds.subscriptionManager + + var nodeB: WakuNode + lockNewGlobalBrokerContext: + let nodeBKey = generateSecp256k1Key() + nodeB = newTestWakuNode(nodeBKey, parseIpAddress("127.0.0.1"), Port(0)) + let driver = newSqliteArchiveDriver() + nodeB.mountArchive(driver).expect("Node B failed to mount archive") + (await nodeB.mountRelay()).expect("Node B failed to mount relay") + (await nodeB.mountLightpush()).expect("Node B failed to mount lightpush") + await nodeB.mountFilter() + await nodeB.mountStore() + require nodeB.mountAutoSharding(1, 8).isOk + nodeB.mountMetadata(1, toSeq(0'u16 ..< 8'u16)).expect( + "Node B failed to mount metadata" + ) + await nodeB.start() + + let monitorA = NodeHealthMonitor.new(nodeA) + + var + lastStatus = ConnectionStatus.Disconnected + healthSignal = newAsyncEvent() + + monitorA.onConnectionStatusChange = proc(status: ConnectionStatus) {.async.} = + lastStatus = status + healthSignal.fire() + + monitorA.startHealthMonitor().expect("Health monitor failed to start") + + var metadataFut = newFuture[void]("waitForMetadata") + let metadataLis = WakuPeerEvent + .listen( + nodeA.brokerCtx, + proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} = + if not metadataFut.finished and + evt.kind == WakuPeerEventKind.EventMetadataUpdated: + metadataFut.complete() + , + ) + .expect("Failed to listen for metadata") + + await nodeA.connectToNodes(@[nodeB.switch.peerInfo.toRemotePeerInfo()]) + + let metadataOk = await metadataFut.withTimeout(TestConnectivityTimeLimit) + await WakuPeerEvent.dropListener(nodeA.brokerCtx, metadataLis) + require metadataOk + + var deadline = Moment.now() + TestConnectivityTimeLimit + while Moment.now() < deadline: + if lastStatus == ConnectionStatus.PartiallyConnected: + break + if await healthSignal.wait().withTimeout(deadline - Moment.now()): + healthSignal.clear() + + check lastStatus == ConnectionStatus.PartiallyConnected + + var shardHealthFut = newFuture[EventShardTopicHealthChange]("waitForShardHealth") + + let shardHealthLis = EventShardTopicHealthChange + .listen( + nodeA.brokerCtx, + proc( + evt: EventShardTopicHealthChange + ): Future[void] {.async: (raises: []), gcsafe.} = + if not shardHealthFut.finished and ( + evt.health == TopicHealth.MINIMALLY_HEALTHY or + evt.health == TopicHealth.SUFFICIENTLY_HEALTHY + ): + shardHealthFut.complete(evt) + , + ) + .expect("Failed to listen for shard health") + + let contentTopic = ContentTopic("/waku/2/default-content/proto") + subMgr.subscribe(contentTopic).expect("Failed to subscribe") + + let shardHealthOk = await shardHealthFut.withTimeout(TestConnectivityTimeLimit) + await EventShardTopicHealthChange.dropListener(nodeA.brokerCtx, shardHealthLis) + + check shardHealthOk == true + check subMgr.edgeFilterSubStates.len > 0 + + healthSignal.clear() + deadline = Moment.now() + TestConnectivityTimeLimit + while Moment.now() < deadline: + if lastStatus == ConnectionStatus.PartiallyConnected: + break + if await healthSignal.wait().withTimeout(deadline - Moment.now()): + healthSignal.clear() + + check lastStatus == ConnectionStatus.PartiallyConnected + + await ds.stopDeliveryService() + await monitorA.stopHealthMonitor() + await nodeB.stop() + await nodeA.stop() diff --git a/tests/node/test_wakunode_legacy_lightpush.nim b/tests/node/test_wakunode_legacy_lightpush.nim index 9525892a1..68c6cacde 100644 --- a/tests/node/test_wakunode_legacy_lightpush.nim +++ b/tests/node/test_wakunode_legacy_lightpush.nim @@ -12,7 +12,8 @@ import waku_core, node/peer_manager, node/waku_node, - node/api, + node/kernel_api, + node/kernel_api/lightpush, waku_lightpush_legacy, waku_lightpush_legacy/common, waku_lightpush_legacy/protocol_metrics, @@ -24,9 +25,6 @@ import suite "Waku Legacy Lightpush - End To End": var - handlerFuture {.threadvar.}: Future[(PubsubTopic, WakuMessage)] - handler {.threadvar.}: PushMessageHandler - server {.threadvar.}: WakuNode client {.threadvar.}: WakuNode @@ -36,13 +34,6 @@ suite "Waku Legacy Lightpush - End To End": message {.threadvar.}: WakuMessage asyncSetup: - handlerFuture = newPushHandlerFuture() - handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage - ): Future[WakuLightPushResult[void]] {.async.} = - handlerFuture.complete((pubsubTopic, message)) - return ok() - let serverKey = generateSecp256k1Key() clientKey = generateSecp256k1Key() @@ -56,7 +47,7 @@ suite "Waku Legacy Lightpush - End To End": (await server.mountRelay()).isOkOr: assert false, "Failed to mount relay" - await server.mountLegacyLightpush() # without rln-relay + check (await server.mountLegacyLightpush()).isOk() # without rln-relay client.mountLegacyLightpushClient() serverRemotePeerInfo = server.peerInfo.toRemotePeerInfo() @@ -107,9 +98,6 @@ suite "Waku Legacy Lightpush - End To End": suite "RLN Proofs as a Lightpush Service": var - handlerFuture {.threadvar.}: Future[(PubsubTopic, WakuMessage)] - handler {.threadvar.}: PushMessageHandler - server {.threadvar.}: WakuNode client {.threadvar.}: WakuNode anvilProc {.threadvar.}: Process @@ -121,13 +109,6 @@ suite "RLN Proofs as a Lightpush Service": message {.threadvar.}: WakuMessage asyncSetup: - handlerFuture = newPushHandlerFuture() - handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage - ): Future[WakuLightPushResult[void]] {.async.} = - handlerFuture.complete((pubsubTopic, message)) - return ok() - let serverKey = generateSecp256k1Key() clientKey = generateSecp256k1Key() @@ -135,8 +116,8 @@ suite "RLN Proofs as a Lightpush Service": server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - anvilProc = runAnvil() - manager = waitFor setupOnchainGroupManager() + anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH)) + manager = waitFor setupOnchainGroupManager(deployContracts = false) # mount rln-relay let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) @@ -147,17 +128,14 @@ suite "RLN Proofs as a Lightpush Service": (await server.mountRelay()).isOkOr: assert false, "Failed to mount relay" await server.mountRlnRelay(wakuRlnConfig) - await server.mountLegacyLightPush() + check (await server.mountLegacyLightPush()).isOk() client.mountLegacyLightPushClient() let manager1 = cast[OnchainGroupManager](server.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node1", rootUpdated1 @@ -213,7 +191,7 @@ suite "Waku Legacy Lightpush message delivery": assert false, "Failed to mount relay" (await bridgeNode.mountRelay()).isOkOr: assert false, "Failed to mount relay" - await bridgeNode.mountLegacyLightPush() + check (await bridgeNode.mountLegacyLightPush()).isOk() lightNode.mountLegacyLightPushClient() discard await lightNode.peerManager.dialPeer( @@ -249,3 +227,19 @@ suite "Waku Legacy Lightpush message delivery": ## Cleanup await allFutures(lightNode.stop(), bridgeNode.stop(), destNode.stop()) + +suite "Waku Legacy Lightpush mounting behavior": + asyncTest "fails to mount when relay is not mounted": + ## Given a node without Relay mounted + let + key = generateSecp256k1Key() + node = newTestWakuNode(key, parseIpAddress("0.0.0.0"), Port(0)) + + # Do not mount Relay on purpose + check node.wakuRelay.isNil() + + ## Then mounting Legacy Lightpush must fail + let res = await node.mountLegacyLightPush() + check: + res.isErr() + res.error == MountWithoutRelayError diff --git a/tests/node/test_wakunode_legacy_store.nim b/tests/node/test_wakunode_legacy_store.nim deleted file mode 100644 index 1863066bc..000000000 --- a/tests/node/test_wakunode_legacy_store.nim +++ /dev/null @@ -1,1070 +0,0 @@ -{.used.} - -import std/options, testutils/unittests, chronos, libp2p/crypto/crypto - -import - waku/[ - common/paging, - node/waku_node, - node/api, - node/peer_manager, - waku_core, - waku_store_legacy, - waku_archive_legacy, - ], - ../waku_store_legacy/store_utils, - ../waku_archive_legacy/archive_utils, - ../testlib/[wakucore, wakunode, testasync, testutils] - -suite "Waku Store - End to End - Sorted Archive": - var pubsubTopic {.threadvar.}: PubsubTopic - var contentTopic {.threadvar.}: ContentTopic - var contentTopicSeq {.threadvar.}: seq[ContentTopic] - - var archiveMessages {.threadvar.}: seq[WakuMessage] - var historyQuery {.threadvar.}: HistoryQuery - - var server {.threadvar.}: WakuNode - var client {.threadvar.}: WakuNode - - var archiveDriver {.threadvar.}: ArchiveDriver - var serverRemotePeerInfo {.threadvar.}: RemotePeerInfo - var clientPeerId {.threadvar.}: PeerId - - asyncSetup: - pubsubTopic = DefaultPubsubTopic - contentTopic = DefaultContentTopic - contentTopicSeq = @[contentTopic] - - let timeOrigin = now() - archiveMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] - - historyQuery = HistoryQuery( - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.Forward, - pageSize: 5, - ) - - let - serverKey = generateSecp256k1Key() - clientKey = generateSecp256k1Key() - - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - archiveDriver = newArchiveDriverWithMessages(pubsubTopic, archiveMessages) - let mountArchiveResult = server.mountLegacyArchive(archiveDriver) - assert mountArchiveResult.isOk() - - await server.mountLegacyStore() - client.mountLegacyStoreClient() - - await allFutures(server.start(), client.start()) - - serverRemotePeerInfo = server.peerInfo.toRemotePeerInfo() - clientPeerId = client.peerInfo.toRemotePeerInfo().peerId - - asyncTeardown: - await allFutures(client.stop(), server.stop()) - - suite "Message Pagination": - asyncTest "Forward Pagination": - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[0 ..< 5] - - # Given the next query - var otherHistoryQuery = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - # When making the next history query - let otherQueryResponse = - await client.query(otherHistoryQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - otherQueryResponse.get().messages == archiveMessages[5 ..< 10] - - asyncTest "Backward Pagination": - # Given the history query is backward - historyQuery.direction = PagingDirection.BACKWARD - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[5 ..< 10] - - # Given the next query - var nextHistoryQuery = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.BACKWARD, - pageSize: 5, - ) - - # When making the next history query - let otherQueryResponse = - await client.query(nextHistoryQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - otherQueryResponse.get().messages == archiveMessages[0 ..< 5] - - suite "Pagination with Differente Page Sizes": - asyncTest "Pagination with Small Page Size": - # Given the first query (1/5) - historyQuery.pageSize = 2 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse1.get().messages == archiveMessages[0 ..< 2] - - # Given the next query (2/5) - let historyQuery2 = HistoryQuery( - cursor: queryResponse1.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 2, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[2 ..< 4] - - # Given the next query (3/5) - let historyQuery3 = HistoryQuery( - cursor: queryResponse2.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 2, - ) - - # When making the next history query - let queryResponse3 = await client.query(historyQuery3, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse3.get().messages == archiveMessages[4 ..< 6] - - # Given the next query (4/5) - let historyQuery4 = HistoryQuery( - cursor: queryResponse3.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 2, - ) - - # When making the next history query - let queryResponse4 = await client.query(historyQuery4, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse4.get().messages == archiveMessages[6 ..< 8] - - # Given the next query (5/5) - let historyQuery5 = HistoryQuery( - cursor: queryResponse4.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 2, - ) - - # When making the next history query - let queryResponse5 = await client.query(historyQuery5, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse5.get().messages == archiveMessages[8 ..< 10] - - asyncTest "Pagination with Large Page Size": - # Given the first query (1/2) - historyQuery.pageSize = 8 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse1.get().messages == archiveMessages[0 ..< 8] - - # Given the next query (2/2) - let historyQuery2 = HistoryQuery( - cursor: queryResponse1.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 8, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[8 ..< 10] - - asyncTest "Pagination with Excessive Page Size": - # Given the first query (1/1) - historyQuery.pageSize = 100 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse1.get().messages == archiveMessages[0 ..< 10] - - asyncTest "Pagination with Mixed Page Size": - # Given the first query (1/3) - historyQuery.pageSize = 2 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse1.get().messages == archiveMessages[0 ..< 2] - - # Given the next query (2/3) - let historyQuery2 = HistoryQuery( - cursor: queryResponse1.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 4, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[2 ..< 6] - - # Given the next query (3/3) - let historyQuery3 = HistoryQuery( - cursor: queryResponse2.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 6, - ) - - # When making the next history query - let queryResponse3 = await client.query(historyQuery3, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse3.get().messages == archiveMessages[6 ..< 10] - - asyncTest "Pagination with Zero Page Size (Behaves as DefaultPageSize)": - # Given a message list of size higher than the default page size - let currentStoreLen = uint((await archiveDriver.getMessagesCount()).get()) - assert archive.DefaultPageSize > currentStoreLen, - "This test requires a store with more than (DefaultPageSize) messages" - let missingMessagesAmount = archive.DefaultPageSize - currentStoreLen + 5 - - let lastMessageTimestamp = archiveMessages[archiveMessages.len - 1].timestamp - var extraMessages: seq[WakuMessage] = @[] - for i in 0 ..< missingMessagesAmount: - let - timestampOffset = 10 * int(i + 1) - # + 1 to avoid collision with existing messages - message: WakuMessage = - fakeWakuMessage(@[byte i], ts = ts(timestampOffset, lastMessageTimestamp)) - extraMessages.add(message) - discard archiveDriver.put(pubsubTopic, extraMessages) - - let totalMessages = archiveMessages & extraMessages - - # Given the a query with zero page size (1/2) - historyQuery.pageSize = 0 - - # When making a history query - let queryResponse1 = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the archive.DefaultPageSize messages - check: - queryResponse1.get().messages == totalMessages[0 ..< archive.DefaultPageSize] - - # Given the next query (2/2) - let historyQuery2 = HistoryQuery( - cursor: queryResponse1.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 0, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the remaining messages - check: - queryResponse2.get().messages == - totalMessages[archive.DefaultPageSize ..< archive.DefaultPageSize + 5] - - asyncTest "Pagination with Default Page Size": - # Given a message list of size higher than the default page size - let currentStoreLen = uint((await archiveDriver.getMessagesCount()).get()) - assert archive.DefaultPageSize > currentStoreLen, - "This test requires a store with more than (DefaultPageSize) messages" - let missingMessagesAmount = archive.DefaultPageSize - currentStoreLen + 5 - - let lastMessageTimestamp = archiveMessages[archiveMessages.len - 1].timestamp - var extraMessages: seq[WakuMessage] = @[] - for i in 0 ..< missingMessagesAmount: - let - timestampOffset = 10 * int(i + 1) - # + 1 to avoid collision with existing messages - message: WakuMessage = - fakeWakuMessage(@[byte i], ts = ts(timestampOffset, lastMessageTimestamp)) - extraMessages.add(message) - discard archiveDriver.put(pubsubTopic, extraMessages) - - let totalMessages = archiveMessages & extraMessages - - # Given a query with default page size (1/2) - historyQuery = HistoryQuery( - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - ) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == totalMessages[0 ..< archive.DefaultPageSize] - - # Given the next query (2/2) - let historyQuery2 = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == - totalMessages[archive.DefaultPageSize ..< archive.DefaultPageSize + 5] - - suite "Pagination with Different Cursors": - asyncTest "Starting Cursor": - # Given a cursor pointing to the first message - let cursor = computeHistoryCursor(pubsubTopic, archiveMessages[0]) - historyQuery.cursor = some(cursor) - historyQuery.pageSize = 1 - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the message - check: - queryResponse.get().messages == archiveMessages[1 ..< 2] - - asyncTest "Middle Cursor": - # Given a cursor pointing to the middle message1 - let cursor = computeHistoryCursor(pubsubTopic, archiveMessages[5]) - historyQuery.cursor = some(cursor) - historyQuery.pageSize = 1 - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the message - check: - queryResponse.get().messages == archiveMessages[6 ..< 7] - - asyncTest "Ending Cursor": - # Given a cursor pointing to the last message - let cursor = computeHistoryCursor(pubsubTopic, archiveMessages[9]) - historyQuery.cursor = some(cursor) - historyQuery.pageSize = 1 - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - suite "Message Sorting": - asyncTest "Cursor Reusability Across Nodes": - # Given a different server node with the same archive - let - otherArchiveDriverWithMessages = - newArchiveDriverWithMessages(pubsubTopic, archiveMessages) - otherServerKey = generateSecp256k1Key() - otherServer = - newTestWakuNode(otherServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountOtherArchiveResult = - otherServer.mountLegacyArchive(otherArchiveDriverWithMessages) - assert mountOtherArchiveResult.isOk() - - await otherServer.mountLegacyStore() - - await otherServer.start() - let otherServerRemotePeerInfo = otherServer.peerInfo.toRemotePeerInfo() - - # When making a history query to the first server node - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[0 ..< 5] - - # Given the cursor from the first query - let cursor = queryResponse.get().cursor - - # When making a history query to the second server node - let otherHistoryQuery = HistoryQuery( - cursor: cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - let otherQueryResponse = - await client.query(otherHistoryQuery, otherServerRemotePeerInfo) - - # Then the response contains the remaining messages - check: - otherQueryResponse.get().messages == archiveMessages[5 ..< 10] - - # Cleanup - await otherServer.stop() - -suite "Waku Store - End to End - Unsorted Archive": - var pubsubTopic {.threadvar.}: PubsubTopic - var contentTopic {.threadvar.}: ContentTopic - var contentTopicSeq {.threadvar.}: seq[ContentTopic] - - var historyQuery {.threadvar.}: HistoryQuery - var unsortedArchiveMessages {.threadvar.}: seq[WakuMessage] - - var server {.threadvar.}: WakuNode - var client {.threadvar.}: WakuNode - - var serverRemotePeerInfo {.threadvar.}: RemotePeerInfo - - asyncSetup: - pubsubTopic = DefaultPubsubTopic - contentTopic = DefaultContentTopic - contentTopicSeq = @[contentTopic] - - historyQuery = HistoryQuery( - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - let timeOrigin = now() - unsortedArchiveMessages = - @[ # SortIndex (by timestamp and digest) - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), # 1 - fakeWakuMessage(@[byte 03], ts = ts(00, timeOrigin)), # 2 - fakeWakuMessage(@[byte 08], ts = ts(00, timeOrigin)), # 0 - fakeWakuMessage(@[byte 07], ts = ts(10, timeOrigin)), # 4 - fakeWakuMessage(@[byte 02], ts = ts(10, timeOrigin)), # 3 - fakeWakuMessage(@[byte 09], ts = ts(10, timeOrigin)), # 5 - fakeWakuMessage(@[byte 06], ts = ts(20, timeOrigin)), # 6 - fakeWakuMessage(@[byte 01], ts = ts(20, timeOrigin)), # 9 - fakeWakuMessage(@[byte 04], ts = ts(20, timeOrigin)), # 7 - fakeWakuMessage(@[byte 05], ts = ts(20, timeOrigin)), # 8 - ] - - let - serverKey = generateSecp256k1Key() - clientKey = generateSecp256k1Key() - - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - let - unsortedArchiveDriverWithMessages = - newArchiveDriverWithMessages(pubsubTopic, unsortedArchiveMessages) - mountUnsortedArchiveResult = - server.mountLegacyArchive(unsortedArchiveDriverWithMessages) - - assert mountUnsortedArchiveResult.isOk() - - await server.mountLegacyStore() - client.mountLegacyStoreClient() - - await allFutures(server.start(), client.start()) - - serverRemotePeerInfo = server.peerInfo.toRemotePeerInfo() - - asyncTeardown: - await allFutures(client.stop(), server.stop()) - - asyncTest "Basic (Timestamp and Digest) Sorting Validation": - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - unsortedArchiveMessages[2], - unsortedArchiveMessages[0], - unsortedArchiveMessages[1], - unsortedArchiveMessages[4], - unsortedArchiveMessages[3], - ] - - # Given the next query - var historyQuery2 = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == - @[ - unsortedArchiveMessages[5], - unsortedArchiveMessages[6], - unsortedArchiveMessages[8], - unsortedArchiveMessages[9], - unsortedArchiveMessages[7], - ] - - asyncTest "Backward pagination with Ascending Sorting": - # Given a history query with backward pagination - let cursor = computeHistoryCursor(pubsubTopic, unsortedArchiveMessages[4]) - historyQuery.direction = PagingDirection.BACKWARD - historyQuery.cursor = some(cursor) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - unsortedArchiveMessages[2], - unsortedArchiveMessages[0], - unsortedArchiveMessages[1], - ] - - asyncTest "Forward Pagination with Ascending Sorting": - # Given a history query with forward pagination - let cursor = computeHistoryCursor(pubsubTopic, unsortedArchiveMessages[4]) - historyQuery.direction = PagingDirection.FORWARD - historyQuery.cursor = some(cursor) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - unsortedArchiveMessages[3], - unsortedArchiveMessages[5], - unsortedArchiveMessages[6], - unsortedArchiveMessages[8], - unsortedArchiveMessages[9], - ] - -suite "Waku Store - End to End - Archive with Multiple Topics": - var pubsubTopic {.threadvar.}: PubsubTopic - var pubsubTopicB {.threadvar.}: PubsubTopic - var contentTopic {.threadvar.}: ContentTopic - var contentTopicB {.threadvar.}: ContentTopic - var contentTopicC {.threadvar.}: ContentTopic - var contentTopicSpecials {.threadvar.}: ContentTopic - var contentTopicSeq {.threadvar.}: seq[ContentTopic] - - var historyQuery {.threadvar.}: HistoryQuery - var originTs {.threadvar.}: proc(offset: int): Timestamp {.gcsafe, raises: [].} - var archiveMessages {.threadvar.}: seq[WakuMessage] - - var server {.threadvar.}: WakuNode - var client {.threadvar.}: WakuNode - - var serverRemotePeerInfo {.threadvar.}: RemotePeerInfo - - asyncSetup: - pubsubTopic = DefaultPubsubTopic - pubsubTopicB = "topicB" - contentTopic = DefaultContentTopic - contentTopicB = "topicB" - contentTopicC = "topicC" - contentTopicSpecials = "!@#$%^&*()_+" - contentTopicSeq = - @[contentTopic, contentTopicB, contentTopicC, contentTopicSpecials] - - historyQuery = HistoryQuery( - pubsubTopic: some(pubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - let timeOrigin = now() - originTs = proc(offset = 0): Timestamp {.gcsafe, raises: [].} = - ts(offset, timeOrigin) - - archiveMessages = - @[ - fakeWakuMessage(@[byte 00], ts = originTs(00), contentTopic = contentTopic), - fakeWakuMessage(@[byte 01], ts = originTs(10), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 02], ts = originTs(20), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 03], ts = originTs(30), contentTopic = contentTopic), - fakeWakuMessage(@[byte 04], ts = originTs(40), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 05], ts = originTs(50), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 06], ts = originTs(60), contentTopic = contentTopic), - fakeWakuMessage(@[byte 07], ts = originTs(70), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 08], ts = originTs(80), contentTopic = contentTopicC), - fakeWakuMessage( - @[byte 09], ts = originTs(90), contentTopic = contentTopicSpecials - ), - ] - - let - serverKey = generateSecp256k1Key() - clientKey = generateSecp256k1Key() - - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - let archiveDriver = newSqliteArchiveDriver() - .put(pubsubTopic, archiveMessages[0 ..< 6]) - .put(pubsubTopicB, archiveMessages[6 ..< 10]) - let mountSortedArchiveResult = server.mountLegacyArchive(archiveDriver) - - assert mountSortedArchiveResult.isOk() - - await server.mountLegacyStore() - client.mountLegacyStoreClient() - - await allFutures(server.start(), client.start()) - - serverRemotePeerInfo = server.peerInfo.toRemotePeerInfo() - - asyncTeardown: - await allFutures(client.stop(), server.stop()) - - suite "Validation of Content Filtering": - asyncTest "Basic Content Filtering": - # Given a history query with content filtering - historyQuery.contentTopics = @[contentTopic] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == @[archiveMessages[0], archiveMessages[3]] - - asyncTest "Multiple Content Filters": - # Given a history query with multiple content filtering - historyQuery.contentTopics = @[contentTopic, contentTopicB] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - archiveMessages[0], - archiveMessages[1], - archiveMessages[3], - archiveMessages[4], - ] - - asyncTest "Empty Content Filtering": - # Given a history query with empty content filtering - historyQuery.contentTopics = @[] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[0 ..< 5] - - # Given the next query - let historyQuery2 = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: none(PubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[5 ..< 10] - - asyncTest "Non-Existent Content Topic": - # Given a history query with non-existent content filtering - historyQuery.contentTopics = @["non-existent-topic"] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - asyncTest "Special Characters in Content Filtering": - # Given a history query with special characters in content filtering - historyQuery.pubsubTopic = some(pubsubTopicB) - historyQuery.contentTopics = @["!@#$%^&*()_+"] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages == @[archiveMessages[9]] - - asyncTest "PubsubTopic Specified": - # Given a history query with pubsub topic specified - historyQuery.pubsubTopic = some(pubsubTopicB) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - archiveMessages[6], - archiveMessages[7], - archiveMessages[8], - archiveMessages[9], - ] - - asyncTest "PubsubTopic Left Empty": - # Given a history query with pubsub topic left empty - historyQuery.pubsubTopic = none(PubsubTopic) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == archiveMessages[0 ..< 5] - - # Given the next query - let historyQuery2 = HistoryQuery( - cursor: queryResponse.get().cursor, - pubsubTopic: none(PubsubTopic), - contentTopics: contentTopicSeq, - direction: PagingDirection.FORWARD, - pageSize: 5, - ) - - # When making the next history query - let queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse2.get().messages == archiveMessages[5 ..< 10] - - suite "Validation of Time-based Filtering": - asyncTest "Basic Time Filtering": - # Given a history query with start and end time - historyQuery.startTime = some(originTs(20)) - historyQuery.endTime = some(originTs(40)) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[archiveMessages[2], archiveMessages[3], archiveMessages[4]] - - asyncTest "Only Start Time Specified": - # Given a history query with only start time - historyQuery.startTime = some(originTs(20)) - historyQuery.endTime = none(Timestamp) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - archiveMessages[2], - archiveMessages[3], - archiveMessages[4], - archiveMessages[5], - ] - - asyncTest "Only End Time Specified": - # Given a history query with only end time - historyQuery.startTime = none(Timestamp) - historyQuery.endTime = some(originTs(40)) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages == - @[ - archiveMessages[0], - archiveMessages[1], - archiveMessages[2], - archiveMessages[3], - archiveMessages[4], - ] - - asyncTest "Invalid Time Range": - # Given a history query with invalid time range - historyQuery.startTime = some(originTs(60)) - historyQuery.endTime = some(originTs(40)) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - asyncTest "Time Filtering with Content Filtering": - # Given a history query with time and content filtering - historyQuery.startTime = some(originTs(20)) - historyQuery.endTime = some(originTs(60)) - historyQuery.contentTopics = @[contentTopicC] - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == @[archiveMessages[2], archiveMessages[5]] - - asyncTest "Messages Outside of Time Range": - # Given a history query with a valid time range which does not contain any messages - historyQuery.startTime = some(originTs(100)) - historyQuery.endTime = some(originTs(200)) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - suite "Ephemeral": - # TODO: Ephemeral value is not properly set for Sqlite - xasyncTest "Only ephemeral Messages:": - # Given an archive with only ephemeral messages - let - ephemeralMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] - ephemeralArchiveDriver = - newSqliteArchiveDriver().put(pubsubTopic, ephemeralMessages) - - # And a server node with the ephemeral archive - let - ephemeralServerKey = generateSecp256k1Key() - ephemeralServer = - newTestWakuNode(ephemeralServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountEphemeralArchiveResult = - ephemeralServer.mountLegacyArchive(ephemeralArchiveDriver) - assert mountEphemeralArchiveResult.isOk() - - await ephemeralServer.mountLegacyStore() - await ephemeralServer.start() - let ephemeralServerRemotePeerInfo = ephemeralServer.peerInfo.toRemotePeerInfo() - - # When making a history query to the server with only ephemeral messages - let queryResponse = - await client.query(historyQuery, ephemeralServerRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - # Cleanup - await ephemeralServer.stop() - - xasyncTest "Mixed messages": - # Given an archive with both ephemeral and non-ephemeral messages - let - ephemeralMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] - nonEphemeralMessages = - @[ - fakeWakuMessage(@[byte 03], ts = ts(30), ephemeral = false), - fakeWakuMessage(@[byte 04], ts = ts(40), ephemeral = false), - fakeWakuMessage(@[byte 05], ts = ts(50), ephemeral = false), - ] - mixedArchiveDriver = newSqliteArchiveDriver() - .put(pubsubTopic, ephemeralMessages) - .put(pubsubTopic, nonEphemeralMessages) - - # And a server node with the mixed archive - let - mixedServerKey = generateSecp256k1Key() - mixedServer = - newTestWakuNode(mixedServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountMixedArchiveResult = mixedServer.mountLegacyArchive(mixedArchiveDriver) - assert mountMixedArchiveResult.isOk() - - await mixedServer.mountLegacyStore() - await mixedServer.start() - let mixedServerRemotePeerInfo = mixedServer.peerInfo.toRemotePeerInfo() - - # When making a history query to the server with mixed messages - let queryResponse = await client.query(historyQuery, mixedServerRemotePeerInfo) - - # Then the response contains the non-ephemeral messages - check: - queryResponse.get().messages == nonEphemeralMessages - - # Cleanup - await mixedServer.stop() - - suite "Edge Case Scenarios": - asyncTest "Empty Message Store": - # Given an empty archive - let emptyArchiveDriver = newSqliteArchiveDriver() - - # And a server node with the empty archive - let - emptyServerKey = generateSecp256k1Key() - emptyServer = - newTestWakuNode(emptyServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountEmptyArchiveResult = emptyServer.mountLegacyArchive(emptyArchiveDriver) - assert mountEmptyArchiveResult.isOk() - - await emptyServer.mountLegacyStore() - await emptyServer.start() - let emptyServerRemotePeerInfo = emptyServer.peerInfo.toRemotePeerInfo() - - # When making a history query to the server with an empty archive - let queryResponse = await client.query(historyQuery, emptyServerRemotePeerInfo) - - # Then the response contains no messages - check: - queryResponse.get().messages.len == 0 - - # Cleanup - await emptyServer.stop() - - asyncTest "Voluminous Message Store": - # Given a voluminous archive (1M+ messages) - var voluminousArchiveMessages: seq[WakuMessage] = @[] - for i in 0 ..< 100000: - let topic = "topic" & $i - voluminousArchiveMessages.add(fakeWakuMessage(@[byte i], contentTopic = topic)) - let voluminousArchiveDriverWithMessages = - newArchiveDriverWithMessages(pubsubTopic, voluminousArchiveMessages) - - # And a server node with the voluminous archive - let - voluminousServerKey = generateSecp256k1Key() - voluminousServer = - newTestWakuNode(voluminousServerKey, parseIpAddress("0.0.0.0"), Port(0)) - mountVoluminousArchiveResult = - voluminousServer.mountLegacyArchive(voluminousArchiveDriverWithMessages) - assert mountVoluminousArchiveResult.isOk() - - await voluminousServer.mountLegacyStore() - await voluminousServer.start() - let voluminousServerRemotePeerInfo = voluminousServer.peerInfo.toRemotePeerInfo() - - # Given the following history query - historyQuery.contentTopics = - @["topic10000", "topic30000", "topic50000", "topic70000", "topic90000"] - - # When making a history query to the server with a voluminous archive - let queryResponse = - await client.query(historyQuery, voluminousServerRemotePeerInfo) - - # Then the response contains the messages - check: - queryResponse.get().messages == - @[ - voluminousArchiveMessages[10000], - voluminousArchiveMessages[30000], - voluminousArchiveMessages[50000], - voluminousArchiveMessages[70000], - voluminousArchiveMessages[90000], - ] - - # Cleanup - await voluminousServer.stop() - - asyncTest "Large contentFilters Array": - # Given a history query with the max contentFilters len, 10 - historyQuery.contentTopics = @[contentTopic] - for i in 0 ..< 9: - let topic = "topic" & $i - historyQuery.contentTopics.add(topic) - - # When making a history query - let queryResponse = await client.query(historyQuery, serverRemotePeerInfo) - - # Then the response should trigger no errors - check: - queryResponse.get().messages == @[archiveMessages[0], archiveMessages[3]] diff --git a/tests/node/test_wakunode_lightpush.nim b/tests/node/test_wakunode_lightpush.nim index 27c3a4d3e..b407327e3 100644 --- a/tests/node/test_wakunode_lightpush.nim +++ b/tests/node/test_wakunode_lightpush.nim @@ -12,7 +12,8 @@ import waku_core, node/peer_manager, node/waku_node, - node/api, + node/kernel_api, + node/kernel_api/lightpush, waku_lightpush, waku_rln_relay, ], @@ -36,13 +37,6 @@ suite "Waku Lightpush - End To End": message {.threadvar.}: WakuMessage asyncSetup: - handlerFuture = newPushHandlerFuture() - handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage - ): Future[WakuLightPushResult] {.async.} = - handlerFuture.complete((pubsubTopic, message)) - return ok(PublishedToOnePeer) - let serverKey = generateSecp256k1Key() clientKey = generateSecp256k1Key() @@ -55,7 +49,7 @@ suite "Waku Lightpush - End To End": (await server.mountRelay()).isOkOr: assert false, "Failed to mount relay" - await server.mountLightpush() # without rln-relay + check (await server.mountLightpush()).isOk() # without rln-relay client.mountLightpushClient() serverRemotePeerInfo = server.peerInfo.toRemotePeerInfo() @@ -107,9 +101,6 @@ suite "Waku Lightpush - End To End": suite "RLN Proofs as a Lightpush Service": var - handlerFuture {.threadvar.}: Future[(PubsubTopic, WakuMessage)] - handler {.threadvar.}: PushMessageHandler - server {.threadvar.}: WakuNode client {.threadvar.}: WakuNode anvilProc {.threadvar.}: Process @@ -121,13 +112,6 @@ suite "RLN Proofs as a Lightpush Service": message {.threadvar.}: WakuMessage asyncSetup: - handlerFuture = newPushHandlerFuture() - handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage - ): Future[WakuLightPushResult] {.async.} = - handlerFuture.complete((pubsubTopic, message)) - return ok(PublishedToOnePeer) - let serverKey = generateSecp256k1Key() clientKey = generateSecp256k1Key() @@ -135,8 +119,8 @@ suite "RLN Proofs as a Lightpush Service": server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - anvilProc = runAnvil() - manager = waitFor setupOnchainGroupManager() + anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH)) + manager = waitFor setupOnchainGroupManager(deployContracts = false) # mount rln-relay let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) @@ -147,17 +131,14 @@ suite "RLN Proofs as a Lightpush Service": (await server.mountRelay()).isOkOr: assert false, "Failed to mount relay" await server.mountRlnRelay(wakuRlnConfig) - await server.mountLightPush() + check (await server.mountLightPush()).isOk() client.mountLightPushClient() let manager1 = cast[OnchainGroupManager](server.wakuRlnRelay.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let rootUpdated1 = waitFor manager1.updateRoots() info "Updated root for node1", rootUpdated1 @@ -213,7 +194,7 @@ suite "Waku Lightpush message delivery": assert false, "Failed to mount relay" (await bridgeNode.mountRelay()).isOkOr: assert false, "Failed to mount relay" - await bridgeNode.mountLightPush() + check (await bridgeNode.mountLightPush()).isOk() lightNode.mountLightPushClient() discard await lightNode.peerManager.dialPeer( @@ -251,3 +232,19 @@ suite "Waku Lightpush message delivery": ## Cleanup await allFutures(lightNode.stop(), bridgeNode.stop(), destNode.stop()) + +suite "Waku Lightpush mounting behavior": + asyncTest "fails to mount when relay is not mounted": + ## Given a node without Relay mounted + let + key = generateSecp256k1Key() + node = newTestWakuNode(key, parseIpAddress("0.0.0.0"), Port(0)) + + # Do not mount Relay on purpose + check node.wakuRelay.isNil() + + ## Then mounting Lightpush must fail + let res = await node.mountLightPush() + check: + res.isErr() + res.error == MountWithoutRelayError diff --git a/tests/node/test_wakunode_peer_exchange.nim b/tests/node/test_wakunode_peer_exchange.nim index 4ebeae4ae..e6649c455 100644 --- a/tests/node/test_wakunode_peer_exchange.nim +++ b/tests/node/test_wakunode_peer_exchange.nim @@ -14,7 +14,7 @@ import import waku/[ waku_node, - node/api, + node/kernel_api, discovery/waku_discv5, waku_peer_exchange, node/peer_manager, @@ -66,15 +66,17 @@ suite "Waku Peer Exchange": suite "fetchPeerExchangePeers": var node2 {.threadvar.}: WakuNode + var node3 {.threadvar.}: WakuNode asyncSetup: node = newTestWakuNode(generateSecp256k1Key(), bindIp, bindPort) node2 = newTestWakuNode(generateSecp256k1Key(), bindIp, bindPort) + node3 = newTestWakuNode(generateSecp256k1Key(), bindIp, bindPort) - await allFutures(node.start(), node2.start()) + await allFutures(node.start(), node2.start(), node3.start()) asyncTeardown: - await allFutures(node.stop(), node2.stop()) + await allFutures(node.stop(), node2.stop(), node3.stop()) asyncTest "Node fetches without mounting peer exchange": # When a node, without peer exchange mounted, fetches peers @@ -104,12 +106,10 @@ suite "Waku Peer Exchange": await allFutures([node.mountPeerExchangeClient(), node2.mountPeerExchange()]) check node.peerManager.switch.peerStore.peers.len == 0 - # Mock that we discovered a node (to avoid running discv5) - var enr = enr.Record() - assert enr.fromUri( - "enr:-Iu4QGNuTvNRulF3A4Kb9YHiIXLr0z_CpvWkWjWKU-o95zUPR_In02AWek4nsSk7G_-YDcaT4bDRPzt5JIWvFqkXSNcBgmlkgnY0gmlwhE0WsGeJc2VjcDI1NmsxoQKp9VzU2FAh7fwOwSpg1M_Ekz4zzl0Fpbg6po2ZwgVwQYN0Y3CC6mCFd2FrdTIB" - ), "Failed to parse ENR" - node2.wakuPeerExchange.enrCache.add(enr) + # Simulate node2 discovering node3 via Discv5 + var rpInfo = node3.peerInfo.toRemotePeerInfo() + rpInfo.enr = some(node3.enr) + node2.peerManager.addPeer(rpInfo, PeerOrigin.Discv5) # Set node2 as service peer (default one) for px protocol node.peerManager.addServicePeer( @@ -121,10 +121,8 @@ suite "Waku Peer Exchange": check res.tryGet() == 1 # Check that the peer ended up in the peerstore - let rpInfo = enr.toRemotePeerInfo.get() check: node.peerManager.switch.peerStore.peers.anyIt(it.peerId == rpInfo.peerId) - node.peerManager.switch.peerStore.peers.anyIt(it.addrs == rpInfo.addrs) suite "setPeerExchangePeer": var node2 {.threadvar.}: WakuNode diff --git a/tests/node/test_wakunode_peer_manager.nim b/tests/node/test_wakunode_peer_manager.nim index 6b1c2a427..ed58db7fe 100644 --- a/tests/node/test_wakunode_peer_manager.nim +++ b/tests/node/test_wakunode_peer_manager.nim @@ -17,7 +17,7 @@ import waku_core, node/peer_manager, node/waku_node, - node/api, + node/kernel_api, discovery/waku_discv5, waku_filter_v2/common, waku_relay/protocol, diff --git a/tests/node/test_wakunode_relay_rln.nim b/tests/node/test_wakunode_relay_rln.nim index 1acf6b590..c8ca9b43d 100644 --- a/tests/node/test_wakunode_relay_rln.nim +++ b/tests/node/test_wakunode_relay_rln.nim @@ -17,7 +17,7 @@ import node/peer_manager, waku_core, waku_node, - node/api, + node/kernel_api, common/error_handling, waku_rln_relay, waku_rln_relay/rln, @@ -283,31 +283,31 @@ suite "Waku RlnRelay - End to End - Static": doAssert( client.wakuRlnRelay - .appendRLNProof( - message1b, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 0) - ) - .isOk() + .appendRLNProof( + message1b, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 0) + ) + .isOk() ) doAssert( client.wakuRlnRelay - .appendRLNProof( - message1kib, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 1) - ) - .isOk() + .appendRLNProof( + message1kib, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 1) + ) + .isOk() ) doAssert( client.wakuRlnRelay - .appendRLNProof( - message150kib, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 2) - ) - .isOk() + .appendRLNProof( + message150kib, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 2) + ) + .isOk() ) doAssert( client.wakuRlnRelay - .appendRLNProof( - message151kibPlus, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 3) - ) - .isOk() + .appendRLNProof( + message151kibPlus, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 3) + ) + .isOk() ) # When sending the 1B message @@ -372,10 +372,10 @@ suite "Waku RlnRelay - End to End - Static": doAssert( client.wakuRlnRelay - .appendRLNProof( - message151kibPlus, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 3) - ) - .isOk() + .appendRLNProof( + message151kibPlus, epoch + float64(client.wakuRlnRelay.rlnEpochSizeSec * 3) + ) + .isOk() ) # When sending the 150KiB plus message @@ -496,11 +496,11 @@ suite "Waku RlnRelay - End to End - OnChain": # However, it doesn't reduce the retries against the blockchain that the mounting rln process attempts (until it accepts failure). # Note: These retries might be an unintended library issue. discard await server - .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig1) - .withTimeout(FUTURE_TIMEOUT) + .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig1) + .withTimeout(FUTURE_TIMEOUT) discard await client - .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig2) - .withTimeout(FUTURE_TIMEOUT) + .setupRelayWithOnChainRln(@[pubsubTopic], wakuRlnConfig2) + .withTimeout(FUTURE_TIMEOUT) check: (await serverErrorFuture.waitForResult()).get() == @@ -514,7 +514,7 @@ suite "Waku RlnRelay - End to End - OnChain": ## Issues ### TreeIndex For some reason the calls to `getWakuRlnConfigOnChain` need to be made with `treeIndex` = 0 and 1, in that order. - But the registration needs to be made with 1 and 2. + But the registration needs to be made with 1 and 2. #### Solutions Requires investigation ### Monkeypatching diff --git a/tests/node/test_wakunode_sharding.nim b/tests/node/test_wakunode_sharding.nim index 945c22eee..f482b6abc 100644 --- a/tests/node/test_wakunode_sharding.nim +++ b/tests/node/test_wakunode_sharding.nim @@ -14,9 +14,8 @@ import waku/[ waku_core/topics/pubsub_topic, waku_core/topics/sharding, - waku_store_legacy/common, node/waku_node, - node/api, + node/kernel_api, common/paging, waku_core, waku_store/common, @@ -282,7 +281,7 @@ suite "Sharding": asyncTest "lightpush": # Given a connected server and client subscribed to the same pubsub topic client.mountLegacyLightPushClient() - await server.mountLightpush() + check (await server.mountLightpush()).isOk() let topic = "/waku/2/rs/0/1" @@ -405,7 +404,7 @@ suite "Sharding": asyncTest "lightpush (automatic sharding filtering)": # Given a connected server and client using the same content topic (with two different formats) client.mountLegacyLightPushClient() - await server.mountLightpush() + check (await server.mountLightpush()).isOk() let contentTopicShort = "/toychat/2/huilong/proto" @@ -434,18 +433,16 @@ suite "Sharding": contentTopicShort = "/toychat/2/huilong/proto" contentTopicFull = "/0/toychat/2/huilong/proto" pubsubTopic = "/waku/2/rs/0/58355" - archiveMessages1 = - @[ - fakeWakuMessage( - @[byte 00], ts = ts(00, timeOrigin), contentTopic = contentTopicShort - ) - ] - archiveMessages2 = - @[ - fakeWakuMessage( - @[byte 01], ts = ts(10, timeOrigin), contentTopic = contentTopicFull - ) - ] + archiveMessages1 = @[ + fakeWakuMessage( + @[byte 00], ts = ts(00, timeOrigin), contentTopic = contentTopicShort + ) + ] + archiveMessages2 = @[ + fakeWakuMessage( + @[byte 01], ts = ts(10, timeOrigin), contentTopic = contentTopicFull + ) + ] archiveDriver = newArchiveDriverWithMessages(pubsubTopic, archiveMessages1) discard archiveDriver.put(pubsubTopic, archiveMessages2) let mountArchiveResult = server.mountArchive(archiveDriver) @@ -456,29 +453,33 @@ suite "Sharding": # Given one query for each content topic format let - historyQuery1 = HistoryQuery( + storeQuery1 = StoreQueryRequest( contentTopics: @[contentTopicShort], - direction: PagingDirection.Forward, - pageSize: 3, + paginationForward: PagingDirection.Forward, + paginationLimit: some(3'u64), + includeData: true, ) - historyQuery2 = HistoryQuery( + storeQuery2 = StoreQueryRequest( contentTopics: @[contentTopicFull], - direction: PagingDirection.Forward, - pageSize: 3, + paginationForward: PagingDirection.Forward, + paginationLimit: some(3'u64), + includeData: true, ) # When the client queries the server for the messages let serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() - queryResponse1 = await client.query(historyQuery1, serverRemotePeerInfo) - queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) + queryResponse1 = await client.query(storeQuery1, serverRemotePeerInfo) + queryResponse2 = await client.query(storeQuery2, serverRemotePeerInfo) assertResultOk(queryResponse1) assertResultOk(queryResponse2) # Then the responses of both queries should contain all the messages check: - queryResponse1.get().messages == archiveMessages1 & archiveMessages2 - queryResponse2.get().messages == archiveMessages1 & archiveMessages2 + queryResponse1.get().messages.mapIt(it.message.get()) == + archiveMessages1 & archiveMessages2 + queryResponse2.get().messages.mapIt(it.message.get()) == + archiveMessages1 & archiveMessages2 asyncTest "relay - exclusion (automatic sharding filtering)": # Given a connected server and client subscribed to different content topics @@ -563,7 +564,7 @@ suite "Sharding": asyncTest "lightpush - exclusion (automatic sharding filtering)": # Given a connected server and client using different content topics client.mountLegacyLightPushClient() - await server.mountLightpush() + check (await server.mountLightpush()).isOk() let contentTopic1 = "/toychat/2/huilong/proto" @@ -597,18 +598,16 @@ suite "Sharding": contentTopic2 = "/0/toychat2/2/huilong/proto" pubsubTopic2 = "/waku/2/rs/0/23286" # Automatically generated from the contentTopic above - archiveMessages1 = - @[ - fakeWakuMessage( - @[byte 00], ts = ts(00, timeOrigin), contentTopic = contentTopic1 - ) - ] - archiveMessages2 = - @[ - fakeWakuMessage( - @[byte 01], ts = ts(10, timeOrigin), contentTopic = contentTopic2 - ) - ] + archiveMessages1 = @[ + fakeWakuMessage( + @[byte 00], ts = ts(00, timeOrigin), contentTopic = contentTopic1 + ) + ] + archiveMessages2 = @[ + fakeWakuMessage( + @[byte 01], ts = ts(10, timeOrigin), contentTopic = contentTopic2 + ) + ] archiveDriver = newArchiveDriverWithMessages(pubsubTopic1, archiveMessages1) discard archiveDriver.put(pubsubTopic2, archiveMessages2) let mountArchiveResult = server.mountArchive(archiveDriver) @@ -619,29 +618,31 @@ suite "Sharding": # Given one query for each content topic let - historyQuery1 = HistoryQuery( + storeQuery1 = StoreQueryRequest( contentTopics: @[contentTopic1], - direction: PagingDirection.Forward, - pageSize: 2, + paginationForward: PagingDirection.Forward, + paginationLimit: some(2'u64), + includeData: true, ) - historyQuery2 = HistoryQuery( + storeQuery2 = StoreQueryRequest( contentTopics: @[contentTopic2], - direction: PagingDirection.Forward, - pageSize: 2, + paginationForward: PagingDirection.Forward, + paginationLimit: some(2'u64), + includeData: true, ) # When the client queries the server for the messages let serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() - queryResponse1 = await client.query(historyQuery1, serverRemotePeerInfo) - queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) + queryResponse1 = await client.query(storeQuery1, serverRemotePeerInfo) + queryResponse2 = await client.query(storeQuery2, serverRemotePeerInfo) assertResultOk(queryResponse1) assertResultOk(queryResponse2) # Then each response should contain only the messages of the corresponding content topic check: - queryResponse1.get().messages == archiveMessages1 - queryResponse2.get().messages == archiveMessages2 + queryResponse1.get().messages.mapIt(it.message.get()) == archiveMessages1 + queryResponse2.get().messages.mapIt(it.message.get()) == archiveMessages2 suite "Specific Tests": asyncTest "Configure Node with Multiple PubSub Topics": @@ -874,7 +875,7 @@ suite "Sharding": asyncTest "Waku LightPush Sharding (Static Sharding)": # Given a connected server and client using two different pubsub topics client.mountLegacyLightPushClient() - await server.mountLightpush() + check (await server.mountLightpush()).isOk() # Given a connected server and client subscribed to multiple pubsub topics let @@ -1007,22 +1008,30 @@ suite "Sharding": # Given one query for each pubsub topic let - historyQuery1 = HistoryQuery( - pubsubTopic: some(topic1), direction: PagingDirection.Forward, pageSize: 2 + storeQuery1 = StoreQueryRequest( + pubsubTopic: some(topic1), + paginationForward: PagingDirection.Forward, + paginationLimit: some(2'u64), + includeData: true, ) - historyQuery2 = HistoryQuery( - pubsubTopic: some(topic2), direction: PagingDirection.Forward, pageSize: 2 + storeQuery2 = StoreQueryRequest( + pubsubTopic: some(topic2), + paginationForward: PagingDirection.Forward, + paginationLimit: some(2'u64), + includeData: true, ) # When the client queries the server for the messages let serverRemotePeerInfo = server.switch.peerInfo.toRemotePeerInfo() - queryResponse1 = await client.query(historyQuery1, serverRemotePeerInfo) - queryResponse2 = await client.query(historyQuery2, serverRemotePeerInfo) + queryResponse1 = await client.query(storeQuery1, serverRemotePeerInfo) + queryResponse2 = await client.query(storeQuery2, serverRemotePeerInfo) assertResultOk(queryResponse1) assertResultOk(queryResponse2) # Then each response should contain only the messages of the corresponding pubsub topic check: - queryResponse1.get().messages == archiveMessages1[0 ..< 1] - queryResponse2.get().messages == archiveMessages2[0 ..< 1] + queryResponse1.get().messages.mapIt(it.message.get()) == + archiveMessages1[0 ..< 1] + queryResponse2.get().messages.mapIt(it.message.get()) == + archiveMessages2[0 ..< 1] diff --git a/tests/node/test_wakunode_store.nim b/tests/node/test_wakunode_store.nim index 284b32e64..01deb2903 100644 --- a/tests/node/test_wakunode_store.nim +++ b/tests/node/test_wakunode_store.nim @@ -6,7 +6,7 @@ import waku/[ common/paging, node/waku_node, - node/api, + node/kernel_api, node/peer_manager, waku_core, waku_core/message/digest, @@ -38,19 +38,18 @@ suite "Waku Store - End to End - Sorted Archive": contentTopicSeq = @[contentTopic] let timeOrigin = now() - let messages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let messages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] archiveMessages = messages.mapIt( WakuMessageKeyValue( messageHash: computeMessageHash(pubsubTopic, it), @@ -486,7 +485,7 @@ suite "Waku Store - End to End - Sorted Archive": await otherServer.start() let otherServerRemotePeerInfo = otherServer.peerInfo.toRemotePeerInfo() - # When making a history query to the first server node + # When making a history query to the first server node let queryResponse = await client.query(storeQuery, serverRemotePeerInfo) # Then the response contains the messages @@ -542,19 +541,18 @@ suite "Waku Store - End to End - Unsorted Archive": ) let timeOrigin = now() - let messages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(20, timeOrigin)), - ] + let messages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(20, timeOrigin)), + ] unsortedArchiveMessages = messages.mapIt( WakuMessageKeyValue( messageHash: computeMessageHash(pubsubTopic, it), @@ -759,19 +757,19 @@ suite "Waku Store - End to End - Unsorted Archive without provided Timestamp": paginationLimit: some(uint64(5)), ) - let messages = - @[ # Not providing explicit timestamp means it will be set in "arrive" order - fakeWakuMessage(@[byte 09]), - fakeWakuMessage(@[byte 07]), - fakeWakuMessage(@[byte 05]), - fakeWakuMessage(@[byte 03]), - fakeWakuMessage(@[byte 01]), - fakeWakuMessage(@[byte 00]), - fakeWakuMessage(@[byte 02]), - fakeWakuMessage(@[byte 04]), - fakeWakuMessage(@[byte 06]), - fakeWakuMessage(@[byte 08]), - ] + let messages = @[ + # Not providing explicit timestamp means it will be set in "arrive" order + fakeWakuMessage(@[byte 09]), + fakeWakuMessage(@[byte 07]), + fakeWakuMessage(@[byte 05]), + fakeWakuMessage(@[byte 03]), + fakeWakuMessage(@[byte 01]), + fakeWakuMessage(@[byte 00]), + fakeWakuMessage(@[byte 02]), + fakeWakuMessage(@[byte 04]), + fakeWakuMessage(@[byte 06]), + fakeWakuMessage(@[byte 08]), + ] unsortedArchiveMessages = messages.mapIt( WakuMessageKeyValue( messageHash: computeMessageHash(pubsubTopic, it), @@ -900,21 +898,20 @@ suite "Waku Store - End to End - Archive with Multiple Topics": originTs = proc(offset = 0): Timestamp {.gcsafe, raises: [].} = ts(offset, timeOrigin) - let messages = - @[ - fakeWakuMessage(@[byte 00], ts = originTs(00), contentTopic = contentTopic), - fakeWakuMessage(@[byte 01], ts = originTs(10), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 02], ts = originTs(20), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 03], ts = originTs(30), contentTopic = contentTopic), - fakeWakuMessage(@[byte 04], ts = originTs(40), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 05], ts = originTs(50), contentTopic = contentTopicC), - fakeWakuMessage(@[byte 06], ts = originTs(60), contentTopic = contentTopic), - fakeWakuMessage(@[byte 07], ts = originTs(70), contentTopic = contentTopicB), - fakeWakuMessage(@[byte 08], ts = originTs(80), contentTopic = contentTopicC), - fakeWakuMessage( - @[byte 09], ts = originTs(90), contentTopic = contentTopicSpecials - ), - ] + let messages = @[ + fakeWakuMessage(@[byte 00], ts = originTs(00), contentTopic = contentTopic), + fakeWakuMessage(@[byte 01], ts = originTs(10), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 02], ts = originTs(20), contentTopic = contentTopicC), + fakeWakuMessage(@[byte 03], ts = originTs(30), contentTopic = contentTopic), + fakeWakuMessage(@[byte 04], ts = originTs(40), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 05], ts = originTs(50), contentTopic = contentTopicC), + fakeWakuMessage(@[byte 06], ts = originTs(60), contentTopic = contentTopic), + fakeWakuMessage(@[byte 07], ts = originTs(70), contentTopic = contentTopicB), + fakeWakuMessage(@[byte 08], ts = originTs(80), contentTopic = contentTopicC), + fakeWakuMessage( + @[byte 09], ts = originTs(90), contentTopic = contentTopicSpecials + ), + ] archiveMessages = messages.mapIt( WakuMessageKeyValue( @@ -1172,12 +1169,11 @@ suite "Waku Store - End to End - Archive with Multiple Topics": xasyncTest "Only ephemeral Messages:": # Given an archive with only ephemeral messages let - ephemeralMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] + ephemeralMessages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), + fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), + fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), + ] ephemeralArchiveDriver = newSqliteArchiveDriver().put(pubsubTopic, ephemeralMessages) @@ -1207,18 +1203,16 @@ suite "Waku Store - End to End - Archive with Multiple Topics": xasyncTest "Mixed messages": # Given an archive with both ephemeral and non-ephemeral messages let - ephemeralMessages = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), - fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), - fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), - ] - nonEphemeralMessages = - @[ - fakeWakuMessage(@[byte 03], ts = ts(30), ephemeral = false), - fakeWakuMessage(@[byte 04], ts = ts(40), ephemeral = false), - fakeWakuMessage(@[byte 05], ts = ts(50), ephemeral = false), - ] + ephemeralMessages = @[ + fakeWakuMessage(@[byte 00], ts = ts(00), ephemeral = true), + fakeWakuMessage(@[byte 01], ts = ts(10), ephemeral = true), + fakeWakuMessage(@[byte 02], ts = ts(20), ephemeral = true), + ] + nonEphemeralMessages = @[ + fakeWakuMessage(@[byte 03], ts = ts(30), ephemeral = false), + fakeWakuMessage(@[byte 04], ts = ts(40), ephemeral = false), + fakeWakuMessage(@[byte 05], ts = ts(50), ephemeral = false), + ] mixedArchiveDriver = newSqliteArchiveDriver() .put(pubsubTopic, ephemeralMessages) .put(pubsubTopic, nonEphemeralMessages) diff --git a/tests/persistency/test_all.nim b/tests/persistency/test_all.nim new file mode 100644 index 000000000..194977692 --- /dev/null +++ b/tests/persistency/test_all.nim @@ -0,0 +1,9 @@ +{.used.} + +import ./test_keys +import ./test_backend +import ./test_lifecycle +import ./test_facade +import ./test_encoding +import ./test_string_lookup +import ./test_singleton diff --git a/tests/persistency/test_backend.nim b/tests/persistency/test_backend.nim new file mode 100644 index 000000000..e5689d95f --- /dev/null +++ b/tests/persistency/test_backend.nim @@ -0,0 +1,195 @@ +{.used.} + +import std/options +import results +import testutils/unittests +import waku/persistency/[types, keys, backend_sqlite] + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +proc payload(s: string): seq[byte] = + result = newSeq[byte](s.len) + for i, c in s: + result[i] = byte(c) + +suite "Persistency SQLite backend": + test "open in-memory backend and round-trip a single value": + let b = openBackendInMemory().get() + defer: + b.close() + + b + .applyOps( + [ + TxOp( + category: "msg", + key: key("c1", 1'i64), + kind: txPut, + payload: payload("hello"), + ) + ] + ) + .get() + + let got = b.getOne("msg", key("c1", 1'i64)).get() + check got.isSome + check str(got.get) == "hello" + + check b.existsOne("msg", key("c1", 1'i64)).get() + check not b.existsOne("msg", key("c1", 2'i64)).get() + + test "INSERT OR REPLACE overwrites payload for the same key": + let b = openBackendInMemory().get() + defer: + b.close() + let k = key("c1", 1'i64) + b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("v1"))]).get() + b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("v2"))]).get() + check str(b.getOne("msg", k).get().get) == "v2" + + test "deleteOne reports whether the row existed": + let b = openBackendInMemory().get() + defer: + b.close() + let k = key("c1", 1'i64) + check not b.deleteOne("msg", k).get() + b.applyOps([TxOp(category: "msg", key: k, kind: txPut, payload: payload("x"))]).get() + check b.deleteOne("msg", k).get() + check not b.existsOne("msg", k).get() + + test "applyOps batches multiple ops atomically": + let b = openBackendInMemory().get() + defer: + b.close() + b + .applyOps( + [ + TxOp( + category: "msg", key: key("c1", 1'i64), kind: txPut, payload: payload("a") + ), + TxOp( + category: "msg", key: key("c1", 2'i64), kind: txPut, payload: payload("b") + ), + TxOp( + category: "msg", key: key("c1", 3'i64), kind: txPut, payload: payload("c") + ), + ] + ) + .get() + check b.countRange("msg", prefixRange(key("c1"))).get() == 3 + + test "scanRange ascending yields rows in key order": + let b = openBackendInMemory().get() + defer: + b.close() + let inserts = @[5'i64, 1, 4, 2, 3] + var ops: seq[TxOp] = @[] + for i in inserts: + ops.add( + TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i)) + ) + b.applyOps(ops).get() + + let rows = b.scanRange("msg", prefixRange(key("c1"))).get() + check rows.len == 5 + var seenOrder: seq[string] + for r in rows: + seenOrder.add(str(r.payload)) + check seenOrder == @["1", "2", "3", "4", "5"] + + test "scanRange descending yields rows in reverse key order": + let b = openBackendInMemory().get() + defer: + b.close() + for i in [1'i64, 2, 3]: + b + .applyOps( + [TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))] + ) + .get() + let rows = b.scanRange("msg", prefixRange(key("c1")), reverse = true).get() + check rows.len == 3 + check str(rows[0].payload) == "3" + check str(rows[2].payload) == "1" + + test "scanRange respects half-open [start, stop) bounds": + let b = openBackendInMemory().get() + defer: + b.close() + for i in [1'i64, 2, 3, 4, 5]: + b + .applyOps( + [TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))] + ) + .get() + let rng = KeyRange(start: key("c1", 2'i64), stop: key("c1", 4'i64)) + let rows = b.scanRange("msg", rng).get() + check rows.len == 2 # 2 and 3, not 4 + check str(rows[0].payload) == "2" + check str(rows[1].payload) == "3" + + test "scanRange with empty stop is open-ended": + let b = openBackendInMemory().get() + defer: + b.close() + for i in [1'i64, 2, 3]: + b + .applyOps( + [TxOp(category: "msg", key: key("c1", i), kind: txPut, payload: payload($i))] + ) + .get() + let rng = KeyRange(start: key("c1", 2'i64), stop: rawKey(@[])) + let rows = b.scanRange("msg", rng).get() + check rows.len == 2 + check str(rows[1].payload) == "3" + + test "categories isolate keyspaces": + let b = openBackendInMemory().get() + defer: + b.close() + let k = key("c1", 1'i64) + b + .applyOps( + [ + TxOp(category: "log", key: k, kind: txPut, payload: payload("log-1")), + TxOp( + category: "outgoing", key: k, kind: txPut, payload: payload("outgoing-1") + ), + ] + ) + .get() + check str(b.getOne("log", k).get().get) == "log-1" + check str(b.getOne("outgoing", k).get().get) == "outgoing-1" + check b.countRange("log", prefixRange(key("c1"))).get() == 1 + check b.countRange("outgoing", prefixRange(key("c1"))).get() == 1 + + test "txDelete inside a batch removes the row": + let b = openBackendInMemory().get() + defer: + b.close() + let k = key("c1", 1'i64) + b + .applyOps( + [ + TxOp(category: "msg", key: k, kind: txPut, payload: payload("v")), + TxOp(category: "msg", key: k, kind: txDelete), + ] + ) + .get() + check not b.existsOne("msg", k).get() + + test "missing key returns none": + let b = openBackendInMemory().get() + defer: + b.close() + check b.getOne("msg", key("nope")).get().isNone + + test "countRange of empty category is zero": + let b = openBackendInMemory().get() + defer: + b.close() + check b.countRange("msg", prefixRange(key("c1"))).get() == 0 diff --git a/tests/persistency/test_encoding.nim b/tests/persistency/test_encoding.nim new file mode 100644 index 000000000..22bd58209 --- /dev/null +++ b/tests/persistency/test_encoding.nim @@ -0,0 +1,154 @@ +{.used.} + +import std/[algorithm, options, os, times] +import chronos, results +import testutils/unittests +import waku/persistency/persistency + +# Reusable byte-wise comparator (Key has its own `<`, but we sometimes +# want to sort `seq[Key]` here without relying on it for double-checking). +proc cmpBytes(a, b: Key): int = + let ab = bytes(a) + let bb = bytes(b) + let n = min(ab.len, bb.len) + for i in 0 ..< n: + if ab[i] != bb[i]: + return cmp(ab[i], bb[i]) + cmp(ab.len, bb.len) + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +# Shared payload types used by multiple tests. +type + Mood = enum + moodCalm + moodHappy + moodAngry + + Header = object + sender: string + epoch: int64 + + Msg = object + header: Header + mood: Mood + body: seq[byte] + +suite "Persistency generic encoding": + # ── Key macro: composite types ──────────────────────────────────────── + + test "key macro accepts plain tuples": + let k1 = key(("ch", 1'i64)) + let k2 = key("ch", 1'i64) + # A plain tuple is encoded field-by-field, so the result is identical + # to passing the fields directly. + check k1 == k2 + + test "key macro accepts named tuples": + type Coord = tuple[lane: string, seqNum: int64] + let k = key((lane: "a", seqNum: 7'i64)) + let kFlat = key("a", 7'i64) + check k == kFlat + + test "key macro accepts a user object": + let k1 = key(Header(sender: "alice", epoch: 5'i64)) + let k2 = key("alice", 5'i64) + check k1 == k2 + + test "key macro accepts nested object inside another arg": + let k1 = key("v1", Header(sender: "alice", epoch: 5'i64)) + let k2 = key("v1", "alice", 5'i64) + check k1 == k2 + + test "key macro encodes enums": + let k1 = key(moodAngry) + let k2 = key(int64(ord(moodAngry))) + check k1 == k2 + + test "toKey is equivalent to single-arg key()": + check toKey("x") == key("x") + check toKey(42'i64) == key(42'i64) + check toKey(Header(sender: "a", epoch: 1)) == key("a", 1'i64) + + test "tuple-encoded keys preserve field-major sort order": + let inputs = @[ + key(("a", 0'i64)), + key(("a", 1'i64)), + key(("a", int64.high)), + key(("b", int64.low)), + key(("b", 0'i64)), + ] + var shuffled = @[inputs[3], inputs[0], inputs[4], inputs[2], inputs[1]] + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "embedded Key encodes verbatim": + let inner = key("a", 7'i64) + let outer = key("prefix", inner) + # Expanded: bytes of "prefix" + raw bytes of inner. + let expanded = key("prefix", "a", 7'i64) + check outer == expanded + + # ── Payload macro / toPayload ───────────────────────────────────────── + + test "toPayload encodes primitives": + check str(toPayload("hi")).len == 4 # 2-byte len prefix + 2 chars + check toPayload(42'i64).len == 8 + check toPayload(true) == @[1'u8] + check toPayload(false) == @[0'u8] + + test "toPayload encodes objects field-by-field": + let m = Msg( + header: Header(sender: "alice", epoch: 9'i64), + mood: moodHappy, + body: @[0xAA'u8, 0xBB, 0xCC], + ) + let p = toPayload(m) + let pManual = payload("alice", 9'i64, int64(ord(moodHappy)), @[0xAA'u8, 0xBB, 0xCC]) + check p == pManual + + test "payload macro concatenates parts": + let p = payload("v1", 1'i64, @[0xDE'u8, 0xAD]) + # Same as building each piece separately. + var expected: seq[byte] = @[] + encodePart(expected, "v1") + encodePart(expected, 1'i64) + encodePart(expected, @[0xDE'u8, 0xAD]) + check p == expected + + # ── End-to-end through the facade ───────────────────────────────────── + + asyncTest "persistEncoded round-trips a struct through SQLite": + let root = getTempDir() / ("persistency_enc_" & $epochTime().int) + removeDir(root) + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let job = p.openJob("t").get() + + let m = Msg( + header: Header(sender: "alice", epoch: 1'i64), + mood: moodHappy, + body: @[1'u8, 2, 3], + ) + let k = key("channel-42", m.header.epoch) + await job.persistEncoded("msg", k, m) + + # Poll for the row, then read it back as raw bytes. + let deadline = epochTime() + 1.0 + var got: Option[seq[byte]] + while epochTime() < deadline: + let r = await job.get("msg", k) + check r.isOk + got = r.get() + if got.isSome: + break + await sleepAsync(chronos.milliseconds(2)) + check got.isSome + check got.get == toPayload(m) diff --git a/tests/persistency/test_facade.nim b/tests/persistency/test_facade.nim new file mode 100644 index 000000000..5b5f9eac1 --- /dev/null +++ b/tests/persistency/test_facade.nim @@ -0,0 +1,196 @@ +{.used.} + +import std/[options, os, strutils, times] +import chronos, results +import testutils/unittests +import waku/persistency/persistency + +proc payload(s: string): seq[byte] = + result = newSeq[byte](s.len) + for i, c in s: + result[i] = byte(c) + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +proc tmpRoot(label: string): string = + let p = getTempDir() / ("persistency_facade_" & label & "_" & $epochTime().int) + removeDir(p) + p + +# Bounded poll on exists() to bridge the documented persist->read race. +proc waitUntilExists( + t: Job, category: string, k: Key, timeoutMs = 1000 +): Future[bool] {.async.} = + let deadline = epochTime() + (timeoutMs.float / 1000.0) + while epochTime() < deadline: + let r = await t.exists(category, k) + if r.isOk and r.get(): + return true + await sleepAsync(chronos.milliseconds(2)) + return false + +suite "Persistency facade": + asyncTest "persistPut then get round-trips": + let root = tmpRoot("put_get") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + let k = key("c", 1'i64) + await t.persistPut("msg", k, payload("hi")) + let ckOk1 = await t.waitUntilExists("msg", k) + check ckOk1 + + let aw1 = await t.get("msg", k) + let got = aw1.get() + check got.isSome + check str(got.get) == "hi" + + asyncTest "persist (batch) is atomic and visible together": + let root = tmpRoot("batch") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + var ops: seq[TxOp] + for i in 1'i64 .. 4: + ops.add( + TxOp(category: "msg", key: key("c", i), kind: txPut, payload: payload($i)) + ) + await t.persist(ops) + let ckOk2 = await t.waitUntilExists("msg", key("c", 4'i64)) + check ckOk2 + + let aw2 = await t.count("msg", prefixRange(key("c"))) + let cnt = aw2.get() + check cnt == 4 + + asyncTest "scanPrefix returns rows in key order": + let root = tmpRoot("scan") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + for i in [3'i64, 1, 4, 1, 5, 9, 2]: + await t.persistPut("msg", key("c", i), payload($i)) + let ckOk3 = await t.waitUntilExists("msg", key("c", 9'i64)) + check ckOk3 + + let aw3 = await t.scanPrefix("msg", key("c")) + let rows = aw3.get() + # 7 ops with duplicate key i=1 -> 6 distinct rows + check rows.len == 6 + + var seenOrder: seq[int] + for r in rows: + seenOrder.add(parseInt(str(r.payload))) + check seenOrder == @[1, 2, 3, 4, 5, 9] + + asyncTest "scanPrefix reverse=true returns rows in reverse order": + let root = tmpRoot("scan_rev") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + for i in 1'i64 .. 3: + await t.persistPut("msg", key("c", i), payload($i)) + let ckOk4 = await t.waitUntilExists("msg", key("c", 3'i64)) + check ckOk4 + + let aw4 = await t.scanPrefix("msg", key("c"), reverse = true) + let rows = aw4.get() + check rows.len == 3 + check str(rows[0].payload) == "3" + check str(rows[2].payload) == "1" + + asyncTest "deleteAcked round-trips and reports row presence": + let root = tmpRoot("delete") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + let k = key("c", 1'i64) + let aw5 = await t.deleteAcked("msg", k) + let miss = aw5.get() + check miss == false + + await t.persistPut("msg", k, payload("v")) + let ckOk5 = await t.waitUntilExists("msg", k) + check ckOk5 + + let aw6 = await t.deleteAcked("msg", k) + let hit = aw6.get() + check hit == true + let aw7 = await t.exists("msg", k) + check aw7.get() == false + + asyncTest "persistDelete fire-and-forget removes the row": + let root = tmpRoot("fadel") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + let k = key("c", 1'i64) + await t.persistPut("msg", k, payload("v")) + let ckOk6 = await t.waitUntilExists("msg", k) + check ckOk6 + await t.persistDelete("msg", k) + # Poll for absence. + let deadline = epochTime() + 1.0 + var gone = false + while epochTime() < deadline: + let aw8 = await t.exists("msg", k) + if not aw8.get(): + gone = true + break + await sleepAsync(chronos.milliseconds(2)) + check gone + + asyncTest "two jobs do not see each other's data via the facade": + let root = tmpRoot("iso") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let a = p.openJob("a").get() + let b = p.openJob("b").get() + + let k = key("c", 1'i64) + await a.persistPut("msg", k, payload("A")) + await b.persistPut("msg", k, payload("B")) + let ckOk7 = await a.waitUntilExists("msg", k) + check ckOk7 + let ckOk8 = await b.waitUntilExists("msg", k) + check ckOk8 + + let aw9 = await a.get("msg", k) + check str(aw9.get().get) == "A" + let aw10 = await b.get("msg", k) + check str(aw10.get().get) == "B" + let aw11 = await a.count("msg", prefixRange(key("c"))) + check aw11.get() == 1 + let aw12 = await b.count("msg", prefixRange(key("c"))) + check aw12.get() == 1 diff --git a/tests/persistency/test_keys.nim b/tests/persistency/test_keys.nim new file mode 100644 index 000000000..e33020849 --- /dev/null +++ b/tests/persistency/test_keys.nim @@ -0,0 +1,135 @@ +{.used.} + +import std/[algorithm, sequtils] +import testutils/unittests +import waku/persistency/[types, keys] + +proc cmpBytes(a, b: Key): int = + let ab = bytes(a) + let bb = bytes(b) + let n = min(ab.len, bb.len) + for i in 0 ..< n: + if ab[i] != bb[i]: + return cmp(ab[i], bb[i]) + cmp(ab.len, bb.len) + +suite "Persistency keys": + test "string components sort by length, then byte order": + var ks = @[key("ab"), key(""), key("a"), key("aa"), key("b")] + ks.sort(cmpBytes) + # length-prefix encoding => shorter strings always sort before longer + # ones; same-length strings sort in byte order. + check ks == @[key(""), key("a"), key("b"), key("aa"), key("ab")] + + test "same-length strings sort in byte order": + var ks = @[key("delta"), key("alpha"), key("gamma"), key("bravo")] + ks.sort(cmpBytes) + check ks == @[key("alpha"), key("bravo"), key("delta"), key("gamma")] + + test "int64 sign-flip preserves order across negative/zero/positive": + let inputs = @[ + key("c", int64.low), + key("c", -2'i64), + key("c", -1'i64), + key("c", 0'i64), + key("c", 1'i64), + key("c", 2'i64), + key("c", int64.high), + ] + var shuffled = inputs + # rotate so the natural order is not the input order + shuffled = @[ + shuffled[3], + shuffled[6], + shuffled[0], + shuffled[5], + shuffled[1], + shuffled[4], + shuffled[2], + ] + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "uint64 big-endian preserves order": + let inputs = @[ + key("u", 0'u64), + key("u", 1'u64), + key("u", 256'u64), + key("u", 1_000_000'u64), + key("u", uint64.high - 1), + key("u", uint64.high), + ] + var shuffled = @[inputs[3], inputs[0], inputs[5], inputs[2], inputs[1], inputs[4]] + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "composite (string, string) tuple ordering": + # First component "a" / "b" — both length 1, so byte order applies. + # Second components grouped by first; within each group, again + # length-then-byte: "" (len 0) < "a","z" (len 1) < "ab" (len 2). + let inputs = @[ + key("a", ""), + key("a", "a"), + key("a", "z"), + key("a", "ab"), + key("b", ""), + key("b", "a"), + ] + var shuffled = inputs.reversed() + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "composite (string, int64) tuple ordering": + let inputs = @[ + key("a", int64.low), + key("a", -1'i64), + key("a", 0'i64), + key("a", 1'i64), + key("b", int64.low), + key("b", 0'i64), + ] + var shuffled = inputs.reversed() + shuffled.sort(cmpBytes) + check shuffled == inputs + + test "shorter composite key precedes longer one sharing its prefix": + check key("a") < key("a", 0'i64) + check key("a") < key("a", "") + check key("a", "x") < key("a", "x", "y") + + test "Key equality is byte-wise": + check key("a", 1'i64) == key("a", 1'i64) + check not (key("a", 1'i64) == key("a", 2'i64)) + + test "prefixRange.start equals prefix": + let r = prefixRange(key("a")) + check r.start == key("a") + + test "prefixRange.stop excludes the prefix and admits all extensions": + let r = prefixRange(key("a")) + let extensions = @[ + key("a"), + key("a", 0'i64), + key("a", int64.high), + key("a", "x"), + key("a", uint64.high), + ] + for k in extensions: + check r.start <= k + check k < r.stop + + test "prefixRange.stop excludes siblings outside the prefix": + let r = prefixRange(key("a")) + # "b" has the same encoded length as "a" but a higher last byte, so it + # should be at-or-above the exclusive stop. + check not (key("b") < r.stop) + # "ab" has more bytes — its 2-byte length prefix bumps it past stop. + check not (key("ab") < r.stop) + # The empty key sits before the start. + check key("") < r.start + + test "prefixRange handles all-0xFF prefix as open-ended": + let prefix = rawKey(@[0xFF'u8, 0xFF, 0xFF]) + let r = prefixRange(prefix) + check r.start == prefix + check bytes(r.stop).len == 0 diff --git a/tests/persistency/test_lifecycle.nim b/tests/persistency/test_lifecycle.nim new file mode 100644 index 000000000..6b1a6ee60 --- /dev/null +++ b/tests/persistency/test_lifecycle.nim @@ -0,0 +1,302 @@ +{.used.} + +import std/[options, os, times] +import chronos, results +import testutils/unittests +import brokers/[event_broker, request_broker] +import waku/persistency/persistency +import waku/persistency/backend_comm + +proc payloadBytes(s: string): seq[byte] = + result = newSeq[byte](s.len) + for i, c in s: + result[i] = byte(c) + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +proc tmpRoot(label: string): string = + let p = getTempDir() / ("persistency_test_" & label & "_" & $epochTime().int) + removeDir(p) + p + +# Cross-thread persist: emit a PersistEvent then poll until the row shows up +# via KvExists. The PersistEvent listener is fire-and-forget, so reads +# immediately after emit are racy by design (documented in v1). +proc pollExists( + t: Job, category: string, k: Key, timeoutMs = 1000 +): Future[bool] {.async.} = + let deadline = epochTime() + (timeoutMs.float / 1000.0) + while epochTime() < deadline: + let r = await KvExists.request(t.context, category, k) + if r.isOk and r.get().value: + return true + await sleepAsync(chronos.milliseconds(2)) + return false + +suite "Persistency lifecycle": + test "Persistency.instance accepts a pre-existing rootDir": + let root = tmpRoot("preexisting") + defer: + removeDir(root) + createDir(root) # pretend a previous run left it + let marker = root / "do-not-touch.txt" + writeFile(marker, "hi") + defer: + removeFile(marker) + + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + # The pre-existing file is untouched. + check fileExists(marker) + check readFile(marker) == "hi" + + test "Persistency.instance refuses a non-directory path": + let root = tmpRoot("collision") + defer: + removeFile(root) + writeFile(root, "im a file not a dir") # collide with rootDir name + let r = Persistency.instance(root) + check r.isErr + check r.error.kind == peInvalidArgument + + test "Persistency.instance defers rootDir creation until first openJob": + let root = tmpRoot("lazy") + defer: + removeDir(root) + check not dirExists(root) + + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + # instance() must not have touched the filesystem + check not dirExists(root) + + discard p.openJob("first").get() + # first openJob materialises the directory + check dirExists(root) + + test "Persistency.instance refuses a path whose ancestor is not a directory": + let parent = tmpRoot("bad-parent") + defer: + removeFile(parent) + writeFile(parent, "not a directory") + let root = parent / "child" + let r = Persistency.instance(root) + check r.isErr + check r.error.kind == peInvalidArgument + + asyncTest "openJob reuses an existing DB file across processes-of-one": + let root = tmpRoot("reopen") + defer: + removeDir(root) + + # First "session": write something then close. + block firstSession: + let p = Persistency.instance(root).get() + let j = p.openJob("persist").get() + await j.persistPut("msg", key("c", 1'i64), payloadBytes("v1")) + let ckOk1 = await j.pollExists("msg", key("c", 1'i64)) + check ckOk1 + Persistency.reset() + + check fileExists(root / "persist.db") + + # Second "session": reopen and read the data back. + block secondSession: + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let j = p.openJob("persist").get() + let aw1 = await KvGet.request(j.context, "msg", key("c", 1'i64)) + let got = aw1.get() + check got.value.isSome + check str(got.value.get) == "v1" + + test "openJob is idempotent within a session": + let root = tmpRoot("idem") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let a = p.openJob("same").get() + let b = p.openJob("same").get() + check a.id == b.id + check a.context == b.context + + test "openJob materialises rootDir and launches a worker": + let root = tmpRoot("basic") + defer: + removeDir(root) + + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let t = p.openJob("alpha").get() + check t.id == "alpha" + check t.running + check fileExists(root / "alpha.db") + + asyncTest "persist then read round-trips via brokers": + let root = tmpRoot("rw") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t1").get() + + let k = key("c", 1'i64) + let ev = PersistEvent( + ops: @[TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("hello"))] + ) + await PersistEvent.emit(t.context, ev) + let ckOk2 = await t.pollExists("msg", k) + check ckOk2 + + let aw2 = await KvGet.request(t.context, "msg", k) + let got = aw2.get() + check got.value.isSome + check str(got.value.get) == "hello" + + asyncTest "two jobs run in parallel with isolated DBs": + let root = tmpRoot("isolation") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let a = p.openJob("alpha").get() + let b = p.openJob("beta").get() + check a.context != b.context + + let k = key("shared", 1'i64) + await PersistEvent.emit( + a.context, + PersistEvent( + ops: @[ + TxOp( + category: "msg", key: k, kind: txPut, payload: payloadBytes("from-alpha") + ) + ] + ), + ) + await PersistEvent.emit( + b.context, + PersistEvent( + ops: @[ + TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("from-beta")) + ] + ), + ) + let ckOk3 = await a.pollExists("msg", k) + check ckOk3 + let ckOk4 = await b.pollExists("msg", k) + check ckOk4 + + let aw3 = await KvGet.request(a.context, "msg", k) + let aGot = aw3.get() + let aw4 = await KvGet.request(b.context, "msg", k) + let bGot = aw4.get() + check str(aGot.value.get) == "from-alpha" + check str(bGot.value.get) == "from-beta" + + # Each job has its own DB file. + check fileExists(root / "alpha.db") + check fileExists(root / "beta.db") + + asyncTest "closeJob joins the worker and frees the slot": + let root = tmpRoot("close") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let t = p.openJob("x").get() + let ctx = t.context + p.closeJob("x") + check not t.running + + # After close, requests on the old context have no provider. + let r = await KvExists.request(ctx, "msg", key("k")) + check r.isErr + + test "dropJob removes the DB file": + let root = tmpRoot("drop") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + discard p.openJob("ephemeral").get() + check fileExists(root / "ephemeral.db") + p.dropJob("ephemeral") + check not fileExists(root / "ephemeral.db") + + asyncTest "scan and count over a range": + let root = tmpRoot("scan") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + var ops: seq[TxOp] + for i in 1'i64 .. 5: + ops.add( + TxOp(category: "msg", key: key("c", i), kind: txPut, payload: payloadBytes($i)) + ) + await PersistEvent.emit(t.context, PersistEvent(ops: ops)) + # Wait for the last insert to land. + let ckOk5 = await t.pollExists("msg", key("c", 5'i64)) + check ckOk5 + + let rng = prefixRange(key("c")) + let aw5 = await KvCount.request(t.context, "msg", rng) + let cnt = aw5.get() + check cnt.n == 5 + + let aw6 = await KvScan.request(t.context, "msg", rng, false) + let scn = aw6.get() + check scn.rows.len == 5 + check str(scn.rows[0].payload) == "1" + check str(scn.rows[4].payload) == "5" + + asyncTest "acked delete reports whether the row existed": + let root = tmpRoot("delete") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let t = p.openJob("t").get() + + let k = key("d", 1'i64) + let aw7 = await KvDelete.request(t.context, "msg", k) + let r1 = aw7.get() + check r1.existed == false + + await PersistEvent.emit( + t.context, + PersistEvent( + ops: @[TxOp(category: "msg", key: k, kind: txPut, payload: payloadBytes("v"))] + ), + ) + let ckOk6 = await t.pollExists("msg", k) + check ckOk6 + + let aw8 = await KvDelete.request(t.context, "msg", k) + let r2 = aw8.get() + check r2.existed == true + let aw9 = await KvExists.request(t.context, "msg", k) + let r3 = aw9.get() + check r3.value == false diff --git a/tests/persistency/test_singleton.nim b/tests/persistency/test_singleton.nim new file mode 100644 index 000000000..f17841611 --- /dev/null +++ b/tests/persistency/test_singleton.nim @@ -0,0 +1,79 @@ +{.used.} + +import std/[os, strutils, times] +import chronos, results +import testutils/unittests +import brokers/multi_request_broker +import waku/persistency/persistency + +proc tmpRoot(label: string): string = + let p = getTempDir() / ("persistency_singleton_" & label & "_" & $epochTime().int) + removeDir(p) + p + +suite "Persistency singleton": + test "instance(rootDir) is idempotent with the same rootDir": + let root = tmpRoot("idem") + defer: + removeDir(root) + defer: + Persistency.reset() + + let p1 = Persistency.instance(root).get() + let p2 = Persistency.instance(root).get() + check p1 == p2 + + test "instance(rootDir) refuses re-init with a different rootDir": + let rootA = tmpRoot("a") + let rootB = tmpRoot("b") + defer: + removeDir(rootA) + defer: + removeDir(rootB) + defer: + Persistency.reset() + + discard Persistency.instance(rootA).get() + let r = Persistency.instance(rootB) + check r.isErr + check r.error.kind == peInvalidArgument + + test "no-arg instance() fails before init, succeeds after": + let root = tmpRoot("noarg") + defer: + removeDir(root) + defer: + Persistency.reset() + + let before = Persistency.instance() + check before.isErr + check before.error.kind == peClosed + + discard Persistency.instance(root).get() + let after = Persistency.instance() + check after.isOk + + test "reset() makes the next instance() target a different rootDir": + let rootA = tmpRoot("rs-a") + let rootB = tmpRoot("rs-b") + defer: + removeDir(rootA) + defer: + removeDir(rootB) + defer: + Persistency.reset() + + let pA = Persistency.instance(rootA).get() + check pA.rootDir == rootA + Persistency.reset() + + let pB = Persistency.instance(rootB).get() + check pB.rootDir == rootB + check pA != pB + + test "reset() is idempotent": + defer: + Persistency.reset() + Persistency.reset() + Persistency.reset() + check Persistency.instance().isErr diff --git a/tests/persistency/test_string_lookup.nim b/tests/persistency/test_string_lookup.nim new file mode 100644 index 000000000..11ac5fed3 --- /dev/null +++ b/tests/persistency/test_string_lookup.nim @@ -0,0 +1,184 @@ +{.used.} + +import std/[options, os, times] +import chronos, results +import testutils/unittests +import waku/persistency/persistency + +proc payloadBytes(s: string): seq[byte] = + result = newSeq[byte](s.len) + for i, c in s: + result[i] = byte(c) + +template str(b: seq[byte]): string = + var s = newString(b.len) + for i, x in b: + s[i] = char(x) + s + +proc tmpRoot(label: string): string = + let p = getTempDir() / ("persistency_lookup_" & label & "_" & $epochTime().int) + removeDir(p) + p + +# Bridge the persist->read race (writes are fire-and-forget in v1). +proc waitUntilExists( + p: Persistency, jobId, category: string, k: Key, timeoutMs = 1000 +): Future[bool] {.async.} = + let deadline = epochTime() + (timeoutMs.float / 1000.0) + while epochTime() < deadline: + let r = await p.exists(jobId, category, k) + if r.isOk and r.get(): + return true + await sleepAsync(chronos.milliseconds(2)) + return false + +suite "Persistency string-id lookup": + test "job(p, id) returns peJobNotFound when not open": + let root = tmpRoot("notfound") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let r = p.job("nope") + check r.isErr + check r.error.kind == peJobNotFound + + test "job(p, id) returns the Job after openJob": + let root = tmpRoot("found") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let opened = p.openJob("alpha").get() + let looked = p.job("alpha").get() + check looked.id == "alpha" + check looked == opened # same ref, no need to peek at .context + + test "hasJob mirrors p.job()": + let root = tmpRoot("has") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + check not p.hasJob("x") + discard p.openJob("x") + check p.hasJob("x") + p.closeJob("x") + check not p.hasJob("x") + + test "subscript [] returns the open Job": + let root = tmpRoot("subscript") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + discard p.openJob("a").get() + let j = p["a"] + check j.id == "a" + + asyncTest "string-lookup persistPut + get round-trips without a Job ref": + let root = tmpRoot("rw") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + discard p.openJob("svc").get() + + let k = key("c", 1'i64) + await p.persistPut("svc", "msg", k, payloadBytes("hello")) + let ckOk1 = await p.waitUntilExists("svc", "msg", k) + check ckOk1 + + let aw1 = await p.get("svc", "msg", k) + let got = aw1.get() + check got.isSome + check str(got.get) == "hello" + + asyncTest "string-lookup reads short-circuit with peJobNotFound": + let root = tmpRoot("missingread") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + let g = await p.get("nope", "msg", key("k")) + check g.isErr + check g.error.kind == peJobNotFound + + let c = await p.count("nope", "msg", prefixRange(key("k"))) + check c.isErr + check c.error.kind == peJobNotFound + + let d = await p.deleteAcked("nope", "msg", key("k")) + check d.isErr + check d.error.kind == peJobNotFound + + asyncTest "string-lookup writes to an unknown job are dropped, not raised": + let root = tmpRoot("missingwrite") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + + # Should not raise and should not leak any state. + await p.persistPut("ghost", "msg", key("k"), payloadBytes("v")) + await p.persistDelete("ghost", "msg", key("k")) + await p.persistEncoded("ghost", "msg", key("k"), 42'i64) + check not p.hasJob("ghost") + + asyncTest "string-lookup persistEncoded round-trips a struct": + let root = tmpRoot("encoded") + defer: + removeDir(root) + type Item = object + tag: string + n: int64 + + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + discard p.openJob("e").get() + + let k = key("items", 1'i64) + await p.persistEncoded("e", "msg", k, Item(tag: "alpha", n: 7)) + let ckOk2 = await p.waitUntilExists("e", "msg", k) + check ckOk2 + + let aw2 = await p.get("e", "msg", k) + let got = aw2.get() + check got.isSome + check got.get == toPayload(Item(tag: "alpha", n: 7)) + + asyncTest "string-lookup scan returns the same rows as Job-form": + let root = tmpRoot("scan") + defer: + removeDir(root) + let p = Persistency.instance(root).get() + defer: + Persistency.reset() + let j = p.openJob("s").get() + + for i in 1'i64 .. 3: + await p.persistPut("s", "msg", key("c", i), payloadBytes($i)) + let ckOk3 = await p.waitUntilExists("s", "msg", key("c", 3'i64)) + check ckOk3 + + let aw3 = await p.scanPrefix("s", "msg", key("c")) + let viaId = aw3.get() + let aw4 = await j.scanPrefix("msg", key("c")) + let viaRef = aw4.get() + check viaId.len == viaRef.len + for i in 0 ..< viaId.len: + check viaId[i].key == viaRef[i].key + check viaId[i].payload == viaRef[i].payload diff --git a/tests/resources/payloads.nim b/tests/resources/payloads.nim index 723bf788c..0fc1eaebb 100644 --- a/tests/resources/payloads.nim +++ b/tests/resources/payloads.nim @@ -8,8 +8,7 @@ const EMOJI* = "😀 😃 😄 😁 😆 😅 🤣 😂 🙂 🙃 😉 😊 😇 🥰 😍 🤩 😘 😗 😚 😙" CODE* = "def main():\n\tprint('Hello, world!')" - QUERY* = - """ + QUERY* = """ SELECT u.id, u.name, @@ -30,8 +29,7 @@ const u.id = 1 """ TEXT_SMALL* = "Lorem ipsum dolor sit amet, consectetur adipiscing elit." - TEXT_LARGE* = - """ + TEXT_LARGE* = """ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras gravida vulputate semper. Proin eleifend varius cursus. Morbi lacinia posuere quam sit amet pretium. Sed non metus fermentum, venenatis nisl id, vestibulum eros. Quisque non lorem sit amet lectus faucibus elementum eu diff --git a/tests/test_message_cache.nim b/tests/test_message_cache.nim index cd2e882c1..95904f8f2 100644 --- a/tests/test_message_cache.nim +++ b/tests/test_message_cache.nim @@ -1,7 +1,7 @@ {.used.} import std/[sets, random], results, stew/byteutils, testutils/unittests -import waku/waku_core, waku/waku_api/message_cache, ./testlib/wakucore +import waku/waku_core, waku/rest_api/message_cache, ./testlib/wakucore randomize() diff --git a/tests/test_peer_manager.nim b/tests/test_peer_manager.nim index 1369f3f88..608889d32 100644 --- a/tests/test_peer_manager.nim +++ b/tests/test_peer_manager.nim @@ -54,6 +54,44 @@ procSuite "Peer Manager": nodes[0].peerManager.switch.peerStore.connectedness(nodes[1].peerInfo.peerId) == Connectedness.Connected + asyncTest "Peer manager tracks active store request state": + let nodes = toSeq(0 ..< 2).mapIt( + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + ) + + await allFutures(nodes.mapIt(it.start())) + await allFutures(nodes.mapIt(it.mountRelay())) + + let peerId = nodes[1].peerInfo.peerId + require ( + await nodes[0].peerManager.connectPeer(nodes[1].peerInfo.toRemotePeerInfo()) + ) + await sleepAsync(chronos.milliseconds(500)) + + nodes[0].peerManager.addActiveStoreRequest(peerId) + check: + nodes[0].peerManager.hasActiveStoreRequest(peerId) + + await nodes[0].peerManager.evictPeer(peerId) + await sleepAsync(chronos.milliseconds(100)) + + check: + nodes[0].peerManager.switch.peerStore.connectedness(peerId) == + Connectedness.Connected + + nodes[0].peerManager.removeActiveStoreRequest(peerId) + check: + not nodes[0].peerManager.hasActiveStoreRequest(peerId) + + await nodes[0].peerManager.evictPeer(peerId) + await sleepAsync(chronos.milliseconds(100)) + + check: + nodes[0].peerManager.switch.peerStore.connectedness(peerId) != + Connectedness.Connected + + await allFutures(nodes.mapIt(it.stop())) + asyncTest "dialPeer() works": # Create 2 nodes let nodes = toSeq(0 ..< 2).mapIt( @@ -997,6 +1035,7 @@ procSuite "Peer Manager": .build(), maxFailedAttempts = 1, storage = nil, + maxConnections = 20, ) # Create 30 peers and add them to the peerstore @@ -1063,6 +1102,7 @@ procSuite "Peer Manager": backoffFactor = 2, maxFailedAttempts = 10, storage = nil, + maxConnections = 20, ) var p1: PeerId require p1.init("QmeuZJbXrszW2jdT7GdduSjQskPU3S7vvGWKtKgDfkDvW" & "1") @@ -1116,6 +1156,7 @@ procSuite "Peer Manager": .build(), maxFailedAttempts = 150, storage = nil, + maxConnections = 20, ) # Should result in backoff > 1 week @@ -1131,6 +1172,7 @@ procSuite "Peer Manager": .build(), maxFailedAttempts = 10, storage = nil, + maxConnections = 20, ) let pm = PeerManager.new( @@ -1144,6 +1186,7 @@ procSuite "Peer Manager": .build(), maxFailedAttempts = 5, storage = nil, + maxConnections = 20, ) asyncTest "colocationLimit is enforced by pruneConnsByIp()": @@ -1202,3 +1245,231 @@ procSuite "Peer Manager": r = node1.peerManager.selectPeer(WakuPeerExchangeCodec) assert r.isSome(), "could not retrieve peer mounting WakuPeerExchangeCodec" + + asyncTest "selectPeer() filters peers by shard using ENR": + ## Given: A peer manager with 3 peers having different shards in their ENRs + let + clusterId = 0.uint16 + shardId0 = 0.uint16 + shardId1 = 1.uint16 + + # Create 3 nodes with different shards + let nodes = @[ + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ), + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId1], + ), + newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ), + ] + + await allFutures(nodes.mapIt(it.start())) + for node in nodes: + discard await node.mountRelay() + + # Get peer infos with ENRs + let peerInfos = collect: + for node in nodes: + var peerInfo = node.switch.peerInfo.toRemotePeerInfo() + peerInfo.enr = some(node.enr) + peerInfo + + # Add all peers to node 0's peer manager and peerstore + for i in 1 .. 2: + nodes[0].peerManager.addPeer(peerInfos[i]) + nodes[0].peerManager.switch.peerStore[AddressBook][peerInfos[i].peerId] = + peerInfos[i].addrs + nodes[0].peerManager.switch.peerStore[ProtoBook][peerInfos[i].peerId] = + @[WakuRelayCodec] + + ## When: We select a peer for shard 0 + let shard0Topic = some(PubsubTopic("/waku/2/rs/0/0")) + let selectedPeer0 = nodes[0].peerManager.selectPeer(WakuRelayCodec, shard0Topic) + + ## Then: Only peers supporting shard 0 are considered (nodes 2, not node 1) + check: + selectedPeer0.isSome() + selectedPeer0.get().peerId != peerInfos[1].peerId # node1 has shard 1 + selectedPeer0.get().peerId == peerInfos[2].peerId # node2 has shard 0 + + ## When: We select a peer for shard 1 + let shard1Topic = some(PubsubTopic("/waku/2/rs/0/1")) + let selectedPeer1 = nodes[0].peerManager.selectPeer(WakuRelayCodec, shard1Topic) + + ## Then: Only peer with shard 1 is selected + check: + selectedPeer1.isSome() + selectedPeer1.get().peerId == peerInfos[1].peerId # node1 has shard 1 + + await allFutures(nodes.mapIt(it.stop())) + + asyncTest "selectPeer() filters peers by shard using shards field": + ## Given: A peer manager with peers having shards in RemotePeerInfo (no ENR) + let + clusterId = 0.uint16 + shardId0 = 0.uint16 + shardId1 = 1.uint16 + + # Create peer manager + let pm = PeerManager.new( + switch = SwitchBuilder.new().withRng(rng()).withMplex().withNoise().build(), + storage = nil, + ) + + # Create peer infos with shards field populated (simulating metadata exchange) + let basePeerId = "16Uiu2HAm7QGEZKujdSbbo1aaQyfDPQ6Bw3ybQnj6fruH5Dxwd7D" + let peers = toSeq(1 .. 3) + .mapIt(parsePeerInfo("/ip4/0.0.0.0/tcp/30300/p2p/" & basePeerId & $it)) + .filterIt(it.isOk()) + .mapIt(it.value) + require: + peers.len == 3 + + # Manually populate the shards field (ENR is not available) + var peerInfos: seq[RemotePeerInfo] = @[] + for i, peer in peers: + var peerInfo = RemotePeerInfo.init(peer.peerId, peer.addrs) + # Peer 0 and 2 have shard 0, peer 1 has shard 1 + peerInfo.shards = + if i == 1: + @[shardId1] + else: + @[shardId0] + # Note: ENR is intentionally left as none + peerInfos.add(peerInfo) + + # Add peers to peerstore + for peerInfo in peerInfos: + pm.switch.peerStore[AddressBook][peerInfo.peerId] = peerInfo.addrs + pm.switch.peerStore[ProtoBook][peerInfo.peerId] = @[WakuRelayCodec] + # simulate metadata exchange by setting shards field in peerstore + pm.switch.peerStore.setShardInfo(peerInfo.peerId, peerInfo.shards) + + ## When: We select a peer for shard 0 + let shard0Topic = some(PubsubTopic("/waku/2/rs/0/0")) + let selectedPeer0 = pm.selectPeer(WakuRelayCodec, shard0Topic) + + ## Then: Peers with shard 0 in shards field are selected + check: + selectedPeer0.isSome() + selectedPeer0.get().peerId in [peerInfos[0].peerId, peerInfos[2].peerId] + + ## When: We select a peer for shard 1 + let shard1Topic = some(PubsubTopic("/waku/2/rs/0/1")) + let selectedPeer1 = pm.selectPeer(WakuRelayCodec, shard1Topic) + + ## Then: Peer with shard 1 in shards field is selected + check: + selectedPeer1.isSome() + selectedPeer1.get().peerId == peerInfos[1].peerId + + asyncTest "selectPeer() handles invalid pubsub topic gracefully": + ## Given: A peer manager with valid peers + let node = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = 0, + subscribeShards = @[0'u16], + ) + await node.start() + + # Add a peer + let peer = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + await peer.start() + discard await peer.mountRelay() + + var peerInfo = peer.switch.peerInfo.toRemotePeerInfo() + peerInfo.enr = some(peer.enr) + node.peerManager.addPeer(peerInfo) + node.peerManager.switch.peerStore[ProtoBook][peerInfo.peerId] = @[WakuRelayCodec] + + ## When: selectPeer is called with malformed pubsub topic + let invalidTopics = @[ + some(PubsubTopic("invalid-topic")), + some(PubsubTopic("/waku/2/invalid")), + some(PubsubTopic("/waku/2/rs/abc/0")), # non-numeric cluster + some(PubsubTopic("")), # empty topic + ] + + ## Then: Returns none(RemotePeerInfo) without crashing + for invalidTopic in invalidTopics: + let result = node.peerManager.selectPeer(WakuRelayCodec, invalidTopic) + check: + result.isNone() + + await allFutures(node.stop(), peer.stop()) + + asyncTest "selectPeer() prioritizes ENR over shards field": + ## Given: A peer with both ENR and shards field populated + let + clusterId = 0.uint16 + shardId0 = 0.uint16 + shardId1 = 1.uint16 + + let node = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ) + await node.start() + discard await node.mountRelay() + + # Create peer with ENR containing shard 0 + let peer = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[shardId0], + ) + await peer.start() + discard await peer.mountRelay() + + # Create peer info with ENR (shard 0) but set shards field to shard 1 + var peerInfo = peer.switch.peerInfo.toRemotePeerInfo() + peerInfo.enr = some(peer.enr) # ENR has shard 0 + peerInfo.shards = @[shardId1] # shards field has shard 1 + + node.peerManager.addPeer(peerInfo) + node.peerManager.switch.peerStore[ProtoBook][peerInfo.peerId] = @[WakuRelayCodec] + # simulate metadata exchange by setting shards field in peerstore + node.peerManager.switch.peerStore.setShardInfo(peerInfo.peerId, peerInfo.shards) + + ## When: We select for shard 0 + let shard0Topic = some(PubsubTopic("/waku/2/rs/0/0")) + let selectedPeer = node.peerManager.selectPeer(WakuRelayCodec, shard0Topic) + + ## Then: Peer is selected because ENR (shard 0) takes precedence + check: + selectedPeer.isSome() + selectedPeer.get().peerId == peerInfo.peerId + + ## When: We select for shard 1 + let shard1Topic = some(PubsubTopic("/waku/2/rs/0/1")) + let selectedPeer1 = node.peerManager.selectPeer(WakuRelayCodec, shard1Topic) + + ## Then: Peer is still selected because shards field is checked as fallback + check: + selectedPeer1.isSome() + selectedPeer1.get().peerId == peerInfo.peerId + + await allFutures(node.stop(), peer.stop()) diff --git a/tests/test_waku.nim b/tests/test_waku.nim index b8e2b26b1..cf5675716 100644 --- a/tests/test_waku.nim +++ b/tests/test_waku.nim @@ -3,49 +3,47 @@ import chronos, testutils/unittests, std/options import waku +import tools/confutils/cli_args suite "Waku API - Create node": asyncTest "Create node with minimal configuration": ## Given - let nodeConfig = NodeConfig.init( - protocolsConfig = ProtocolsConfig.init(entryNodes = @[], clusterId = 1) - ) + var nodeConf = defaultWakuNodeConf().valueOr: + raiseAssert error + nodeConf.mode = Core + nodeConf.clusterId = 3'u16 + nodeConf.rest = false # This is the actual minimal config but as the node auto-start, it is not suitable for tests - # NodeConfig.init(ethRpcEndpoints = @["http://someaddress"]) ## When - let node = (await createNode(nodeConfig)).valueOr: + let node = (await createNode(nodeConf)).valueOr: raiseAssert error ## Then check: not node.isNil() - node.conf.clusterId == 1 + node.conf.clusterId == 3 node.conf.relay == true asyncTest "Create node with full configuration": ## Given - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = - @[ - "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g" - ], - staticStoreNodes = - @[ - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" - ], - clusterId = 99, - autoShardingConfig = AutoShardingConfig(numShardsInCluster: 16), - messageValidation = - MessageValidation(maxMessageSize: "1024 KiB", rlnConfig: none(RlnConfig)), - ), - ) + var nodeConf = defaultWakuNodeConf().valueOr: + raiseAssert error + nodeConf.mode = Core + nodeConf.clusterId = 99'u16 + nodeConf.rest = false + nodeConf.numShardsInNetwork = 16 + nodeConf.maxMessageSize = "1024 KiB" + nodeConf.entryNodes = @[ + "enr:-QESuEC1p_s3xJzAC_XlOuuNrhVUETmfhbm1wxRGis0f7DlqGSw2FM-p2Vn7gmfkTTnAe8Ys2cgGBN8ufJnvzKQFZqFMBgmlkgnY0iXNlY3AyNTZrMaEDS8-D878DrdbNwcuY-3p1qdDp5MOoCurhdsNPJTXZ3c5g3RjcIJ2X4N1ZHCCd2g" + ] + nodeConf.staticnodes = @[ + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc" + ] ## When - let node = (await createNode(nodeConfig)).valueOr: + let node = (await createNode(nodeConf)).valueOr: raiseAssert error ## Then @@ -62,20 +60,18 @@ suite "Waku API - Create node": asyncTest "Create node with mixed entry nodes (enrtree, multiaddr)": ## Given - let nodeConfig = NodeConfig.init( - mode = Core, - protocolsConfig = ProtocolsConfig.init( - entryNodes = - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", - "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", - ], - clusterId = 42, - ), - ) + var nodeConf = defaultWakuNodeConf().valueOr: + raiseAssert error + nodeConf.mode = Core + nodeConf.clusterId = 42'u16 + nodeConf.rest = false + nodeConf.entryNodes = @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im", + "/ip4/127.0.0.1/tcp/60000/p2p/16Uuu2HBmAcHvhLqQKwSSbX6BG5JLWUDRcaLVrehUVqpw7fz1hbYc", + ] ## When - let node = (await createNode(nodeConfig)).valueOr: + let node = (await createNode(nodeConf)).valueOr: raiseAssert error ## Then diff --git a/tests/test_waku_enr.nim b/tests/test_waku_enr.nim index 2ffff5e57..10183adf5 100644 --- a/tests/test_waku_enr.nim +++ b/tests/test_waku_enr.nim @@ -271,6 +271,44 @@ suite "Waku ENR - Multiaddresses": multiaddrs.contains(expectedAddr1) multiaddrs.contains(addr2) + test "encode and decode record with multiaddrs field deduplicates duplicate entries": + ## Given + let + enrSeqNum = 1u64 + enrPrivKey = generatesecp256k1key() + + let + addr1 = MultiAddress + .init( + "/ip4/127.0.0.1/tcp/80/ws/p2p/16Uiu2HAm4v86W3bmT1BiH6oSPzcsSr31iDQpSN5Qa882BCjjwgrD" + ) + .get() + addr1NoPeerId = MultiAddress.init("/ip4/127.0.0.1/tcp/80/ws").get() + addr2 = MultiAddress.init("/ip4/127.0.0.1/tcp/443/wss").get() + + ## When + var builder = EnrBuilder.init(enrPrivKey, seqNum = enrSeqNum) + builder.withMultiaddrs(@[addr1, addr1NoPeerId, addr2, addr2]) + + let recordRes = builder.build() + + require recordRes.isOk() + let record = recordRes.tryGet() + + let typedRecord = record.toTyped() + require typedRecord.isOk() + + let multiaddrsOpt = typedRecord.value.multiaddrs + + ## Then + check multiaddrsOpt.isSome() + + let multiaddrs = multiaddrsOpt.get() + check: + multiaddrs.len == 2 + multiaddrs.contains(addr1NoPeerId) + multiaddrs.contains(addr2) + suite "Waku ENR - Relay static sharding": test "new relay shards object with single invalid shard id": ## Given diff --git a/tests/test_waku_keepalive.nim b/tests/test_waku_keepalive.nim index c12f20a05..5d8402268 100644 --- a/tests/test_waku_keepalive.nim +++ b/tests/test_waku_keepalive.nim @@ -44,8 +44,7 @@ suite "Waku Keepalive": await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) - let healthMonitor = NodeHealthMonitor() - healthMonitor.setNodeToHealthMonitor(node1) + let healthMonitor = NodeHealthMonitor.new(node1) healthMonitor.startKeepalive(2.seconds).isOkOr: assert false, "Failed to start keepalive" diff --git a/tests/test_waku_keystore_keyfile.nim b/tests/test_waku_keystore_keyfile.nim index 5f4c74591..afdb7e44b 100644 --- a/tests/test_waku_keystore_keyfile.nim +++ b/tests/test_waku_keystore_keyfile.nim @@ -307,30 +307,29 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)": # but the last byte of mac is changed to 00. # While ciphertext is the correct encryption of priv under password, # mac verfication should fail and nothing will be decrypted - let keyfileWrongMac = - %*{ - "keyfile": { - "crypto": { - "cipher": "aes-128-ctr", - "cipherparams": {"iv": "6087dab2f9fdbbfaddc31a909735c1e6"}, - "ciphertext": - "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", - "kdf": "pbkdf2", - "kdfparams": { - "c": 262144, - "dklen": 32, - "prf": "hmac-sha256", - "salt": "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd", - }, - "mac": "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e900", + let keyfileWrongMac = %*{ + "keyfile": { + "crypto": { + "cipher": "aes-128-ctr", + "cipherparams": {"iv": "6087dab2f9fdbbfaddc31a909735c1e6"}, + "ciphertext": + "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", + "kdf": "pbkdf2", + "kdfparams": { + "c": 262144, + "dklen": 32, + "prf": "hmac-sha256", + "salt": "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd", }, - "id": "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version": 3, + "mac": "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e900", }, - "name": "test1", - "password": "testpassword", - "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d", - } + "id": "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version": 3, + }, + "name": "test1", + "password": "testpassword", + "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d", + } # Decryption with correct password let expectedSecret = decodeHex(keyfileWrongMac.getOrDefault("priv").getStr()) diff --git a/tests/test_waku_metadata.nim b/tests/test_waku_metadata.nim index b30fd1712..cfceb89b5 100644 --- a/tests/test_waku_metadata.nim +++ b/tests/test_waku_metadata.nim @@ -13,14 +13,15 @@ import eth/keys, eth/p2p/discoveryv5/enr import - waku/ - [ - waku_node, - waku_core/topics, - node/peer_manager, - discovery/waku_discv5, - waku_metadata, - ], + waku/[ + waku_node, + waku_core/topics, + waku_core, + node/peer_manager, + discovery/waku_discv5, + waku_metadata, + waku_relay/protocol, + ], ./testlib/wakucore, ./testlib/wakunode @@ -41,26 +42,86 @@ procSuite "Waku Metadata Protocol": clusterId = clusterId, ) + # Mount metadata protocol on both nodes before starting + discard node1.mountMetadata(clusterId, @[]) + discard node2.mountMetadata(clusterId, @[]) + + # Mount relay so metadata can track subscriptions + discard await node1.mountRelay() + discard await node2.mountRelay() + # Start nodes await allFutures([node1.start(), node2.start()]) - node1.topicSubscriptionQueue.emit((kind: PubsubSub, topic: "/waku/2/rs/10/7")) - node1.topicSubscriptionQueue.emit((kind: PubsubSub, topic: "/waku/2/rs/10/6")) + # Subscribe to topics on node1 - relay will track these and metadata will report them + let noOpHandler: WakuRelayHandler = proc( + pubsubTopic: PubsubTopic, message: WakuMessage + ): Future[void] {.async.} = + discard + + node1.wakuRelay.subscribe("/waku/2/rs/10/7", noOpHandler) + node1.wakuRelay.subscribe("/waku/2/rs/10/6", noOpHandler) # Create connection let connOpt = await node2.peerManager.dialPeer( node1.switch.peerInfo.toRemotePeerInfo(), WakuMetadataCodec ) require: - connOpt.isSome + connOpt.isSome() # Request metadata let response1 = await node2.wakuMetadata.request(connOpt.get()) # Check the response or dont even continue require: - response1.isOk + response1.isOk() check: response1.get().clusterId.get() == clusterId response1.get().shards == @[uint32(6), uint32(7)] + + await allFutures([node1.stop(), node2.stop()]) + + asyncTest "Metadata reports configured shards before relay subscription": + ## Given: Node with configured shards but no relay subscriptions yet + let + clusterId = 10.uint16 + configuredShards = @[uint16(0), uint16(1)] + + let node1 = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = configuredShards, + ) + let node2 = newTestWakuNode( + generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0), clusterId = clusterId + ) + + # Mount metadata with configured shards on node1 + discard node1.mountMetadata(clusterId, configuredShards) + # Mount metadata on node2 so it can make requests + discard node2.mountMetadata(clusterId, @[]) + + # Start nodes (relay is NOT mounted yet on node1) + await allFutures([node1.start(), node2.start()]) + + ## When: Node2 requests metadata from Node1 before relay is active + let connOpt = await node2.peerManager.dialPeer( + node1.switch.peerInfo.toRemotePeerInfo(), WakuMetadataCodec + ) + require: + connOpt.isSome + + let response = await node2.wakuMetadata.request(connOpt.get()) + + ## Then: Response contains configured shards even without relay subscriptions + require: + response.isOk() + + check: + response.get().clusterId.get() == clusterId + response.get().shards == @[uint32(0), uint32(1)] + + await allFutures([node1.stop(), node2.stop()]) diff --git a/tests/test_waku_netconfig.nim b/tests/test_waku_netconfig.nim index 5f9ff4b46..0aff64121 100644 --- a/tests/test_waku_netconfig.nim +++ b/tests/test_waku_netconfig.nim @@ -5,7 +5,7 @@ import chronos, confutils/toml/std/net, libp2p/multiaddress, testutils/unittests import ./testlib/wakunode, waku/waku_enr/capabilities include - waku/node/net_config, + waku/net/net_config, waku/factory/conf_builder/web_socket_conf_builder, waku/factory/conf_builder/conf_builder @@ -152,6 +152,31 @@ suite "Waku NetConfig": netConfig.announcedAddresses.len == 1 # DNS address netConfig.announcedAddresses[0] == dns4TcpEndPoint(dns4DomainName, extPort) + asyncTest "AnnouncedAddresses and enrMultiaddrs deduplicate dns4DomainName and extMultiAddrs overlap": + let + conf = defaultTestWakuConf() + dns4DomainName = "example.com" + extPort = Port(1234) + dns4Address = dns4TcpEndPoint(dns4DomainName, extPort) + + let netConfigRes = NetConfig.init( + bindIp = conf.endpointConf.p2pListenAddress, + bindPort = conf.endpointConf.p2pTcpPort, + dns4DomainName = some(dns4DomainName), + extPort = some(extPort), + extMultiAddrs = @[dns4Address], + ) + + assert netConfigRes.isOk(), $netConfigRes.error + + let netConfig = netConfigRes.get() + + check: + netConfig.announcedAddresses.len == 1 + netConfig.announcedAddresses[0] == dns4Address + netConfig.enrMultiAddrs.len == 1 + netConfig.enrMultiAddrs[0] == dns4Address + asyncTest "AnnouncedAddresses includes WebSocket addresses when enabled": var confBuilder = defaultTestWakuConfBuilder() diff --git a/tests/test_waku_noise.nim b/tests/test_waku_noise.nim index 980e752f5..6566f9eed 100644 --- a/tests/test_waku_noise.nim +++ b/tests/test_waku_noise.nim @@ -669,11 +669,10 @@ procSuite "Waku Noise": # <- s # ... # So we define accordingly the sequence of the pre-message public keys - let preMessagePKs: seq[NoisePublicKey] = - @[ - toNoisePublicKey(getPublicKey(aliceStaticKey)), - toNoisePublicKey(getPublicKey(bobStaticKey)), - ] + let preMessagePKs: seq[NoisePublicKey] = @[ + toNoisePublicKey(getPublicKey(aliceStaticKey)), + toNoisePublicKey(getPublicKey(bobStaticKey)), + ] var aliceHS = initialize( hsPattern = hsPattern, diff --git a/tests/test_waku_rendezvous.nim b/tests/test_waku_rendezvous.nim index fa2efbd47..88845dc25 100644 --- a/tests/test_waku_rendezvous.nim +++ b/tests/test_waku_rendezvous.nim @@ -1,12 +1,21 @@ {.used.} -import std/options, chronos, testutils/unittests, libp2p/builders +import + std/options, + chronos, + testutils/unittests, + libp2p/builders, + libp2p/protocols/rendezvous import waku/waku_core/peers, + waku/waku_core/codecs, + waku/waku_core, waku/node/waku_node, waku/node/peer_manager/peer_manager, waku/waku_rendezvous/protocol, + waku/waku_rendezvous/common, + waku/waku_rendezvous/waku_peer_record, ./testlib/[wakucore, wakunode] procSuite "Waku Rendezvous": @@ -50,18 +59,87 @@ procSuite "Waku Rendezvous": node2.peerManager.addPeer(peerInfo3) node3.peerManager.addPeer(peerInfo2) - let namespace = "test/name/space" - - let res = await node1.wakuRendezvous.batchAdvertise( - namespace, 60.seconds, @[peerInfo2.peerId] - ) + let res = await node1.wakuRendezvous.advertiseAll() assert res.isOk(), $res.error + # Rendezvous Request API requires dialing first + let connOpt = + await node3.peerManager.dialPeer(peerInfo2.peerId, WakuRendezVousCodec) + require: + connOpt.isSome - let response = - await node3.wakuRendezvous.batchRequest(namespace, 1, @[peerInfo2.peerId]) - assert response.isOk(), $response.error - let records = response.get() + var records: seq[WakuPeerRecord] + try: + records = await rendezvous.request[WakuPeerRecord]( + node3.wakuRendezvous, + Opt.some(computeMixNamespace(clusterId)), + Opt.some(1), + Opt.some(@[peerInfo2.peerId]), + ) + except CatchableError as e: + assert false, "Request failed with exception: " & e.msg check: records.len == 1 records[0].peerId == peerInfo1.peerId + #records[0].mixPubKey == $node1.wakuMix.pubKey + + asyncTest "Rendezvous advertises configured shards before relay is active": + ## Given: A node with configured shards but no relay subscriptions yet + let + clusterId = 10.uint16 + configuredShards = @[RelayShard(clusterId: clusterId, shardId: 0)] + + let node = newTestWakuNode( + generateSecp256k1Key(), + parseIpAddress("0.0.0.0"), + Port(0), + clusterId = clusterId, + subscribeShards = @[0'u16], + ) + + ## When: Node mounts rendezvous with configured shards (before relay) + await node.mountRendezvous(clusterId, configuredShards) + await node.start() + + ## Then: The rendezvous protocol should be mounted successfully + check: + node.wakuRendezvous != nil + + # Verify that the protocol is running without errors + # (shards are used internally by the getShardsGetter closure) + let namespace = computeMixNamespace(clusterId) + check: + namespace.len > 0 + + await node.stop() + + asyncTest "Rendezvous uses configured shards when relay not mounted": + ## Given: A light client node with no relay protocol + let + clusterId = 10.uint16 + configuredShards = @[ + RelayShard(clusterId: clusterId, shardId: 0), + RelayShard(clusterId: clusterId, shardId: 1), + ] + + let lightClient = newTestWakuNode( + generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0), clusterId = clusterId + ) + + ## When: Node mounts rendezvous with configured shards (no relay mounted) + await lightClient.mountRendezvous(clusterId, configuredShards) + await lightClient.start() + + ## Then: Rendezvous should be mounted successfully without relay + check: + lightClient.wakuRendezvous != nil + lightClient.wakuRelay == nil # Verify relay is not mounted + + # Verify the protocol is working (doesn't fail immediately) + # advertiseAll requires peers,so we just check the protocol is initialized + await sleepAsync(100.milliseconds) + + check: + lightClient.wakuRendezvous != nil + + await lightClient.stop() diff --git a/tests/test_waku_switch.nim b/tests/test_waku_switch.nim index 3e6fd08eb..9f11a41a1 100644 --- a/tests/test_waku_switch.nim +++ b/tests/test_waku_switch.nim @@ -12,14 +12,14 @@ import waku/node/waku_switch, ./testlib/common, ./testlib/wakucore proc newCircuitRelayClientSwitch(relayClient: RelayClient): Switch = SwitchBuilder - .new() - .withRng(rng()) - .withAddresses(@[MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet()]) - .withTcpTransport() - .withMplex() - .withNoise() - .withCircuitRelay(relayClient) - .build() + .new() + .withRng(rng()) + .withAddresses(@[MultiAddress.init("/ip4/0.0.0.0/tcp/0").tryGet()]) + .withTcpTransport() + .withMplex() + .withNoise() + .withCircuitRelay(relayClient) + .build() suite "Waku Switch": asyncTest "Waku Switch works with AutoNat": diff --git a/tests/testlib/futures.nim b/tests/testlib/futures.nim index e9a793388..dad1baec8 100644 --- a/tests/testlib/futures.nim +++ b/tests/testlib/futures.nim @@ -1,6 +1,6 @@ import chronos -import waku/[waku_core/message, waku_store, waku_store_legacy] +import waku/[waku_core/message, waku_store] const FUTURE_TIMEOUT* = 1.seconds @@ -18,9 +18,6 @@ proc newBoolFuture*(): Future[bool] = proc newHistoryFuture*(): Future[StoreQueryRequest] = newFuture[StoreQueryRequest]() -proc newLegacyHistoryFuture*(): Future[waku_store_legacy.HistoryQuery] = - newFuture[waku_store_legacy.HistoryQuery]() - proc toResult*[T](future: Future[T]): Result[T, string] = if future.cancelled(): return chronos.err("Future timeouted before completing.") diff --git a/tests/testlib/postgres_legacy.nim b/tests/testlib/postgres_legacy.nim deleted file mode 100644 index 50988c6c8..000000000 --- a/tests/testlib/postgres_legacy.nim +++ /dev/null @@ -1,27 +0,0 @@ -import chronicles, chronos -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver as driver_module, - waku/waku_archive_legacy/driver/builder, - waku/waku_archive_legacy/driver/postgres_driver - -const storeMessageDbUrl = "postgres://postgres:test123@localhost:5432/postgres" - -proc newTestPostgresDriver*(): Future[Result[ArchiveDriver, string]] {. - async, deprecated -.} = - proc onErr(errMsg: string) {.gcsafe, closure.} = - error "error creating ArchiveDriver", error = errMsg - quit(QuitFailure) - - let - vacuum = false - migrate = true - maxNumConn = 50 - - let driverRes = - await ArchiveDriver.new(storeMessageDbUrl, vacuum, migrate, maxNumConn, onErr) - if driverRes.isErr(): - onErr("could not create archive driver: " & driverRes.error) - - return ok(driverRes.get()) diff --git a/tests/testlib/wakunode.nim b/tests/testlib/wakunode.nim index ef6ba2b24..77c017d96 100644 --- a/tests/testlib/wakunode.nim +++ b/tests/testlib/wakunode.nim @@ -27,22 +27,20 @@ import # TODO: migrate to usage of a test cluster conf proc defaultTestWakuConfBuilder*(): WakuConfBuilder = var builder = WakuConfBuilder.init() - builder.withP2pTcpPort(Port(60000)) builder.withP2pListenAddress(parseIpAddress("0.0.0.0")) builder.restServerConf.withListenAddress(parseIpAddress("127.0.0.1")) builder.withDnsAddrsNameServers( @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")] ) builder.withNatStrategy("any") - builder.withMaxConnections(50) - builder.withRelayServiceRatio("60:40") + builder.withMaxConnections(150) + builder.withRelayServiceRatio("50:50") builder.withMaxMessageSize("1024 KiB") builder.withClusterId(DefaultClusterId) builder.withSubscribeShards(@[DefaultShardId]) builder.withRelay(true) builder.withRendezvous(true) builder.storeServiceConf.withDbMigration(false) - builder.storeServiceConf.withSupportV2(false) return builder proc defaultTestWakuConf*(): WakuConf = @@ -80,7 +78,7 @@ proc newTestWakuNode*( # Update extPort to default value if it's missing and there's an extIp or a DNS domain let extPort = if (extIp.isSome() or dns4DomainName.isSome()) and extPort.isNone(): - some(Port(60000)) + some(Port(0)) else: extPort diff --git a/tests/tools/test_confutils_envvar.nim b/tests/tools/test_confutils_envvar.nim index ed559ad0b..76d9ddd31 100644 --- a/tests/tools/test_confutils_envvar.nim +++ b/tests/tools/test_confutils_envvar.nim @@ -19,7 +19,7 @@ type TestConf = object Option[InputFile] listenAddress* {. - defaultValue: parseIpAddress("127.0.0.1"), + defaultValue: IpAddress(family: IpAddressFamily.IPv4, address_v4: [127u8, 0, 0, 1]), desc: "Listening address", name: "listen-address" .}: IpAddress @@ -62,9 +62,15 @@ suite "nim-confutils - envvar": ## Then check confLoadRes.isOk() + let parsedIpAddress = + try: + parseIpAddress(listenAddress) + except ValueError: + IpAddress(family: IpAddressFamily.IPv4, address_v4: [0u8, 0, 0, 0]) + let conf = confLoadRes.get() check: - conf.listenAddress == parseIpAddress(listenAddress) + conf.listenAddress == parsedIpAddress conf.tcpPort == Port(8080) conf.configFile.isSome() diff --git a/tests/waku_archive/test_driver_postgres_query.nim b/tests/waku_archive/test_driver_postgres_query.nim index 8bbdc52c0..240ac28dd 100644 --- a/tests/waku_archive/test_driver_postgres_query.nim +++ b/tests/waku_archive/test_driver_postgres_query.nim @@ -49,17 +49,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -85,17 +84,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -124,29 +122,28 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), + fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), + fakeWakuMessage( + @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" + ), + fakeWakuMessage( + @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" + ), + fakeWakuMessage( + @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" + ), + fakeWakuMessage( + @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" + ), + fakeWakuMessage( + @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" + ), + fakeWakuMessage( + @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" + ), + ] var messages = expected shuffle(messages) @@ -175,17 +172,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -216,17 +212,16 @@ suite "Postgres driver - queries": const contentTopic2 = "test-content-topic-2" const contentTopic3 = "test-content-topic-3" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -275,14 +270,13 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -338,35 +332,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -395,35 +388,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -450,35 +442,34 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" const pubsubTopic = "test-pubsub-topic" - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -509,18 +500,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -551,18 +540,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -593,17 +580,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -640,18 +626,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -685,18 +669,16 @@ suite "Postgres driver - queries": ## Given const contentTopic = "test-content-topic" - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -732,59 +714,42 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -819,59 +784,42 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -903,19 +851,18 @@ suite "Postgres driver - queries": asyncTest "only hashes - descending order": ## Given let timeOrigin = now() - var expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + var expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -942,17 +889,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -982,17 +928,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1023,61 +968,44 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1109,18 +1037,17 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1153,17 +1080,16 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1195,20 +1121,19 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1240,21 +1165,20 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1289,21 +1213,20 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1339,52 +1262,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1421,51 +1331,38 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1502,52 +1399,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1585,52 +1469,39 @@ suite "Postgres driver - queries": const pubsubTopic = "test-pubsub-topic" let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1667,16 +1538,15 @@ suite "Postgres driver - queries": let timeOrigin = now() let oldestTime = ts(00, timeOrigin) let newestTime = ts(100, timeOrigin) - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = oldestTime), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = newestTime), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = oldestTime), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = newestTime), + ] var messages = expected shuffle(messages) @@ -1714,16 +1584,15 @@ suite "Postgres driver - queries": let timeOrigin = now() let targetTime = ts(40, timeOrigin) - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = targetTime), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = targetTime), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1751,16 +1620,15 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1793,12 +1661,11 @@ suite "Postgres driver - queries": const contentTopic = "test-content-topic" let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ] var messages = expected var hashes = newSeq[WakuMessageHash](0) diff --git a/tests/waku_archive/test_driver_queue_query.nim b/tests/waku_archive/test_driver_queue_query.nim index 11b39a3f8..b93e79bf6 100644 --- a/tests/waku_archive/test_driver_queue_query.nim +++ b/tests/waku_archive/test_driver_queue_query.nim @@ -3,13 +3,9 @@ import std/[options, sequtils, random, algorithm], testutils/unittests, chronos, chronicles import - waku/ - [ - waku_archive, - waku_archive/driver/queue_driver, - waku_core, - waku_core/message/digest, - ], + waku/[ + waku_archive, waku_archive/driver/queue_driver, waku_core, waku_core/message/digest + ], ../testlib/common, ../testlib/wakucore @@ -29,17 +25,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -71,17 +66,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -115,17 +109,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -161,17 +154,16 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -207,14 +199,13 @@ suite "Queue driver - query by content topic": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -281,35 +272,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -345,35 +335,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -407,35 +396,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newTestSqliteDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -474,18 +462,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -521,18 +507,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -568,17 +552,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -618,18 +601,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -668,18 +649,16 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -720,59 +699,42 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -814,59 +776,42 @@ suite "Queue driver - query by cursor": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -908,17 +853,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -953,17 +897,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -999,61 +942,44 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1092,18 +1018,17 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1141,17 +1066,16 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1188,20 +1112,19 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1238,21 +1161,20 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1292,21 +1214,20 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1347,52 +1268,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1436,51 +1344,38 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1524,52 +1419,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1614,52 +1496,39 @@ suite "Queue driver - query by time range": let driver = newTestSqliteDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) diff --git a/tests/waku_archive/test_driver_sqlite_query.nim b/tests/waku_archive/test_driver_sqlite_query.nim index 9812a50f3..6ae7c5b9d 100644 --- a/tests/waku_archive/test_driver_sqlite_query.nim +++ b/tests/waku_archive/test_driver_sqlite_query.nim @@ -22,17 +22,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -65,17 +64,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -110,29 +108,28 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), + fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), + fakeWakuMessage( + @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" + ), + fakeWakuMessage( + @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" + ), + fakeWakuMessage( + @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" + ), + fakeWakuMessage( + @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" + ), + fakeWakuMessage( + @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" + ), + fakeWakuMessage( + @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" + ), + ] var messages = expected shuffle(messages) @@ -167,17 +164,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -214,17 +210,16 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -261,14 +256,13 @@ suite "SQLite driver - query by content topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), + ] var messages = expected shuffle(messages) @@ -337,35 +331,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -400,35 +393,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -461,35 +453,34 @@ suite "SQLite driver - query by pubsub topic": let driver = newSqliteArchiveDriver() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ), + ] var messages = expected shuffle(messages) @@ -527,18 +518,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -575,18 +564,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -623,17 +610,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + var messages = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] shuffle(messages) info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) @@ -674,18 +660,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), # << cursor + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), + fakeWakuMessage(@[byte 7], ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -725,18 +709,16 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], ts = ts(00)), + fakeWakuMessage(@[byte 1], ts = ts(10)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), + ] var messages = expected shuffle(messages) @@ -778,59 +760,42 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -871,59 +836,42 @@ suite "SQLite driver - query by cursor": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), # << cursor + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -964,17 +912,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1010,17 +957,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1057,61 +1003,44 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # start_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1149,18 +1078,17 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + # end_time + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1199,17 +1127,16 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1247,20 +1174,19 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1298,21 +1224,20 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1353,21 +1278,20 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] + let expected = @[ + fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), + # start_time + fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), + # << cursor + fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ] var messages = expected shuffle(messages) @@ -1409,52 +1333,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + # start_time + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + # end_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1497,51 +1408,38 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1584,52 +1482,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) @@ -1673,52 +1558,39 @@ suite "SQLite driver - query by time range": let driver = newSqliteArchiveDriver() let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] + let expected = @[ + (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), + (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), + # << cursor + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), + ), + # start_time + ( + pubsubTopic, + fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), + ), + ( + pubsubTopic, + fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), + ), + (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), + (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), + ), + # end_time + ( + DefaultPubsubTopic, + fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), + ), + ] var messages = expected shuffle(messages) diff --git a/tests/waku_archive/test_retention_policy.nim b/tests/waku_archive/test_retention_policy.nim index ea86e1d69..394d5711d 100644 --- a/tests/waku_archive/test_retention_policy.nim +++ b/tests/waku_archive/test_retention_policy.nim @@ -121,16 +121,15 @@ suite "Waku Archive - Retention policy": retentionPolicy: RetentionPolicy = CapacityRetentionPolicy.new(capacity = capacity) - let messages = - @[ - fakeWakuMessage(contentTopic = DefaultContentTopic, ts = ts(0)), - fakeWakuMessage(contentTopic = DefaultContentTopic, ts = ts(1)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(2)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(3)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(4)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(5)), - fakeWakuMessage(contentTopic = contentTopic, ts = ts(6)), - ] + let messages = @[ + fakeWakuMessage(contentTopic = DefaultContentTopic, ts = ts(0)), + fakeWakuMessage(contentTopic = DefaultContentTopic, ts = ts(1)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(2)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(3)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(4)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(5)), + fakeWakuMessage(contentTopic = contentTopic, ts = ts(6)), + ] ## When for msg in messages: diff --git a/tests/waku_archive/test_waku_archive.nim b/tests/waku_archive/test_waku_archive.nim index 802473d64..9b235dd57 100644 --- a/tests/waku_archive/test_waku_archive.nim +++ b/tests/waku_archive/test_waku_archive.nim @@ -36,14 +36,13 @@ suite "Waku Archive - message handling": let archive = newWakuArchive(driver) ## Given - let msgList = - @[ - fakeWakuMessage(ephemeral = false, payload = "1"), - fakeWakuMessage(ephemeral = true, payload = "2"), - fakeWakuMessage(ephemeral = true, payload = "3"), - fakeWakuMessage(ephemeral = true, payload = "4"), - fakeWakuMessage(ephemeral = false, payload = "5"), - ] + let msgList = @[ + fakeWakuMessage(ephemeral = false, payload = "1"), + fakeWakuMessage(ephemeral = true, payload = "2"), + fakeWakuMessage(ephemeral = true, payload = "3"), + fakeWakuMessage(ephemeral = true, payload = "4"), + fakeWakuMessage(ephemeral = false, payload = "5"), + ] ## When for msg in msgList: @@ -127,39 +126,38 @@ suite "Waku Archive - message handling": procSuite "Waku Archive - find messages": ## Fixtures let timeOrigin = now() - let msgListA = - @[ - fakeWakuMessage( - @[byte 00], contentTopic = ContentTopic("2"), ts = ts(00, timeOrigin) - ), - fakeWakuMessage( - @[byte 01], contentTopic = ContentTopic("1"), ts = ts(10, timeOrigin) - ), - fakeWakuMessage( - @[byte 02], contentTopic = ContentTopic("2"), ts = ts(20, timeOrigin) - ), - fakeWakuMessage( - @[byte 03], contentTopic = ContentTopic("1"), ts = ts(30, timeOrigin) - ), - fakeWakuMessage( - @[byte 04], contentTopic = ContentTopic("2"), ts = ts(40, timeOrigin) - ), - fakeWakuMessage( - @[byte 05], contentTopic = ContentTopic("1"), ts = ts(50, timeOrigin) - ), - fakeWakuMessage( - @[byte 06], contentTopic = ContentTopic("2"), ts = ts(60, timeOrigin) - ), - fakeWakuMessage( - @[byte 07], contentTopic = ContentTopic("1"), ts = ts(70, timeOrigin) - ), - fakeWakuMessage( - @[byte 08], contentTopic = ContentTopic("2"), ts = ts(80, timeOrigin) - ), - fakeWakuMessage( - @[byte 09], contentTopic = ContentTopic("1"), ts = ts(90, timeOrigin) - ), - ] + let msgListA = @[ + fakeWakuMessage( + @[byte 00], contentTopic = ContentTopic("2"), ts = ts(00, timeOrigin) + ), + fakeWakuMessage( + @[byte 01], contentTopic = ContentTopic("1"), ts = ts(10, timeOrigin) + ), + fakeWakuMessage( + @[byte 02], contentTopic = ContentTopic("2"), ts = ts(20, timeOrigin) + ), + fakeWakuMessage( + @[byte 03], contentTopic = ContentTopic("1"), ts = ts(30, timeOrigin) + ), + fakeWakuMessage( + @[byte 04], contentTopic = ContentTopic("2"), ts = ts(40, timeOrigin) + ), + fakeWakuMessage( + @[byte 05], contentTopic = ContentTopic("1"), ts = ts(50, timeOrigin) + ), + fakeWakuMessage( + @[byte 06], contentTopic = ContentTopic("2"), ts = ts(60, timeOrigin) + ), + fakeWakuMessage( + @[byte 07], contentTopic = ContentTopic("1"), ts = ts(70, timeOrigin) + ), + fakeWakuMessage( + @[byte 08], contentTopic = ContentTopic("2"), ts = ts(80, timeOrigin) + ), + fakeWakuMessage( + @[byte 09], contentTopic = ContentTopic("1"), ts = ts(90, timeOrigin) + ), + ] let archiveA = block: let @@ -446,19 +444,18 @@ procSuite "Waku Archive - find messages": driver = newSqliteArchiveDriver() archive = newWakuArchive(driver) - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2")), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 5], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 6], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 7], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 8], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2")), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2")), + fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 5], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 6], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 7], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 8], contentTopic = DefaultContentTopic), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2")), + ] for msg in msgList: require ( diff --git a/tests/waku_archive_legacy/archive_utils.nim b/tests/waku_archive_legacy/archive_utils.nim deleted file mode 100644 index 8df0f5d7f..000000000 --- a/tests/waku_archive_legacy/archive_utils.nim +++ /dev/null @@ -1,55 +0,0 @@ -{.used.} - -import std/options, results, chronos, libp2p/crypto/crypto - -import - waku/[ - node/peer_manager, - waku_core, - waku_archive_legacy, - waku_archive_legacy/common, - waku_archive_legacy/driver/sqlite_driver, - waku_archive_legacy/driver/sqlite_driver/migrations, - common/databases/db_sqlite, - ], - ../testlib/[wakucore] - -proc newSqliteDatabase*(path: Option[string] = string.none()): SqliteDatabase = - SqliteDatabase.new(path.get(":memory:")).tryGet() - -proc newSqliteArchiveDriver*(): ArchiveDriver = - let database = newSqliteDatabase() - migrate(database).tryGet() - return SqliteDriver.new(database).tryGet() - -proc newWakuArchive*(driver: ArchiveDriver): WakuArchive = - WakuArchive.new(driver).get() - -proc computeArchiveCursor*( - pubsubTopic: PubsubTopic, message: WakuMessage -): ArchiveCursor = - ArchiveCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - hash: computeMessageHash(pubsubTopic, message), - ) - -proc put*( - driver: ArchiveDriver, pubsubTopic: PubSubTopic, msgList: seq[WakuMessage] -): ArchiveDriver = - for msg in msgList: - let - msgDigest = computeDigest(msg) - msgHash = computeMessageHash(pubsubTopic, msg) - _ = waitFor driver.put(pubsubTopic, msg, msgDigest, msgHash, msg.timestamp) - # discard crashes - return driver - -proc newArchiveDriverWithMessages*( - pubsubTopic: PubSubTopic, msgList: seq[WakuMessage] -): ArchiveDriver = - var driver = newSqliteArchiveDriver() - driver = driver.put(pubsubTopic, msgList) - return driver diff --git a/tests/waku_archive_legacy/test_all.nim b/tests/waku_archive_legacy/test_all.nim deleted file mode 100644 index 9d45d99a1..000000000 --- a/tests/waku_archive_legacy/test_all.nim +++ /dev/null @@ -1,13 +0,0 @@ -{.used.} - -import - ./test_driver_postgres_query, - ./test_driver_postgres, - ./test_driver_queue_index, - ./test_driver_queue_pagination, - ./test_driver_queue_query, - ./test_driver_queue, - ./test_driver_sqlite_query, - ./test_driver_sqlite, - ./test_retention_policy, - ./test_waku_archive diff --git a/tests/waku_archive_legacy/test_driver_postgres.nim b/tests/waku_archive_legacy/test_driver_postgres.nim deleted file mode 100644 index 7657b6e1f..000000000 --- a/tests/waku_archive_legacy/test_driver_postgres.nim +++ /dev/null @@ -1,220 +0,0 @@ -{.used.} - -import std/[sequtils, options], testutils/unittests, chronos -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/postgres_driver, - waku/waku_archive/driver/postgres_driver as new_postgres_driver, - waku/waku_core, - waku/waku_core/message/digest, - ../testlib/wakucore, - ../testlib/testasync, - ../testlib/postgres_legacy, - ../testlib/postgres as new_postgres - -proc computeTestCursor(pubsubTopic: PubsubTopic, message: WakuMessage): ArchiveCursor = - ArchiveCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - hash: computeMessageHash(pubsubTopic, message), - ) - -suite "Postgres driver": - ## Unique driver instance - var driver {.threadvar.}: postgres_driver.PostgresDriver - - ## We need to artificially create an instance of the "newDriver" - ## because this is the only one in charge of creating partitions - ## We will clean legacy store soon and this file will get removed. - var newDriver {.threadvar.}: new_postgres_driver.PostgresDriver - - asyncSetup: - let driverRes = await postgres_legacy.newTestPostgresDriver() - if driverRes.isErr(): - assert false, driverRes.error - - driver = postgres_driver.PostgresDriver(driverRes.get()) - - let newDriverRes = await new_postgres.newTestPostgresDriver() - if driverRes.isErr(): - assert false, driverRes.error - - newDriver = new_postgres_driver.PostgresDriver(newDriverRes.get()) - - asyncTeardown: - var resetRes = await driver.reset() - if resetRes.isErr(): - assert false, resetRes.error - - (await driver.close()).expect("driver to close") - - resetRes = await newDriver.reset() - if resetRes.isErr(): - assert false, resetRes.error - - (await newDriver.close()).expect("driver to close") - - asyncTest "Asynchronous queries": - var futures = newSeq[Future[ArchiveDriverResult[void]]](0) - - let beforeSleep = now() - for _ in 1 .. 100: - futures.add(driver.sleep(1)) - - await allFutures(futures) - - let diff = now() - beforeSleep - # Actually, the diff randomly goes between 1 and 2 seconds. - # although in theory it should spend 1s because we establish 100 - # connections and we spawn 100 tasks that spend ~1s each. - assert diff < 20_000_000_000 - - asyncTest "Insert a message": - const contentTopic = "test-content-topic" - const meta = "test meta" - - let msg = fakeWakuMessage(contentTopic = contentTopic, meta = meta) - - let computedDigest = computeDigest(msg) - let computedHash = computeMessageHash(DefaultPubsubTopic, msg) - - let putRes = await driver.put( - DefaultPubsubTopic, msg, computedDigest, computedHash, msg.timestamp - ) - assert putRes.isOk(), putRes.error - - let storedMsg = (await driver.getAllMessages()).tryGet() - - assert storedMsg.len == 1 - - let (pubsubTopic, actualMsg, digest, _, hash) = storedMsg[0] - assert actualMsg.contentTopic == contentTopic - assert pubsubTopic == DefaultPubsubTopic - assert toHex(computedDigest.data) == toHex(digest) - assert toHex(actualMsg.payload) == toHex(msg.payload) - assert toHex(computedHash) == toHex(hash) - assert toHex(actualMsg.meta) == toHex(msg.meta) - - asyncTest "Insert and query message": - const contentTopic1 = "test-content-topic-1" - const contentTopic2 = "test-content-topic-2" - const pubsubTopic1 = "pubsubtopic-1" - const pubsubTopic2 = "pubsubtopic-2" - - let msg1 = fakeWakuMessage(contentTopic = contentTopic1) - - var putRes = await driver.put( - pubsubTopic1, - msg1, - computeDigest(msg1), - computeMessageHash(pubsubTopic1, msg1), - msg1.timestamp, - ) - assert putRes.isOk(), putRes.error - - let msg2 = fakeWakuMessage(contentTopic = contentTopic2) - - putRes = await driver.put( - pubsubTopic2, - msg2, - computeDigest(msg2), - computeMessageHash(pubsubTopic2, msg2), - msg2.timestamp, - ) - assert putRes.isOk(), putRes.error - - let countMessagesRes = await driver.getMessagesCount() - - assert countMessagesRes.isOk(), $countMessagesRes.error - assert countMessagesRes.get() == 2 - - var messagesRes = await driver.getMessages(contentTopic = @[contentTopic1]) - - assert messagesRes.isOk(), $messagesRes.error - assert messagesRes.get().len == 1 - - # Get both content topics, check ordering - messagesRes = - await driver.getMessages(contentTopic = @[contentTopic1, contentTopic2]) - assert messagesRes.isOk(), messagesRes.error - - assert messagesRes.get().len == 2 - assert messagesRes.get()[0][1].contentTopic == contentTopic1 - - # Descending order - messagesRes = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], ascendingOrder = false - ) - assert messagesRes.isOk(), messagesRes.error - - assert messagesRes.get().len == 2 - assert messagesRes.get()[0][1].contentTopic == contentTopic2 - - # cursor - # Get both content topics - messagesRes = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], - cursor = some(computeTestCursor(pubsubTopic1, messagesRes.get()[1][1])), - ) - assert messagesRes.isOk() - assert messagesRes.get().len == 1 - - # Get both content topics but one pubsub topic - messagesRes = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], pubsubTopic = some(pubsubTopic1) - ) - assert messagesRes.isOk(), messagesRes.error - - assert messagesRes.get().len == 1 - assert messagesRes.get()[0][1].contentTopic == contentTopic1 - - # Limit - messagesRes = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], maxPageSize = 1 - ) - assert messagesRes.isOk(), messagesRes.error - assert messagesRes.get().len == 1 - - asyncTest "Insert true duplicated messages": - # Validates that two completely equal messages can not be stored. - - let now = now() - - let msg1 = fakeWakuMessage(ts = now) - let msg2 = fakeWakuMessage(ts = now) - - let initialNumMsgs = (await driver.getMessagesCount()).valueOr: - raiseAssert "could not get num mgs correctly: " & $error - - var putRes = await driver.put( - DefaultPubsubTopic, - msg1, - computeDigest(msg1), - computeMessageHash(DefaultPubsubTopic, msg1), - msg1.timestamp, - ) - assert putRes.isOk(), putRes.error - - var newNumMsgs = (await driver.getMessagesCount()).valueOr: - raiseAssert "could not get num mgs correctly: " & $error - - assert newNumMsgs == (initialNumMsgs + 1.int64), - "wrong number of messages: " & $newNumMsgs - - putRes = await driver.put( - DefaultPubsubTopic, - msg2, - computeDigest(msg2), - computeMessageHash(DefaultPubsubTopic, msg2), - msg2.timestamp, - ) - - assert putRes.isOk() - - newNumMsgs = (await driver.getMessagesCount()).valueOr: - raiseAssert "could not get num mgs correctly: " & $error - - assert newNumMsgs == (initialNumMsgs + 1.int64), - "wrong number of messages: " & $newNumMsgs diff --git a/tests/waku_archive_legacy/test_driver_postgres_query.nim b/tests/waku_archive_legacy/test_driver_postgres_query.nim deleted file mode 100644 index e164a63a8..000000000 --- a/tests/waku_archive_legacy/test_driver_postgres_query.nim +++ /dev/null @@ -1,1987 +0,0 @@ -{.used.} - -import - std/[options, sequtils, strformat, random, algorithm], - testutils/unittests, - chronos, - chronicles -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver as driver_module, - waku/waku_archive_legacy/driver/postgres_driver, - waku/waku_archive/driver/postgres_driver as new_postgres_driver, - waku/waku_core, - waku/waku_core/message/digest, - ../testlib/common, - ../testlib/wakucore, - ../testlib/testasync, - ../testlib/postgres_legacy, - ../testlib/postgres as new_postgres, - ../testlib/testutils - -logScope: - topics = "test archive postgres driver" - -## This whole file is copied from the 'test_driver_sqlite_query.nim' file -## and it tests the same use cases but using the postgres driver. - -# Initialize the random number generator -common.randomize() - -proc computeTestCursor(pubsubTopic: PubsubTopic, message: WakuMessage): ArchiveCursor = - ArchiveCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - hash: computeMessageHash(pubsubTopic, message), - ) - -suite "Postgres driver - queries": - ## Unique driver instance - var driver {.threadvar.}: postgres_driver.PostgresDriver - - ## We need to artificially create an instance of the "newDriver" - ## because this is the only one in charge of creating partitions - ## We will clean legacy store soon and this file will get removed. - var newDriver {.threadvar.}: new_postgres_driver.PostgresDriver - - asyncSetup: - let driverRes = await postgres_legacy.newTestPostgresDriver() - if driverRes.isErr(): - assert false, driverRes.error - - driver = postgres_driver.PostgresDriver(driverRes.get()) - - let newDriverRes = await new_postgres.newTestPostgresDriver() - if driverRes.isErr(): - assert false, driverRes.error - - newDriver = new_postgres_driver.PostgresDriver(newDriverRes.get()) - - asyncTeardown: - var resetRes = await driver.reset() - if resetRes.isErr(): - assert false, resetRes.error - - (await driver.close()).expect("driver to close") - - resetRes = await newDriver.reset() - if resetRes.isErr(): - assert false, resetRes.error - - (await newDriver.close()).expect("driver to close") - - asyncTest "no content topic": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages(maxPageSize = 5, ascendingOrder = true) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - asyncTest "single content topic": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - asyncTest "single content topic with meta field": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - asyncTest "single content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = false - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[6 .. 7].reversed() - - asyncTest "multiple content topic": - ## Given - const contentTopic1 = "test-content-topic-1" - const contentTopic2 = "test-content-topic-2" - const contentTopic3 = "test-content-topic-3" - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - var res = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], - pubsubTopic = some(DefaultPubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - startTime = some(ts(00)), - endTime = some(ts(40)), - ) - - ## Then - assert res.isOk(), res.error - var filteredMessages = res.tryGet().mapIt(it[1]) - check filteredMessages == expected[2 .. 3] - - ## When - ## This is very similar to the previous one but we enforce to use the prepared - ## statement by querying one single content topic - res = await driver.getMessages( - contentTopic = @[contentTopic1], - pubsubTopic = some(DefaultPubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - startTime = some(ts(00)), - endTime = some(ts(40)), - ) - - ## Then - assert res.isOk(), res.error - filteredMessages = res.tryGet().mapIt(it[1]) - check filteredMessages == @[expected[2]] - - asyncTest "single content topic - no results": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - asyncTest "content topic and max page size - not enough messages stored": - ## Given - const pageSize: uint = 50 - - for t in 0 ..< 40: - let msg = fakeWakuMessage(@[byte t], DefaultContentTopic, ts = ts(t)) - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[DefaultContentTopic], - maxPageSize = pageSize, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 40 - - asyncTest "pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - asyncTest "no pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages(maxPageSize = 2, ascendingOrder = true) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[0 .. 1] - - asyncTest "content topic and pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - asyncTest "only cursor": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - asyncTest "only cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = false - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3].reversed() - - asyncTest "only cursor - invalid": - ## Given - const contentTopic = "test-content-topic" - - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let fakeCursor = computeMessageHash(DefaultPubsubTopic, fakeWakuMessage()) - let cursor = ArchiveCursor(hash: fakeCursor) - - ## When - let res = await driver.getMessages( - includeData = true, - contentTopicSeq = @[DefaultContentTopic], - pubsubTopic = none(PubsubTopic), - cursor = some(cursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = @[], - maxPageSize = 5, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - check: - res.value.len == 0 - - asyncTest "content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - asyncTest "content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 5].reversed() - - asyncTest "pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[5][0], expected[5][1]) - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[6 .. 7] - - asyncTest "pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[6][0], expected[6][1]) - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - asyncTest "only hashes - descending order": - ## Given - let timeOrigin = now() - var expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - let hashes = messages.mapIt(computeMessageHash(DefaultPubsubTopic, it)) - - for (msg, hash) in messages.zip(hashes): - require ( - await driver.put( - DefaultPubsubTopic, msg, computeDigest(msg), hash, msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages(hashes = hashes, ascendingOrder = false) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.reversed() - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages - - asyncTest "start time only": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - startTime = some(ts(15, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - asyncTest "end time only": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - endTime = some(ts(45, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - asyncTest "start time and end time": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - startTime = some(ts(15, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[2 .. 4] - - asyncTest "invalid time range - no results": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(45, timeOrigin)), - endTime = some(ts(15, timeOrigin)), - maxPageSize = 2, - ascendingOrder = true, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - asyncTest "time range start and content topic": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - asyncTest "time range start and content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6].reversed() - - asyncTest "time range start, single content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[3]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[4 .. 9] - - asyncTest "time range start, single content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[3 .. 4].reversed() - - asyncTest "time range, content topic, pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(0, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[3 .. 4] - - asyncTest "time range, content topic, pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[7][0], expected[7][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - asyncTest "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[1][0], expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - assert res.isOk(), res.error - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - asyncTest "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range, descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeTestCursor(expected[1][0], expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - assert res.isOk(), res.error - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - xasyncTest "Get oldest and newest message timestamp": - ## This test no longer makes sense because that will always be controlled by the newDriver - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let oldestTime = ts(00, timeOrigin) - let newestTime = ts(100, timeOrigin) - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = oldestTime), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = newestTime), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## just keep the second resolution. - ## Notice that the oldest timestamps considers the minimum partition timestamp, which - ## is expressed in seconds. - let oldestPartitionTimestamp = - Timestamp(float(oldestTime) / 1_000_000_000) * 1_000_000_000 - - var res = await driver.getOldestMessageTimestamp() - assert res.isOk(), res.error - - ## We give certain margin of error. The oldest timestamp is obtained from - ## the oldest partition timestamp and there might be at most one second of difference - ## between the time created in the test and the oldest-partition-timestamp created within - ## the driver logic. - assert abs(res.get() - oldestPartitionTimestamp) < (2 * 1_000_000_000), - fmt"Failed to retrieve the latest timestamp {res.get()} != {oldestPartitionTimestamp}" - - res = await driver.getNewestMessageTimestamp() - assert res.isOk(), res.error - assert res.get() == newestTime, "Failed to retrieve the newest timestamp" - - xasyncTest "Delete messages older than certain timestamp": - ## This test no longer makes sense because that will always be controlled by the newDriver - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let targetTime = ts(40, timeOrigin) - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = targetTime), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - var res = await driver.getMessagesCount() - assert res.isOk(), res.error - assert res.get() == 7, "Failed to retrieve the initial number of messages" - - let deleteRes = await driver.deleteMessagesOlderThanTimestamp(targetTime) - assert deleteRes.isOk(), deleteRes.error - - res = await driver.getMessagesCount() - assert res.isOk(), res.error - assert res.get() == 3, "Failed to retrieve the # of messages after deletion" - - xasyncTest "Keep last n messages": - ## This test no longer makes sense because that will always be controlled by the newDriver - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - var res = await driver.getMessagesCount() - assert res.isOk(), res.error - assert res.get() == 7, "Failed to retrieve the initial number of messages" - - let deleteRes = await driver.deleteOldestMessagesNotWithinLimit(2) - assert deleteRes.isOk(), deleteRes.error - - res = await driver.getMessagesCount() - assert res.isOk(), res.error - assert res.get() == 2, "Failed to retrieve the # of messages after deletion" - - asyncTest "Exists table": - var existsRes = await driver.existsTable("version") - assert existsRes.isOk(), existsRes.error - check existsRes.get() == true - - asyncTest "Query by message hash only - legacy": - const contentTopic = "test-content-topic" - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - ] - var messages = expected - - var hashes = newSeq[WakuMessageHash](0) - for msg in messages: - let hash = computeMessageHash(DefaultPubsubTopic, msg) - hashes.add(hash) - require ( - await driver.put( - DefaultPubsubTopic, msg, computeDigest(msg), hash, msg.timestamp - ) - ).isOk() - - let ret = (await driver.getMessages(hashes = hashes)).valueOr: - assert false, $error - return - - check: - ret.len == 3 - ## (PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash) - ret[2][4] == hashes[0] - ret[1][4] == hashes[1] - ret[0][4] == hashes[2] diff --git a/tests/waku_archive_legacy/test_driver_queue.nim b/tests/waku_archive_legacy/test_driver_queue.nim deleted file mode 100644 index aec9ad65d..000000000 --- a/tests/waku_archive_legacy/test_driver_queue.nim +++ /dev/null @@ -1,182 +0,0 @@ -{.used.} - -import std/options, results, testutils/unittests -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/queue_driver/queue_driver {.all.}, - waku/waku_archive_legacy/driver/queue_driver/index, - waku/waku_core - -# Helper functions - -proc genIndexedWakuMessage(i: int8): (Index, WakuMessage) = - ## Use i to generate an Index WakuMessage - var data {.noinit.}: array[32, byte] - for x in data.mitems: - x = i.byte - - let - message = WakuMessage(payload: @[byte i], timestamp: Timestamp(i)) - topic = "test-pubsub-topic" - cursor = Index( - receiverTime: Timestamp(i), - senderTime: Timestamp(i), - digest: MessageDigest(data: data), - pubsubTopic: topic, - hash: computeMessageHash(topic, message), - ) - - (cursor, message) - -proc getPrepopulatedTestQueue(unsortedSet: auto, capacity: int): QueueDriver = - let driver = QueueDriver.new(capacity) - - for i in unsortedSet: - let (index, message) = genIndexedWakuMessage(i.int8) - discard driver.add(index, message) - - driver - -procSuite "Sorted driver queue": - test "queue capacity - add a message over the limit": - ## Given - let capacity = 5 - let driver = QueueDriver.new(capacity) - - ## When - # Fill up the queue - for i in 1 .. capacity: - let (index, message) = genIndexedWakuMessage(i.int8) - require(driver.add(index, message).isOk()) - - # Add one more. Capacity should not be exceeded - let (index, message) = genIndexedWakuMessage(capacity.int8 + 1) - require(driver.add(index, message).isOk()) - - ## Then - check: - driver.len == capacity - - test "queue capacity - add message older than oldest in the queue": - ## Given - let capacity = 5 - let driver = QueueDriver.new(capacity) - - ## When - # Fill up the queue - for i in 1 .. capacity: - let (index, message) = genIndexedWakuMessage(i.int8) - require(driver.add(index, message).isOk()) - - # Attempt to add message with older value than oldest in queue should fail - let - oldestTimestamp = driver.first().get().senderTime - (index, message) = genIndexedWakuMessage(oldestTimestamp.int8 - 1) - addRes = driver.add(index, message) - - ## Then - check: - addRes.isErr() - addRes.error() == "too_old" - - check: - driver.len == capacity - - test "queue sort-on-insert": - ## Given - let - capacity = 5 - unsortedSet = [5, 1, 3, 2, 4] - let driver = getPrepopulatedTestQueue(unsortedSet, capacity) - - # Walk forward through the set and verify ascending order - var (prevSmaller, _) = genIndexedWakuMessage(min(unsortedSet).int8 - 1) - for i in driver.fwdIterator: - let (index, _) = i - check cmp(index, prevSmaller) > 0 - prevSmaller = index - - # Walk backward through the set and verify descending order - var (prevLarger, _) = genIndexedWakuMessage(max(unsortedSet).int8 + 1) - for i in driver.bwdIterator: - let (index, _) = i - check cmp(index, prevLarger) < 0 - prevLarger = index - - test "access first item from queue": - ## Given - let - capacity = 5 - unsortedSet = [5, 1, 3, 2, 4] - let driver = getPrepopulatedTestQueue(unsortedSet, capacity) - - ## When - let firstRes = driver.first() - - ## Then - check: - firstRes.isOk() - - let first = firstRes.tryGet() - check: - first.senderTime == Timestamp(1) - - test "get first item from empty queue should fail": - ## Given - let capacity = 5 - let driver = QueueDriver.new(capacity) - - ## When - let firstRes = driver.first() - - ## Then - check: - firstRes.isErr() - firstRes.error() == "Not found" - - test "access last item from queue": - ## Given - let - capacity = 5 - unsortedSet = [5, 1, 3, 2, 4] - let driver = getPrepopulatedTestQueue(unsortedSet, capacity) - - ## When - let lastRes = driver.last() - - ## Then - check: - lastRes.isOk() - - let last = lastRes.tryGet() - check: - last.senderTime == Timestamp(5) - - test "get last item from empty queue should fail": - ## Given - let capacity = 5 - let driver = QueueDriver.new(capacity) - - ## When - let lastRes = driver.last() - - ## Then - check: - lastRes.isErr() - lastRes.error() == "Not found" - - test "verify if queue contains an index": - ## Given - let - capacity = 5 - unsortedSet = [5, 1, 3, 2, 4] - let driver = getPrepopulatedTestQueue(unsortedSet, capacity) - - let - (existingIndex, _) = genIndexedWakuMessage(4) - (nonExistingIndex, _) = genIndexedWakuMessage(99) - - ## Then - check: - driver.contains(existingIndex) == true - driver.contains(nonExistingIndex) == false diff --git a/tests/waku_archive_legacy/test_driver_queue_index.nim b/tests/waku_archive_legacy/test_driver_queue_index.nim deleted file mode 100644 index 404dca8cb..000000000 --- a/tests/waku_archive_legacy/test_driver_queue_index.nim +++ /dev/null @@ -1,219 +0,0 @@ -{.used.} - -import std/[times, random], stew/byteutils, testutils/unittests, nimcrypto -import waku/waku_core, waku/waku_archive_legacy/driver/queue_driver/index - -var rng = initRand() - -## Helpers - -proc getTestTimestamp(offset = 0): Timestamp = - let now = getNanosecondTime(epochTime() + float(offset)) - Timestamp(now) - -proc hashFromStr(input: string): MDigest[256] = - var ctx: sha256 - - ctx.init() - ctx.update(input.toBytes()) - let hashed = ctx.finish() - ctx.clear() - - return hashed - -proc randomHash(): WakuMessageHash = - var hash: WakuMessageHash - - for i in 0 ..< hash.len: - let numb: byte = byte(rng.next()) - hash[i] = numb - - hash - -suite "Queue Driver - index": - ## Test vars - let - smallIndex1 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(1000), - hash: randomHash(), - ) - smallIndex2 = Index( - digest: hashFromStr("1234567"), # digest is less significant than senderTime - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(1000), - hash: randomHash(), - ) - largeIndex1 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(9000), - hash: randomHash(), - ) # only senderTime differ from smallIndex1 - largeIndex2 = Index( - digest: hashFromStr("12345"), # only digest differs from smallIndex1 - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(1000), - hash: randomHash(), - ) - eqIndex1 = Index( - digest: hashFromStr("0003"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(54321), - hash: randomHash(), - ) - eqIndex2 = Index( - digest: hashFromStr("0003"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(54321), - hash: randomHash(), - ) - eqIndex3 = Index( - digest: hashFromStr("0003"), - receiverTime: getNanosecondTime(9999), - # receiverTime difference should have no effect on comparisons - senderTime: getNanosecondTime(54321), - hash: randomHash(), - ) - diffPsTopic = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(0), - senderTime: getNanosecondTime(1000), - pubsubTopic: "zzzz", - hash: randomHash(), - ) - noSenderTime1 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(1100), - senderTime: getNanosecondTime(0), - pubsubTopic: "zzzz", - hash: randomHash(), - ) - noSenderTime2 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(10000), - senderTime: getNanosecondTime(0), - pubsubTopic: "zzzz", - hash: randomHash(), - ) - noSenderTime3 = Index( - digest: hashFromStr("1234"), - receiverTime: getNanosecondTime(1200), - senderTime: getNanosecondTime(0), - pubsubTopic: "aaaa", - hash: randomHash(), - ) - noSenderTime4 = Index( - digest: hashFromStr("0"), - receiverTime: getNanosecondTime(1200), - senderTime: getNanosecondTime(0), - pubsubTopic: "zzzz", - hash: randomHash(), - ) - - test "Index comparison": - # Index comparison with senderTime diff - check: - cmp(smallIndex1, largeIndex1) < 0 - cmp(smallIndex2, largeIndex1) < 0 - - # Index comparison with digest diff - check: - cmp(smallIndex1, smallIndex2) < 0 - cmp(smallIndex1, largeIndex2) < 0 - cmp(smallIndex2, largeIndex2) > 0 - cmp(largeIndex1, largeIndex2) > 0 - - # Index comparison when equal - check: - cmp(eqIndex1, eqIndex2) == 0 - - # pubsubTopic difference - check: - cmp(smallIndex1, diffPsTopic) < 0 - - # receiverTime diff plays no role when senderTime set - check: - cmp(eqIndex1, eqIndex3) == 0 - - # receiverTime diff plays no role when digest/pubsubTopic equal - check: - cmp(noSenderTime1, noSenderTime2) == 0 - - # sort on receiverTime with no senderTimestamp and unequal pubsubTopic - check: - cmp(noSenderTime1, noSenderTime3) < 0 - - # sort on receiverTime with no senderTimestamp and unequal digest - check: - cmp(noSenderTime1, noSenderTime4) < 0 - - # sort on receiverTime if no senderTimestamp on only one side - check: - cmp(smallIndex1, noSenderTime1) < 0 - cmp(noSenderTime1, smallIndex1) > 0 # Test symmetry - cmp(noSenderTime2, eqIndex3) < 0 - cmp(eqIndex3, noSenderTime2) > 0 # Test symmetry - - test "Index equality": - # Exactly equal - check: - eqIndex1 == eqIndex2 - - # Receiver time plays no role, even without sender time - check: - eqIndex1 == eqIndex3 - noSenderTime1 == noSenderTime2 # only receiver time differs, indices are equal - noSenderTime1 != noSenderTime3 # pubsubTopics differ - noSenderTime1 != noSenderTime4 # digests differ - - # Unequal sender time - check: - smallIndex1 != largeIndex1 - - # Unequal digest - check: - smallIndex1 != smallIndex2 - - # Unequal hash and digest - check: - smallIndex1 != eqIndex1 - - # Unequal pubsubTopic - check: - smallIndex1 != diffPsTopic - - test "Index computation should not be empty": - ## Given - let ts = getTestTimestamp() - let wm = WakuMessage(payload: @[byte 1, 2, 3], timestamp: ts) - - ## When - let ts2 = getTestTimestamp() + 10 - let index = Index.compute(wm, ts2, DefaultContentTopic) - - ## Then - check: - index.digest.data.len != 0 - index.digest.data.len == 32 # sha2 output length in bytes - index.receiverTime == ts2 # the receiver timestamp should be a non-zero value - index.senderTime == ts - index.pubsubTopic == DefaultContentTopic - - test "Index digest of two identical messsage should be the same": - ## Given - let topic = ContentTopic("test-content-topic") - let - wm1 = WakuMessage(payload: @[byte 1, 2, 3], contentTopic: topic) - wm2 = WakuMessage(payload: @[byte 1, 2, 3], contentTopic: topic) - - ## When - let ts = getTestTimestamp() - let - index1 = Index.compute(wm1, ts, DefaultPubsubTopic) - index2 = Index.compute(wm2, ts, DefaultPubsubTopic) - - ## Then - check: - index1.digest == index2.digest diff --git a/tests/waku_archive_legacy/test_driver_queue_pagination.nim b/tests/waku_archive_legacy/test_driver_queue_pagination.nim deleted file mode 100644 index 05d9759a2..000000000 --- a/tests/waku_archive_legacy/test_driver_queue_pagination.nim +++ /dev/null @@ -1,405 +0,0 @@ -{.used.} - -import - std/[options, sequtils, algorithm], testutils/unittests, libp2p/protobuf/minprotobuf -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/queue_driver/queue_driver {.all.}, - waku/waku_archive_legacy/driver/queue_driver/index, - waku/waku_core, - ../testlib/wakucore - -proc getTestQueueDriver(numMessages: int): QueueDriver = - let testQueueDriver = QueueDriver.new(numMessages) - - var data {.noinit.}: array[32, byte] - for x in data.mitems: - x = 1 - - for i in 0 ..< numMessages: - let msg = WakuMessage(payload: @[byte i], timestamp: Timestamp(i)) - - let index = Index( - receiverTime: Timestamp(i), - senderTime: Timestamp(i), - digest: MessageDigest(data: data), - hash: computeMessageHash(DefaultPubsubTopic, msg), - ) - - discard testQueueDriver.add(index, msg) - - return testQueueDriver - -procSuite "Queue driver - pagination": - let driver = getTestQueueDriver(10) - let - indexList: seq[Index] = toSeq(driver.fwdIterator()).mapIt(it[0]) - msgList: seq[WakuMessage] = toSeq(driver.fwdIterator()).mapIt(it[1]) - - test "Forward pagination - normal pagination": - ## Given - let - pageSize: uint = 2 - cursor: Option[Index] = some(indexList[3]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 2 - data == msgList[4 .. 5] - - test "Forward pagination - initial pagination request with an empty cursor": - ## Given - let - pageSize: uint = 2 - cursor: Option[Index] = none(Index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 2 - data == msgList[0 .. 1] - - test "Forward pagination - initial pagination request with an empty cursor to fetch the entire history": - ## Given - let - pageSize: uint = 13 - cursor: Option[Index] = none(Index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 10 - data == msgList[0 .. 9] - - test "Forward pagination - empty msgList": - ## Given - let driver = getTestQueueDriver(0) - let - pageSize: uint = 2 - cursor: Option[Index] = none(Index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Forward pagination - page size larger than the remaining messages": - ## Given - let - pageSize: uint = 10 - cursor: Option[Index] = some(indexList[3]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 6 - data == msgList[4 .. 9] - - test "Forward pagination - page size larger than the maximum allowed page size": - ## Given - let - pageSize: uint = MaxPageSize + 1 - cursor: Option[Index] = some(indexList[3]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - uint(data.len) <= MaxPageSize - - test "Forward pagination - cursor pointing to the end of the message list": - ## Given - let - pageSize: uint = 10 - cursor: Option[Index] = some(indexList[9]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Forward pagination - invalid cursor": - ## Given - let msg = fakeWakuMessage(payload = @[byte 10]) - let index = ArchiveCursor( - pubsubTopic: DefaultPubsubTopic, - senderTime: msg.timestamp, - storeTime: msg.timestamp, - digest: computeDigest(msg), - ).toIndex() - - let - pageSize: uint = 10 - cursor: Option[Index] = some(index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let error = page.tryError() - check: - error == QueueDriverErrorKind.INVALID_CURSOR - - test "Forward pagination - initial paging query over a message list with one message": - ## Given - let driver = getTestQueueDriver(1) - let - pageSize: uint = 10 - cursor: Option[Index] = none(Index) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 1 - - test "Forward pagination - pagination over a message list with one message": - ## Given - let driver = getTestQueueDriver(1) - let - pageSize: uint = 10 - cursor: Option[Index] = some(indexList[0]) - forward: bool = true - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Forward pagination - with pradicate": - ## Given - let - pageSize: uint = 3 - cursor: Option[Index] = none(Index) - forward = true - - proc onlyEvenTimes(index: Index, msg: WakuMessage): bool = - msg.timestamp.int64 mod 2 == 0 - - ## When - let page = driver.getPage( - pageSize = pageSize, forward = forward, cursor = cursor, predicate = onlyEvenTimes - ) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.mapIt(it.timestamp.int) == @[0, 2, 4] - - test "Backward pagination - normal pagination": - ## Given - let - pageSize: uint = 2 - cursor: Option[Index] = some(indexList[3]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data == msgList[1 .. 2].reversed - - test "Backward pagination - empty msgList": - ## Given - let driver = getTestQueueDriver(0) - let - pageSize: uint = 2 - cursor: Option[Index] = none(Index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Backward pagination - initial pagination request with an empty cursor": - ## Given - let - pageSize: uint = 2 - cursor: Option[Index] = none(Index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 2 - data == msgList[8 .. 9].reversed - - test "Backward pagination - initial pagination request with an empty cursor to fetch the entire history": - ## Given - let - pageSize: uint = 13 - cursor: Option[Index] = none(Index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 10 - data == msgList[0 .. 9].reversed - - test "Backward pagination - page size larger than the remaining messages": - ## Given - let - pageSize: uint = 5 - cursor: Option[Index] = some(indexList[3]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data == msgList[0 .. 2].reversed - - test "Backward pagination - page size larger than the Maximum allowed page size": - ## Given - let - pageSize: uint = MaxPageSize + 1 - cursor: Option[Index] = some(indexList[3]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - uint(data.len) <= MaxPageSize - - test "Backward pagination - cursor pointing to the begining of the message list": - ## Given - let - pageSize: uint = 5 - cursor: Option[Index] = some(indexList[0]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Backward pagination - invalid cursor": - ## Given - let msg = fakeWakuMessage(payload = @[byte 10]) - let index = ArchiveCursor( - pubsubTopic: DefaultPubsubTopic, - senderTime: msg.timestamp, - storeTime: msg.timestamp, - digest: computeDigest(msg), - ).toIndex() - - let - pageSize: uint = 2 - cursor: Option[Index] = some(index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let error = page.tryError() - check: - error == QueueDriverErrorKind.INVALID_CURSOR - - test "Backward pagination - initial paging query over a message list with one message": - ## Given - let driver = getTestQueueDriver(1) - let - pageSize: uint = 10 - cursor: Option[Index] = none(Index) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 1 - - test "Backward pagination - paging query over a message list with one message": - ## Given - let driver = getTestQueueDriver(1) - let - pageSize: uint = 10 - cursor: Option[Index] = some(indexList[0]) - forward: bool = false - - ## When - let page = driver.getPage(pageSize = pageSize, forward = forward, cursor = cursor) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.len == 0 - - test "Backward pagination - with predicate": - ## Given - let - pageSize: uint = 3 - cursor: Option[Index] = none(Index) - forward = false - - proc onlyOddTimes(index: Index, msg: WakuMessage): bool = - msg.timestamp.int64 mod 2 != 0 - - ## When - let page = driver.getPage( - pageSize = pageSize, forward = forward, cursor = cursor, predicate = onlyOddTimes - ) - - ## Then - let data = page.tryGet().mapIt(it[1]) - check: - data.mapIt(it.timestamp.int) == @[5, 7, 9].reversed diff --git a/tests/waku_archive_legacy/test_driver_queue_query.nim b/tests/waku_archive_legacy/test_driver_queue_query.nim deleted file mode 100644 index 6ebe5963a..000000000 --- a/tests/waku_archive_legacy/test_driver_queue_query.nim +++ /dev/null @@ -1,1795 +0,0 @@ -{.used.} - -import - std/[options, sequtils, random, algorithm], testutils/unittests, chronos, chronicles -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/queue_driver, - waku/waku_core, - waku/waku_core/message/digest, - ../testlib/common, - ../testlib/wakucore - -logScope: - topics = "test archive queue_driver" - -# Initialize the random number generator -common.randomize() - -proc newTestSqliteDriver(): ArchiveDriver = - QueueDriver.new(capacity = 50) - -proc computeTestCursor(pubsubTopic: PubsubTopic, message: WakuMessage): ArchiveCursor = - ArchiveCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - hash: computeMessageHash(pubsubTopic, message), - ) - -suite "Queue driver - query by content topic": - test "no content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages(maxPageSize = 5, ascendingOrder = true) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "single content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "single content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = false - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[6 .. 7].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "multiple content topic": - ## Given - const contentTopic1 = "test-content-topic-1" - const contentTopic2 = "test-content-topic-2" - const contentTopic3 = "test-content-topic-3" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "single content topic - no results": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "content topic and max page size - not enough messages stored": - ## Given - const pageSize: uint = 50 - - let driver = newTestSqliteDriver() - - for t in 0 ..< 40: - let msg = fakeWakuMessage(@[byte t], DefaultContentTopic, ts = ts(t)) - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[DefaultContentTopic], - maxPageSize = pageSize, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 40 - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - -suite "SQLite driver - query by pubsub topic": - test "pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - pubsubTopic = some(pubsubTopic), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "no pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages(maxPageSize = 2, ascendingOrder = true) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[0 .. 1] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "content topic and pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - -suite "Queue driver - query by cursor": - test "only cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = waitFor driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "only cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = waitFor driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = false - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "only cursor - invalid": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let fakeCursor = computeMessageHash(DefaultPubsubTopic, fakeWakuMessage()) - let cursor = ArchiveCursor(hash: fakeCursor) - - ## When - let res = waitFor driver.getMessages( - includeData = true, - contentTopic = @[DefaultContentTopic], - pubsubTopic = none(PubsubTopic), - cursor = some(cursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = @[], - maxPageSize = 5, - ascendingOrder = true, - ) - - ## Then - check: - res.isErr() - res.error == "invalid_cursor" - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 5].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[5][0], expected[5][1]) - - ## When - let res = waitFor driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[6 .. 7] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[6][0], expected[6][1]) - - ## When - let res = waitFor driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - -suite "Queue driver - query by time range": - test "start time only": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - startTime = some(ts(15, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "end time only": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - endTime = some(ts(45, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "start time and end time": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - startTime = some(ts(15, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[2 .. 4] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "invalid time range - no results": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(45, timeOrigin)), - endTime = some(ts(15, timeOrigin)), - maxPageSize = 2, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - asynctest "time range start and content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - test "time range start and content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - asynctest "time range start, single content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[3]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[4 .. 9] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asynctest "time range start, single content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - let retFut = await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[3 .. 4].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - test "time range, content topic, pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(DefaultPubsubTopic, expected[1][1]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(0, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[3 .. 4] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "time range, content topic, pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[7][0], expected[7][1]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[1][0], expected[1][1]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range, descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newTestSqliteDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - let retFut = waitFor driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - require retFut.isOk() - - let cursor = computeTestCursor(expected[1][0], expected[1][1]) - - ## When - let res = waitFor driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (waitFor driver.close()).expect("driver to close") diff --git a/tests/waku_archive_legacy/test_driver_sqlite.nim b/tests/waku_archive_legacy/test_driver_sqlite.nim deleted file mode 100644 index 9d8c4d14b..000000000 --- a/tests/waku_archive_legacy/test_driver_sqlite.nim +++ /dev/null @@ -1,58 +0,0 @@ -{.used.} - -import std/sequtils, testutils/unittests, chronos -import - waku/waku_archive_legacy, - waku/waku_archive_legacy/driver/sqlite_driver, - waku/waku_core, - ../waku_archive_legacy/archive_utils, - ../testlib/wakucore - -suite "SQLite driver": - test "init driver and database": - ## Given - let database = newSqliteDatabase() - - ## When - let driverRes = SqliteDriver.new(database) - - ## Then - check: - driverRes.isOk() - - let driver: ArchiveDriver = driverRes.tryGet() - check: - not driver.isNil() - - ## Cleanup - (waitFor driver.close()).expect("driver to close") - - test "insert a message": - ## Given - const contentTopic = "test-content-topic" - const meta = "test meta" - - let driver = newSqliteArchiveDriver() - - let msg = fakeWakuMessage(contentTopic = contentTopic, meta = meta) - let msgHash = computeMessageHash(DefaultPubsubTopic, msg) - - ## When - let putRes = waitFor driver.put( - DefaultPubsubTopic, msg, computeDigest(msg), msgHash, msg.timestamp - ) - - ## Then - check: - putRes.isOk() - - let storedMsg = (waitFor driver.getAllMessages()).tryGet() - check: - storedMsg.len == 1 - storedMsg.all do(item: auto) -> bool: - let (pubsubTopic, actualMsg, _, _, hash) = item - actualMsg.contentTopic == contentTopic and pubsubTopic == DefaultPubsubTopic and - hash == msgHash and msg.meta == actualMsg.meta - - ## Cleanup - (waitFor driver.close()).expect("driver to close") diff --git a/tests/waku_archive_legacy/test_driver_sqlite_query.nim b/tests/waku_archive_legacy/test_driver_sqlite_query.nim deleted file mode 100644 index 4143decf6..000000000 --- a/tests/waku_archive_legacy/test_driver_sqlite_query.nim +++ /dev/null @@ -1,1873 +0,0 @@ -{.used.} - -import - std/[options, sequtils, random, algorithm], testutils/unittests, chronos, chronicles - -import - waku/waku_archive_legacy, - waku/waku_core, - waku/waku_core/message/digest, - ../testlib/common, - ../testlib/wakucore, - ../waku_archive_legacy/archive_utils - -logScope: - topics = "test archive _driver" - -# Initialize the random number generator -common.randomize() - -suite "SQLite driver - query by content topic": - asyncTest "no content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages(maxPageSize = 5, ascendingOrder = true) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "single content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "single content topic with meta field": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00), meta = "meta-0"), - fakeWakuMessage(@[byte 1], ts = ts(10), meta = "meta-1"), - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20), meta = "meta-2" - ), - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30), meta = "meta-3" - ), - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40), meta = "meta-4" - ), - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50), meta = "meta-5" - ), - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60), meta = "meta-6" - ), - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70), meta = "meta-7" - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "single content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = false - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[6 .. 7].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "multiple content topic": - ## Given - const contentTopic1 = "test-content-topic-1" - const contentTopic2 = "test-content-topic-2" - const contentTopic3 = "test-content-topic-3" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic1, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic2, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic3, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic1, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic2, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic3, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic1, contentTopic2], - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "single content topic - no results": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = DefaultContentTopic, ts = ts(00)), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic, ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic, ts = ts(40)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "content topic and max page size - not enough messages stored": - ## Given - const pageSize: uint = 50 - - let driver = newSqliteArchiveDriver() - - for t in 0 ..< 40: - let msg = fakeWakuMessage(@[byte t], DefaultContentTopic, ts = ts(t)) - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[DefaultContentTopic], - maxPageSize = pageSize, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 40 - - ## Cleanup - (await driver.close()).expect("driver to close") - -suite "SQLite driver - query by pubsub topic": - asyncTest "pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "no pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages(maxPageSize = 2, ascendingOrder = true) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[0 .. 1] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "content topic and pubsub topic": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10))), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - ), - ( - pubsubTopic, - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - maxPageSize = 2, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (await driver.close()).expect("driver to close") - -suite "SQLite driver - query by cursor": - asyncTest "only cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "only cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - cursor = some(cursor), maxPageSize = 2, ascendingOrder = false - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 3].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "only cursor - invalid": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - var messages = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let fakeCursor = computeMessageHash(DefaultPubsubTopic, fakeWakuMessage()) - let cursor = ArchiveCursor(hash: fakeCursor) - - ## When - let res = await driver.getMessages( - includeData = true, - contentTopic = @[DefaultContentTopic], - pubsubTopic = none(PubsubTopic), - cursor = some(cursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = @[], - maxPageSize = 5, - ascendingOrder = true, - ) - - ## Then - check: - res.isErr() - res.error == "cursor not found" - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - # << cursor - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - fakeWakuMessage(@[byte 7], ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[4]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[5 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let expected = - @[ - fakeWakuMessage(@[byte 0], ts = ts(00)), - fakeWakuMessage(@[byte 1], ts = ts(10)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 5].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[5][0], expected[5][1]) - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[6 .. 7] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), # << cursor - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[6][0], expected[6][1]) - - ## When - let res = await driver.getMessages( - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - -suite "SQLite driver - query by time range": - asyncTest "start time only": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - startTime = some(ts(15, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "end time only": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - endTime = some(ts(45, timeOrigin)), maxPageSize = 10, ascendingOrder = true - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[0 .. 4] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "start time and end time": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # start_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - ## When - let res = await driver.getMessages( - startTime = some(ts(15, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[2 .. 4] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "invalid time range - no results": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - # end_time - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(45, timeOrigin)), - endTime = some(ts(15, timeOrigin)), - maxPageSize = 2, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range start and content topic": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range start and content topic - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[2 .. 6].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range start, single content topic and cursor": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[3]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[4 .. 9] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range start, single content topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - fakeWakuMessage(@[byte 0], contentTopic = contentTopic, ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 1], contentTopic = contentTopic, ts = ts(10, timeOrigin)), - # start_time - fakeWakuMessage(@[byte 2], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 5], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 6], contentTopic = contentTopic, ts = ts(60, timeOrigin)), - # << cursor - fakeWakuMessage(@[byte 7], contentTopic = contentTopic, ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin)), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", sequence = messages.mapIt(it.payload) - - for msg in messages: - require ( - await driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[6]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - cursor = some(cursor), - startTime = some(ts(15, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expected[3 .. 4].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range, content topic, pubsub topic and cursor": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - # start_time - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - # end_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(DefaultPubsubTopic, expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(0, timeOrigin)), - endTime = some(ts(45, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[3 .. 4] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range, content topic, pubsub topic and cursor - descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[7][0], expected[7][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5].reversed() - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[1][0], expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = true, - ) - - ## Then - check: - res.isOk() - - let expectedMessages = expected.mapIt(it[1]) - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages == expectedMessages[4 .. 5] - - ## Cleanup - (await driver.close()).expect("driver to close") - - asyncTest "time range, content topic, pubsub topic and cursor - cursor timestamp out of time range, descending order": - ## Given - const contentTopic = "test-content-topic" - const pubsubTopic = "test-pubsub-topic" - - let driver = newSqliteArchiveDriver() - - let timeOrigin = now() - let expected = - @[ - (DefaultPubsubTopic, fakeWakuMessage(@[byte 0], ts = ts(00, timeOrigin))), - (DefaultPubsubTopic, fakeWakuMessage(@[byte 1], ts = ts(10, timeOrigin))), - # << cursor - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 2], contentTopic = contentTopic, ts = ts(20, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 3], contentTopic = contentTopic, ts = ts(30, timeOrigin) - ), - ), - # start_time - ( - pubsubTopic, - fakeWakuMessage( - @[byte 4], contentTopic = contentTopic, ts = ts(40, timeOrigin) - ), - ), - ( - pubsubTopic, - fakeWakuMessage( - @[byte 5], contentTopic = contentTopic, ts = ts(50, timeOrigin) - ), - ), - (pubsubTopic, fakeWakuMessage(@[byte 6], ts = ts(60, timeOrigin))), - (pubsubTopic, fakeWakuMessage(@[byte 7], ts = ts(70, timeOrigin))), - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 8], contentTopic = contentTopic, ts = ts(80, timeOrigin) - ), - ), - # end_time - ( - DefaultPubsubTopic, - fakeWakuMessage( - @[byte 9], contentTopic = contentTopic, ts = ts(90, timeOrigin) - ), - ), - ] - var messages = expected - - shuffle(messages) - info "randomized message insertion sequence", - sequence = messages.mapIt(it[1].payload) - - for row in messages: - let (topic, msg) = row - require ( - await driver.put( - topic, msg, computeDigest(msg), computeMessageHash(topic, msg), msg.timestamp - ) - ).isOk() - - let cursor = computeArchiveCursor(expected[1][0], expected[1][1]) - - ## When - let res = await driver.getMessages( - contentTopic = @[contentTopic], - pubsubTopic = some(pubsubTopic), - cursor = some(cursor), - startTime = some(ts(35, timeOrigin)), - endTime = some(ts(85, timeOrigin)), - maxPageSize = 10, - ascendingOrder = false, - ) - - ## Then - check: - res.isOk() - - let filteredMessages = res.tryGet().mapIt(it[1]) - check: - filteredMessages.len == 0 - - ## Cleanup - (await driver.close()).expect("driver to close") diff --git a/tests/waku_archive_legacy/test_waku_archive.nim b/tests/waku_archive_legacy/test_waku_archive.nim deleted file mode 100644 index e58b2cfc9..000000000 --- a/tests/waku_archive_legacy/test_waku_archive.nim +++ /dev/null @@ -1,535 +0,0 @@ -{.used.} - -import std/[options, sequtils], testutils/unittests, chronos, libp2p/crypto/crypto - -import - waku/common/paging, - waku/waku_core, - waku/waku_core/message/digest, - waku/waku_archive_legacy, - ../waku_archive_legacy/archive_utils, - ../testlib/wakucore - -suite "Waku Archive - message handling": - test "it should archive a valid and non-ephemeral message": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let validSenderTime = now() - let message = fakeWakuMessage(ephemeral = false, ts = validSenderTime) - - ## When - waitFor archive.handleMessage(DefaultPubSubTopic, message) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 1 - - test "it should not archive ephemeral messages": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let msgList = - @[ - fakeWakuMessage(ephemeral = false, payload = "1"), - fakeWakuMessage(ephemeral = true, payload = "2"), - fakeWakuMessage(ephemeral = true, payload = "3"), - fakeWakuMessage(ephemeral = true, payload = "4"), - fakeWakuMessage(ephemeral = false, payload = "5"), - ] - - ## When - for msg in msgList: - waitFor archive.handleMessage(DefaultPubsubTopic, msg) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 2 - - test "it should archive a message with no sender timestamp": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let invalidSenderTime = 0 - let message = fakeWakuMessage(ts = invalidSenderTime) - - ## When - waitFor archive.handleMessage(DefaultPubSubTopic, message) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 1 - - test "it should not archive a message with a sender time variance greater than max time variance (future)": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let - now = now() - invalidSenderTime = now + MaxMessageTimestampVariance + 1_000_000_000 - # 1 second over the max variance - - let message = fakeWakuMessage(ts = invalidSenderTime) - - ## When - waitFor archive.handleMessage(DefaultPubSubTopic, message) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 0 - - test "it should not archive a message with a sender time variance greater than max time variance (past)": - ## Setup - let driver = newSqliteArchiveDriver() - let archive = newWakuArchive(driver) - - ## Given - let - now = now() - invalidSenderTime = now - MaxMessageTimestampVariance - 1 - - let message = fakeWakuMessage(ts = invalidSenderTime) - - ## When - waitFor archive.handleMessage(DefaultPubSubTopic, message) - - ## Then - check: - (waitFor driver.getMessagesCount()).tryGet() == 0 - -procSuite "Waku Archive - find messages": - ## Fixtures - let timeOrigin = now() - let msgListA = - @[ - fakeWakuMessage( - @[byte 00], contentTopic = ContentTopic("2"), ts = ts(00, timeOrigin) - ), - fakeWakuMessage( - @[byte 01], contentTopic = ContentTopic("1"), ts = ts(10, timeOrigin) - ), - fakeWakuMessage( - @[byte 02], contentTopic = ContentTopic("2"), ts = ts(20, timeOrigin) - ), - fakeWakuMessage( - @[byte 03], contentTopic = ContentTopic("1"), ts = ts(30, timeOrigin) - ), - fakeWakuMessage( - @[byte 04], contentTopic = ContentTopic("2"), ts = ts(40, timeOrigin) - ), - fakeWakuMessage( - @[byte 05], contentTopic = ContentTopic("1"), ts = ts(50, timeOrigin) - ), - fakeWakuMessage( - @[byte 06], contentTopic = ContentTopic("2"), ts = ts(60, timeOrigin) - ), - fakeWakuMessage( - @[byte 07], contentTopic = ContentTopic("1"), ts = ts(70, timeOrigin) - ), - fakeWakuMessage( - @[byte 08], contentTopic = ContentTopic("2"), ts = ts(80, timeOrigin) - ), - fakeWakuMessage( - @[byte 09], contentTopic = ContentTopic("1"), ts = ts(90, timeOrigin) - ), - ] - - let archiveA = block: - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - for msg in msgListA: - require ( - waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - archive - - test "handle query": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let topic = ContentTopic("1") - let - msg1 = fakeWakuMessage(contentTopic = topic) - msg2 = fakeWakuMessage() - - waitFor archive.handleMessage("foo", msg1) - waitFor archive.handleMessage("foo", msg2) - - ## Given - let req = ArchiveQuery(includeData: true, contentTopics: @[topic]) - - ## When - let queryRes = waitFor archive.findMessages(req) - - ## Then - check: - queryRes.isOk() - - let response = queryRes.tryGet() - check: - response.messages.len == 1 - response.messages == @[msg1] - - test "handle query with multiple content filters": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let - topic1 = ContentTopic("1") - topic2 = ContentTopic("2") - topic3 = ContentTopic("3") - - let - msg1 = fakeWakuMessage(contentTopic = topic1) - msg2 = fakeWakuMessage(contentTopic = topic2) - msg3 = fakeWakuMessage(contentTopic = topic3) - - waitFor archive.handleMessage("foo", msg1) - waitFor archive.handleMessage("foo", msg2) - waitFor archive.handleMessage("foo", msg3) - - ## Given - let req = ArchiveQuery(includeData: true, contentTopics: @[topic1, topic3]) - - ## When - let queryRes = waitFor archive.findMessages(req) - - ## Then - check: - queryRes.isOk() - - let response = queryRes.tryGet() - check: - response.messages.len() == 2 - response.messages.anyIt(it == msg1) - response.messages.anyIt(it == msg3) - - test "handle query with more than 10 content filters": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let queryTopics = toSeq(1 .. 15).mapIt(ContentTopic($it)) - - ## Given - let req = ArchiveQuery(contentTopics: queryTopics) - - ## When - let queryRes = waitFor archive.findMessages(req) - - ## Then - check: - queryRes.isErr() - - let error = queryRes.tryError() - check: - error.kind == ArchiveErrorKind.INVALID_QUERY - error.cause == "too many content topics" - - test "handle query with pubsub topic filter": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let - pubsubTopic1 = "queried-topic" - pubsubTopic2 = "non-queried-topic" - - let - contentTopic1 = ContentTopic("1") - contentTopic2 = ContentTopic("2") - contentTopic3 = ContentTopic("3") - - let - msg1 = fakeWakuMessage(contentTopic = contentTopic1) - msg2 = fakeWakuMessage(contentTopic = contentTopic2) - msg3 = fakeWakuMessage(contentTopic = contentTopic3) - - waitFor archive.handleMessage(pubsubtopic1, msg1) - waitFor archive.handleMessage(pubsubtopic2, msg2) - waitFor archive.handleMessage(pubsubtopic2, msg3) - - ## Given - # This query targets: pubsubtopic1 AND (contentTopic1 OR contentTopic3) - let req = ArchiveQuery( - includeData: true, - pubsubTopic: some(pubsubTopic1), - contentTopics: @[contentTopic1, contentTopic3], - ) - - ## When - let queryRes = waitFor archive.findMessages(req) - - ## Then - check: - queryRes.isOk() - - let response = queryRes.tryGet() - check: - response.messages.len() == 1 - response.messages.anyIt(it == msg1) - - test "handle query with pubsub topic filter - no match": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let - pubsubtopic1 = "queried-topic" - pubsubtopic2 = "non-queried-topic" - - let - msg1 = fakeWakuMessage() - msg2 = fakeWakuMessage() - msg3 = fakeWakuMessage() - - waitFor archive.handleMessage(pubsubtopic2, msg1) - waitFor archive.handleMessage(pubsubtopic2, msg2) - waitFor archive.handleMessage(pubsubtopic2, msg3) - - ## Given - let req = ArchiveQuery(pubsubTopic: some(pubsubTopic1)) - - ## When - let res = waitFor archive.findMessages(req) - - ## Then - check: - res.isOk() - - let response = res.tryGet() - check: - response.messages.len() == 0 - - test "handle query with pubsub topic filter - match the entire stored messages": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let pubsubTopic = "queried-topic" - - let - msg1 = fakeWakuMessage(payload = "TEST-1") - msg2 = fakeWakuMessage(payload = "TEST-2") - msg3 = fakeWakuMessage(payload = "TEST-3") - - waitFor archive.handleMessage(pubsubTopic, msg1) - waitFor archive.handleMessage(pubsubTopic, msg2) - waitFor archive.handleMessage(pubsubTopic, msg3) - - ## Given - let req = ArchiveQuery(includeData: true, pubsubTopic: some(pubsubTopic)) - - ## When - let res = waitFor archive.findMessages(req) - - ## Then - check: - res.isOk() - - let response = res.tryGet() - check: - response.messages.len() == 3 - response.messages.anyIt(it == msg1) - response.messages.anyIt(it == msg2) - response.messages.anyIt(it == msg3) - - test "handle query with forward pagination": - ## Given - let req = - ArchiveQuery(includeData: true, pageSize: 4, direction: PagingDirection.FORWARD) - - ## When - var nextReq = req # copy - - var pages = newSeq[seq[WakuMessage]](3) - var cursors = newSeq[Option[ArchiveCursor]](3) - - for i in 0 ..< 3: - let res = waitFor archiveA.findMessages(nextReq) - require res.isOk() - - # Keep query response content - let response = res.get() - pages[i] = response.messages - cursors[i] = response.cursor - - # Set/update the request cursor - nextReq.cursor = cursors[i] - - ## Then - check: - cursors[0] == some(computeArchiveCursor(DefaultPubsubTopic, msgListA[3])) - cursors[1] == some(computeArchiveCursor(DefaultPubsubTopic, msgListA[7])) - cursors[2] == none(ArchiveCursor) - - check: - pages[0] == msgListA[0 .. 3] - pages[1] == msgListA[4 .. 7] - pages[2] == msgListA[8 .. 9] - - test "handle query with backward pagination": - ## Given - let req = - ArchiveQuery(includeData: true, pageSize: 4, direction: PagingDirection.BACKWARD) - - ## When - var nextReq = req # copy - - var pages = newSeq[seq[WakuMessage]](3) - var cursors = newSeq[Option[ArchiveCursor]](3) - - for i in 0 ..< 3: - let res = waitFor archiveA.findMessages(nextReq) - require res.isOk() - - # Keep query response content - let response = res.get() - pages[i] = response.messages - cursors[i] = response.cursor - - # Set/update the request cursor - nextReq.cursor = cursors[i] - - ## Then - check: - cursors[0] == some(computeArchiveCursor(DefaultPubsubTopic, msgListA[6])) - cursors[1] == some(computeArchiveCursor(DefaultPubsubTopic, msgListA[2])) - cursors[2] == none(ArchiveCursor) - - check: - pages[0] == msgListA[6 .. 9] - pages[1] == msgListA[2 .. 5] - pages[2] == msgListA[0 .. 1] - - test "handle query with no paging info - auto-pagination": - ## Setup - let - driver = newSqliteArchiveDriver() - archive = newWakuArchive(driver) - - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2")), - fakeWakuMessage(@[byte 1], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 2], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 3], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 4], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 5], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 6], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 7], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 8], contentTopic = DefaultContentTopic), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2")), - ] - - for msg in msgList: - require ( - waitFor driver.put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - ).isOk() - - ## Given - let req = ArchiveQuery(includeData: true, contentTopics: @[DefaultContentTopic]) - - ## When - let res = waitFor archive.findMessages(req) - - ## Then - check: - res.isOk() - - let response = res.tryGet() - check: - ## No pagination specified. Response will be auto-paginated with - ## up to MaxPageSize messages per page. - response.messages.len() == 8 - response.cursor.isNone() - - test "handle temporal history query with a valid time window": - ## Given - let req = ArchiveQuery( - includeData: true, - contentTopics: @[ContentTopic("1")], - startTime: some(ts(15, timeOrigin)), - endTime: some(ts(55, timeOrigin)), - direction: PagingDirection.FORWARD, - ) - - ## When - let res = waitFor archiveA.findMessages(req) - - ## Then - check res.isOk() - - let response = res.tryGet() - check: - response.messages.len() == 2 - response.messages.mapIt(it.timestamp) == @[ts(30, timeOrigin), ts(50, timeOrigin)] - - test "handle temporal history query with a zero-size time window": - ## A zero-size window results in an empty list of history messages - ## Given - let req = ArchiveQuery( - contentTopics: @[ContentTopic("1")], - startTime: some(Timestamp(2)), - endTime: some(Timestamp(2)), - ) - - ## When - let res = waitFor archiveA.findMessages(req) - - ## Then - check res.isOk() - - let response = res.tryGet() - check: - response.messages.len == 0 - - test "handle temporal history query with an invalid time window": - ## A history query with an invalid time range results in an empty list of history messages - ## Given - let req = ArchiveQuery( - contentTopics: @[ContentTopic("1")], - startTime: some(Timestamp(5)), - endTime: some(Timestamp(2)), - ) - - ## When - let res = waitFor archiveA.findMessages(req) - - ## Then - check res.isOk() - - let response = res.tryGet() - check: - response.messages.len == 0 diff --git a/tests/waku_core/test_message_digest.nim b/tests/waku_core/test_message_digest.nim index 1d1f71225..22a10d84d 100644 --- a/tests/waku_core/test_message_digest.nim +++ b/tests/waku_core/test_message_digest.nim @@ -35,7 +35,7 @@ suite "Waku Message - Deterministic hashing": byteutils.toHex(message.payload) == "010203045445535405060708" byteutils.toHex(message.meta) == "" byteutils.toHex(toBytesBE(uint64(message.timestamp))) == "175789bfa23f8400" - messageHash.toHex() == + byteutils.toHex(messageHash) == "cccab07fed94181c83937c8ca8340c9108492b7ede354a6d95421ad34141fd37" test "digest computation - meta field (12 bytes)": @@ -69,7 +69,7 @@ suite "Waku Message - Deterministic hashing": byteutils.toHex(message.payload) == "010203045445535405060708" byteutils.toHex(message.meta) == "73757065722d736563726574" byteutils.toHex(toBytesBE(uint64(message.timestamp))) == "175789bfa23f8400" - messageHash.toHex() == + byteutils.toHex(messageHash) == "b9b4852f9d8c489846e8bfc6c5ca6a1a8d460a40d28832a966e029eb39619199" test "digest computation - meta field (64 bytes)": @@ -104,7 +104,7 @@ suite "Waku Message - Deterministic hashing": byteutils.toHex(message.meta) == "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f" byteutils.toHex(toBytesBE(uint64(message.timestamp))) == "175789bfa23f8400" - messageHash.toHex() == + byteutils.toHex(messageHash) == "653460d04f66c5b11814d235152f4f246e6f03ef80a305a825913636fbafd0ba" test "digest computation - zero length payload": @@ -132,7 +132,7 @@ suite "Waku Message - Deterministic hashing": ## Then check: - messageHash.toHex() == + byteutils.toHex(messageHash) == "0f6448cc23b2db6c696aa6ab4b693eff4cf3549ff346fe1dbeb281697396a09f" test "waku message - check meta size is enforced": diff --git a/tests/waku_core/test_peers.nim b/tests/waku_core/test_peers.nim index 59ae2e2f3..0ba3e5b04 100644 --- a/tests/waku_core/test_peers.nim +++ b/tests/waku_core/test_peers.nim @@ -1,5 +1,6 @@ {.used.} +import std/options import results, testutils/unittests, diff --git a/tests/waku_discv5/test_waku_discv5.nim b/tests/waku_discv5/test_waku_discv5.nim index d1cd6c46f..936c01826 100644 --- a/tests/waku_discv5/test_waku_discv5.nim +++ b/tests/waku_discv5/test_waku_discv5.nim @@ -22,7 +22,7 @@ import factory/conf_builder/conf_builder, factory/waku, node/waku_node, - node/api, + node/kernel_api, node/peer_manager, ], ../testlib/[wakucore, testasync, assertions, futures, wakunode, testutils], @@ -426,7 +426,6 @@ suite "Waku Discovery v5": confBuilder.withNodeKey(libp2p_keys.PrivateKey.random(Secp256k1, myRng[])[]) confBuilder.discv5Conf.withEnabled(true) confBuilder.discv5Conf.withUdpPort(9000.Port) - let conf = confBuilder.build().valueOr: raiseAssert error @@ -468,6 +467,9 @@ suite "Waku Discovery v5": # leave some time for discv5 to act await sleepAsync(chronos.seconds(10)) + # Connect peers via peer manager to ensure identify happens + discard await waku0.node.peerManager.connectPeer(waku1.node.switch.peerInfo) + var r = waku0.node.peerManager.selectPeer(WakuPeerExchangeCodec) assert r.isSome(), "could not retrieve peer mounting WakuPeerExchangeCodec" @@ -480,7 +482,7 @@ suite "Waku Discovery v5": r = waku2.node.peerManager.selectPeer(WakuPeerExchangeCodec) assert r.isSome(), "could not retrieve peer mounting WakuPeerExchangeCodec" - r = waku2.node.peerManager.selectPeer(RendezVousCodec) + r = waku2.node.peerManager.selectPeer(WakuRendezVousCodec) assert r.isSome(), "could not retrieve peer mounting RendezVousCodec" asyncTest "Discv5 bootstrap nodes should be added to the peer store": @@ -504,7 +506,8 @@ suite "Waku Discovery v5": waku.conf.nodeKey, waku.conf.endpointConf.p2pListenAddress, waku.conf.portsShift, - ) + ).valueOr: + raiseAssert "failed setup discv5 in test: " & $error check: waku.node.peerManager.switch.peerStore.peers().anyIt( @@ -535,7 +538,8 @@ suite "Waku Discovery v5": waku.conf.nodeKey, waku.conf.endpointConf.p2pListenAddress, waku.conf.portsShift, - ) + ).valueOr: + raiseAssert "failed setup discv5 in test: " & $error check: not waku.node.peerManager.switch.peerStore.peers().anyIt( diff --git a/tests/waku_enr/test_sharding.nim b/tests/waku_enr/test_sharding.nim index 0984b7d8d..344436d0e 100644 --- a/tests/waku_enr/test_sharding.nim +++ b/tests/waku_enr/test_sharding.nim @@ -140,14 +140,13 @@ suite "Discovery Mechanisms for Shards": test "Bit Vector Representation": # Given a valid bit vector and its representation let - bitVector: seq[byte] = - @[ - 0, 73, 2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ] + bitVector: seq[byte] = @[ + 0, 73, 2, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ] clusterId: uint16 = 73 # bitVector's clusterId shardIds: seq[uint16] = @[1u16, 10u16] # bitVector's shardIds diff --git a/tests/waku_filter_v2/test_waku_client.nim b/tests/waku_filter_v2/test_waku_client.nim index 6ae1f2902..c57699d39 100644 --- a/tests/waku_filter_v2/test_waku_client.nim +++ b/tests/waku_filter_v2/test_waku_client.nim @@ -3,7 +3,7 @@ import std/[options, sequtils, json], testutils/unittests, results, chronos import - waku/node/[peer_manager, waku_node, api], + waku/node/[peer_manager, waku_node, kernel_api], waku/waku_core, waku/waku_filter_v2/[common, client, subscriptions, protocol, rpc_codec], ../testlib/[wakucore, testasync, testutils, futures, sequtils, wakunode], diff --git a/tests/waku_filter_v2/test_waku_filter_dos_protection.nim b/tests/waku_filter_v2/test_waku_filter_dos_protection.nim index 7c8c640ba..be92fc409 100644 --- a/tests/waku_filter_v2/test_waku_filter_dos_protection.nim +++ b/tests/waku_filter_v2/test_waku_filter_dos_protection.nim @@ -122,24 +122,51 @@ suite "Waku Filter - DOS protection": check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) - await sleepAsync(20.milliseconds) - check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - none(FilterSubscribeErrorKind) + # Avoid using tiny sleeps to control refill behavior: CI scheduling can + # oversleep and mint additional tokens. Instead, issue a small burst of + # subscribe requests and require at least one TOO_MANY_REQUESTS. + var c1SubscribeFutures = newSeq[Future[FilterSubscribeResult]]() + for i in 0 ..< 6: + c1SubscribeFutures.add( + client1.wakuFilterClient.subscribe( + serverRemotePeerInfo, pubsubTopic, contentTopicSeq + ) + ) + + let c1Finished = await allFinished(c1SubscribeFutures) + var c1GotTooMany = false + for fut in c1Finished: + check not fut.failed() + let res = fut.read() + if res.isErr() and res.error().kind == FilterSubscribeErrorKind.TOO_MANY_REQUESTS: + c1GotTooMany = true + break + check c1GotTooMany + + # Ensure the other client is not affected by client1's rate limit. check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) - await sleepAsync(20.milliseconds) - check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - none(FilterSubscribeErrorKind) - await sleepAsync(20.milliseconds) - check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - some(FilterSubscribeErrorKind.TOO_MANY_REQUESTS) - check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - none(FilterSubscribeErrorKind) - check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == - some(FilterSubscribeErrorKind.TOO_MANY_REQUESTS) + + var c2SubscribeFutures = newSeq[Future[FilterSubscribeResult]]() + for i in 0 ..< 6: + c2SubscribeFutures.add( + client2.wakuFilterClient.subscribe( + serverRemotePeerInfo, pubsubTopic, contentTopicSeq + ) + ) + + let c2Finished = await allFinished(c2SubscribeFutures) + var c2GotTooMany = false + for fut in c2Finished: + check not fut.failed() + let res = fut.read() + if res.isErr() and res.error().kind == FilterSubscribeErrorKind.TOO_MANY_REQUESTS: + c2GotTooMany = true + break + check c2GotTooMany # ensure period of time has passed and clients can again use the service - await sleepAsync(1000.milliseconds) + await sleepAsync(1100.milliseconds) check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == @@ -147,29 +174,55 @@ suite "Waku Filter - DOS protection": asyncTest "Ensure normal usage allowed": # Given + # Rate limit setting is (3 requests / 1000ms) per peer. + # In a token-bucket model this means: + # - capacity = 3 tokens + # - refill rate = 3 tokens / second => ~1 token every ~333ms + # - each request consumes 1 token (including UNSUBSCRIBE) check client1.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) - await sleepAsync(500.milliseconds) - check client1.ping(serverRemotePeerInfo) == none(FilterSubscribeErrorKind) - check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) + # Expected remaining tokens (approx): 2 await sleepAsync(500.milliseconds) check client1.ping(serverRemotePeerInfo) == none(FilterSubscribeErrorKind) check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) - await sleepAsync(50.milliseconds) + # After ~500ms, ~1 token refilled; PING consumes 1 => expected remaining: 2 + + await sleepAsync(500.milliseconds) + check client1.ping(serverRemotePeerInfo) == none(FilterSubscribeErrorKind) + check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) + + # After another ~500ms, ~1 token refilled; PING consumes 1 => expected remaining: 2 + check client1.unsubscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) check wakuFilter.subscriptions.isSubscribed(client1.clientPeerId) == false - await sleepAsync(50.milliseconds) check client1.ping(serverRemotePeerInfo) == some(FilterSubscribeErrorKind.NOT_FOUND) - check client1.ping(serverRemotePeerInfo) == some(FilterSubscribeErrorKind.NOT_FOUND) - await sleepAsync(50.milliseconds) - check client1.ping(serverRemotePeerInfo) == - some(FilterSubscribeErrorKind.TOO_MANY_REQUESTS) + # After unsubscribing, PING is expected to return NOT_FOUND while still + # counting towards the rate limit. + + # CI can oversleep / schedule slowly, which can mint extra tokens between + # requests. To make the test robust, issue a small burst of pings and + # require at least one TOO_MANY_REQUESTS response. + var pingFutures = newSeq[Future[FilterSubscribeResult]]() + for i in 0 ..< 9: + pingFutures.add(client1.wakuFilterClient.ping(serverRemotePeerInfo)) + + let finished = await allFinished(pingFutures) + var gotTooMany = false + for fut in finished: + check not fut.failed() + let pingRes = fut.read() + if pingRes.isErr() and + pingRes.error().kind == FilterSubscribeErrorKind.TOO_MANY_REQUESTS: + gotTooMany = true + break + + check gotTooMany check client2.subscribe(serverRemotePeerInfo, pubsubTopic, contentTopicSeq) == none(FilterSubscribeErrorKind) diff --git a/tests/waku_lightpush/test_client.nim b/tests/waku_lightpush/test_client.nim index af22ffa5d..0bc9afdd4 100644 --- a/tests/waku_lightpush/test_client.nim +++ b/tests/waku_lightpush/test_client.nim @@ -38,7 +38,7 @@ suite "Waku Lightpush Client": asyncSetup: handlerFuture = newPushHandlerFuture() handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = let msgLen = message.encode().buffer.len if msgLen > int(DefaultMaxWakuMessageSize) + 64 * 1024: @@ -287,7 +287,7 @@ suite "Waku Lightpush Client": handlerError = "handler-error" handlerFuture2 = newFuture[void]() handler2 = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = handlerFuture2.complete() return lighpushErrorResult(LightPushErrorCode.PAYLOAD_TOO_LARGE, handlerError) diff --git a/tests/waku_lightpush/test_ratelimit.nim b/tests/waku_lightpush/test_ratelimit.nim index b2dcdc7b5..e023bf3f5 100644 --- a/tests/waku_lightpush/test_ratelimit.nim +++ b/tests/waku_lightpush/test_ratelimit.nim @@ -19,7 +19,7 @@ suite "Rate limited push service": ## Given var handlerFuture = newFuture[(string, WakuMessage)]() let handler: PushMessageHandler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = handlerFuture.complete((pubsubTopic, message)) return lightpushSuccessResult(1) # succeed to publish to 1 peer. @@ -37,7 +37,7 @@ suite "Rate limited push service": handlerFuture = newFuture[(string, WakuMessage)]() let requestRes = - await client.publish(some(DefaultPubsubTopic), message, peer = serverPeerId) + await client.publish(some(DefaultPubsubTopic), message, serverPeerId) check await handlerFuture.withTimeout(50.millis) @@ -66,7 +66,7 @@ suite "Rate limited push service": var endTime = Moment.now() var elapsed: Duration = (endTime - startTime) await sleepAsync(tokenPeriod - elapsed + firstWaitExtend) - firstWaitEXtend = 100.millis + firstWaitExtend = 100.millis ## Cleanup await allFutures(clientSwitch.stop(), serverSwitch.stop()) @@ -80,11 +80,12 @@ suite "Rate limited push service": await allFutures(serverSwitch.start(), clientSwitch.start()) ## Given - var handlerFuture = newFuture[(string, WakuMessage)]() + # Don't rely on per-request timing assumptions or a single shared Future. + # CI can be slow enough that sequential requests accidentally refill tokens. + # Instead we issue a small burst and assert we observe at least one rejection. let handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = - handlerFuture.complete((pubsubTopic, message)) return lightpushSuccessResult(1) let @@ -93,45 +94,37 @@ suite "Rate limited push service": client = newTestWakuLightpushClient(clientSwitch) let serverPeerId = serverSwitch.peerInfo.toRemotePeerInfo() - let topic = DefaultPubsubTopic + let tokenPeriod = 500.millis - let successProc = proc(): Future[void] {.async.} = + # Fire a burst of requests; require at least one success and one rejection. + var publishFutures = newSeq[Future[WakuLightPushResult]]() + for i in 0 ..< 10: let message = fakeWakuMessage() - handlerFuture = newFuture[(string, WakuMessage)]() - let requestRes = - await client.publish(some(DefaultPubsubTopic), message, peer = serverPeerId) - discard await handlerFuture.withTimeout(10.millis) + publishFutures.add( + client.publish(some(DefaultPubsubTopic), message, serverPeerId) + ) - check: - requestRes.isOk() - handlerFuture.finished() - let (handledMessagePubsubTopic, handledMessage) = handlerFuture.read() - check: - handledMessagePubsubTopic == DefaultPubsubTopic - handledMessage == message + let finished = await allFinished(publishFutures) + var gotOk = false + var gotTooMany = false + for fut in finished: + check not fut.failed() + let res = fut.read() + if res.isOk(): + gotOk = true + else: + check res.error.code == LightPushErrorCode.TOO_MANY_REQUESTS + check res.error.desc == some(TooManyRequestsMessage) + gotTooMany = true - let rejectProc = proc(): Future[void] {.async.} = - let message = fakeWakuMessage() - handlerFuture = newFuture[(string, WakuMessage)]() - let requestRes = - await client.publish(some(DefaultPubsubTopic), message, peer = serverPeerId) - discard await handlerFuture.withTimeout(10.millis) + check gotOk + check gotTooMany - check: - requestRes.isErr() - requestRes.error.code == LightPushErrorCode.TOO_MANY_REQUESTS - requestRes.error.desc == some(TooManyRequestsMessage) - - for testCnt in 0 .. 2: - await successProc() - await sleepAsync(20.millis) - - await rejectProc() - - await sleepAsync(500.millis) - - ## next one shall succeed due to the rate limit time window has passed - await successProc() + # ensure period of time has passed and the client can again use the service + await sleepAsync(tokenPeriod + 100.millis) + let recoveryRes = + await client.publish(some(DefaultPubsubTopic), fakeWakuMessage(), serverPeerId) + check recoveryRes.isOk() ## Cleanup await allFutures(clientSwitch.stop(), serverSwitch.stop()) diff --git a/tests/waku_lightpush_legacy/lightpush_utils.nim b/tests/waku_lightpush_legacy/lightpush_utils.nim index 11c4bf929..d5602173a 100644 --- a/tests/waku_lightpush_legacy/lightpush_utils.nim +++ b/tests/waku_lightpush_legacy/lightpush_utils.nim @@ -1,6 +1,9 @@ {.used.} -import std/options, chronos, libp2p/crypto/crypto +import std/options, chronos, chronicles, libp2p/crypto/crypto + +logScope: + topics = "test waku_lightpush_legacy" import waku/node/peer_manager, diff --git a/tests/waku_lightpush_legacy/test_client.nim b/tests/waku_lightpush_legacy/test_client.nim index 1dcb466c9..3d3027e9c 100644 --- a/tests/waku_lightpush_legacy/test_client.nim +++ b/tests/waku_lightpush_legacy/test_client.nim @@ -35,7 +35,7 @@ suite "Waku Legacy Lightpush Client": asyncSetup: handlerFuture = newPushHandlerFuture() handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = let msgLen = message.encode().buffer.len if msgLen > int(DefaultMaxWakuMessageSize) + 64 * 1024: @@ -282,7 +282,7 @@ suite "Waku Legacy Lightpush Client": handlerError = "handler-error" handlerFuture2 = newFuture[void]() handler2 = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = handlerFuture2.complete() return err(handlerError) diff --git a/tests/waku_lightpush_legacy/test_ratelimit.nim b/tests/waku_lightpush_legacy/test_ratelimit.nim index 3df8d369d..ae5f5ed28 100644 --- a/tests/waku_lightpush_legacy/test_ratelimit.nim +++ b/tests/waku_lightpush_legacy/test_ratelimit.nim @@ -25,7 +25,7 @@ suite "Rate limited push service": ## Given var handlerFuture = newFuture[(string, WakuMessage)]() let handler: PushMessageHandler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = handlerFuture.complete((pubsubTopic, message)) return ok() @@ -86,58 +86,52 @@ suite "Rate limited push service": await allFutures(serverSwitch.start(), clientSwitch.start()) ## Given - var handlerFuture = newFuture[(string, WakuMessage)]() let handler = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = - handlerFuture.complete((pubsubTopic, message)) return ok() let + tokenPeriod = 500.millis server = await newTestWakuLegacyLightpushNode( - serverSwitch, handler, some((3, 500.millis)) + serverSwitch, handler, some((3, tokenPeriod)) ) client = newTestWakuLegacyLightpushClient(clientSwitch) let serverPeerId = serverSwitch.peerInfo.toRemotePeerInfo() - let topic = DefaultPubsubTopic - let successProc = proc(): Future[void] {.async.} = - let message = fakeWakuMessage() - handlerFuture = newFuture[(string, WakuMessage)]() - let requestRes = - await client.publish(DefaultPubsubTopic, message, peer = serverPeerId) - discard await handlerFuture.withTimeout(10.millis) + # Avoid assuming the exact Nth request will be rejected. With Chronos TokenBucket + # minting semantics and real network latency, CI timing can allow refills. + # Instead, send a short burst and require that we observe at least one rejection. + let burstSize = 10 + var publishFutures: seq[Future[WakuLightPushResult[string]]] = @[] + for _ in 0 ..< burstSize: + publishFutures.add( + client.publish(DefaultPubsubTopic, fakeWakuMessage(), peer = serverPeerId) + ) - check: - requestRes.isOk() - handlerFuture.finished() - let (handledMessagePubsubTopic, handledMessage) = handlerFuture.read() - check: - handledMessagePubsubTopic == DefaultPubsubTopic - handledMessage == message + let finished = await allFinished(publishFutures) + var gotOk = false + var gotTooMany = false + for fut in finished: + check not fut.failed() + let res = fut.read() + if res.isOk(): + gotOk = true + elif res.error == "TOO_MANY_REQUESTS": + gotTooMany = true - let rejectProc = proc(): Future[void] {.async.} = - let message = fakeWakuMessage() - handlerFuture = newFuture[(string, WakuMessage)]() - let requestRes = - await client.publish(DefaultPubsubTopic, message, peer = serverPeerId) - discard await handlerFuture.withTimeout(10.millis) + check: + gotOk + gotTooMany - check: - requestRes.isErr() - requestRes.error == "TOO_MANY_REQUESTS" - - for testCnt in 0 .. 2: - await successProc() - await sleepAsync(20.millis) - - await rejectProc() - - await sleepAsync(500.millis) + await sleepAsync(tokenPeriod + 100.millis) ## next one shall succeed due to the rate limit time window has passed - await successProc() + let afterCooldownRes = + await client.publish(DefaultPubsubTopic, fakeWakuMessage(), peer = serverPeerId) + check: + afterCooldownRes.isOk() ## Cleanup await allFutures(clientSwitch.stop(), serverSwitch.stop()) diff --git a/tests/waku_peer_exchange/test_protocol.nim b/tests/waku_peer_exchange/test_protocol.nim index 204338a85..74cdba110 100644 --- a/tests/waku_peer_exchange/test_protocol.nim +++ b/tests/waku_peer_exchange/test_protocol.nim @@ -142,9 +142,13 @@ suite "Waku Peer Exchange": newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) node2 = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + node3 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + node4 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) # Start and mount peer exchange - await allFutures([node1.start(), node2.start()]) + await allFutures([node1.start(), node2.start(), node3.start(), node4.start()]) await allFutures([node1.mountPeerExchange(), node2.mountPeerExchangeClient()]) # Create connection @@ -154,18 +158,15 @@ suite "Waku Peer Exchange": require: connOpt.isSome - # Create some enr and add to peer exchange (simulating disv5) - var enr1, enr2 = enr.Record() - check enr1.fromUri( - "enr:-Iu4QGNuTvNRulF3A4Kb9YHiIXLr0z_CpvWkWjWKU-o95zUPR_In02AWek4nsSk7G_-YDcaT4bDRPzt5JIWvFqkXSNcBgmlkgnY0gmlwhE0WsGeJc2VjcDI1NmsxoQKp9VzU2FAh7fwOwSpg1M_Ekz4zzl0Fpbg6po2ZwgVwQYN0Y3CC6mCFd2FrdTIB" - ) - check enr2.fromUri( - "enr:-Iu4QGJllOWlviPIh_SGR-VVm55nhnBIU5L-s3ran7ARz_4oDdtJPtUs3Bc5aqZHCiPQX6qzNYF2ARHER0JPX97TFbEBgmlkgnY0gmlwhE0WsGeJc2VjcDI1NmsxoQP3ULycvday4EkvtVu0VqbBdmOkbfVLJx8fPe0lE_dRkIN0Y3CC6mCFd2FrdTIB" - ) + # Simulate node1 discovering node3 via Discv5 + var info3 = node3.peerInfo.toRemotePeerInfo() + info3.enr = some(node3.enr) + node1.peerManager.addPeer(info3, PeerOrigin.Discv5) - # Mock that we have discovered these enrs - node1.wakuPeerExchange.enrCache.add(enr1) - node1.wakuPeerExchange.enrCache.add(enr2) + # Simulate node1 discovering node4 via Discv5 + var info4 = node4.peerInfo.toRemotePeerInfo() + info4.enr = some(node4.enr) + node1.peerManager.addPeer(info4, PeerOrigin.Discv5) # Request 2 peer from px. Test all request variants let response1 = await node2.wakuPeerExchangeClient.request(2) @@ -185,12 +186,12 @@ suite "Waku Peer Exchange": response3.get().peerInfos.len == 2 # Since it can return duplicates test that at least one of the enrs is in the response - response1.get().peerInfos.anyIt(it.enr == enr1.raw) or - response1.get().peerInfos.anyIt(it.enr == enr2.raw) - response2.get().peerInfos.anyIt(it.enr == enr1.raw) or - response2.get().peerInfos.anyIt(it.enr == enr2.raw) - response3.get().peerInfos.anyIt(it.enr == enr1.raw) or - response3.get().peerInfos.anyIt(it.enr == enr2.raw) + response1.get().peerInfos.anyIt(it.enr == node3.enr.raw) or + response1.get().peerInfos.anyIt(it.enr == node4.enr.raw) + response2.get().peerInfos.anyIt(it.enr == node3.enr.raw) or + response2.get().peerInfos.anyIt(it.enr == node4.enr.raw) + response3.get().peerInfos.anyIt(it.enr == node3.enr.raw) or + response3.get().peerInfos.anyIt(it.enr == node4.enr.raw) asyncTest "Request fails gracefully": let @@ -265,8 +266,8 @@ suite "Waku Peer Exchange": peerInfo2.origin = PeerOrigin.Discv5 check: - not poolFilter(cluster, peerInfo1) - poolFilter(cluster, peerInfo2) + poolFilter(cluster, peerInfo1).isErr() + poolFilter(cluster, peerInfo2).isOk() asyncTest "Request 0 peers, with 1 peer in PeerExchange": # Given two valid nodes with PeerExchange @@ -275,9 +276,11 @@ suite "Waku Peer Exchange": newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) node2 = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + node3 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) # Start and mount peer exchange - await allFutures([node1.start(), node2.start()]) + await allFutures([node1.start(), node2.start(), node3.start()]) await allFutures([node1.mountPeerExchange(), node2.mountPeerExchangeClient()]) # Connect the nodes @@ -286,12 +289,10 @@ suite "Waku Peer Exchange": ) assert dialResponse.isSome - # Mock that we have discovered one enr - var record = enr.Record() - check record.fromUri( - "enr:-Iu4QGNuTvNRulF3A4Kb9YHiIXLr0z_CpvWkWjWKU-o95zUPR_In02AWek4nsSk7G_-YDcaT4bDRPzt5JIWvFqkXSNcBgmlkgnY0gmlwhE0WsGeJc2VjcDI1NmsxoQKp9VzU2FAh7fwOwSpg1M_Ekz4zzl0Fpbg6po2ZwgVwQYN0Y3CC6mCFd2FrdTIB" - ) - node1.wakuPeerExchange.enrCache.add(record) + # Simulate node1 discovering node3 via Discv5 + var info3 = node3.peerInfo.toRemotePeerInfo() + info3.enr = some(node3.enr) + node1.peerManager.addPeer(info3, PeerOrigin.Discv5) # When requesting 0 peers let response = await node2.wakuPeerExchangeClient.request(0) @@ -312,13 +313,6 @@ suite "Waku Peer Exchange": await allFutures([node1.start(), node2.start()]) await allFutures([node1.mountPeerExchangeClient(), node2.mountPeerExchange()]) - # Mock that we have discovered one enr - var record = enr.Record() - check record.fromUri( - "enr:-Iu4QGNuTvNRulF3A4Kb9YHiIXLr0z_CpvWkWjWKU-o95zUPR_In02AWek4nsSk7G_-YDcaT4bDRPzt5JIWvFqkXSNcBgmlkgnY0gmlwhE0WsGeJc2VjcDI1NmsxoQKp9VzU2FAh7fwOwSpg1M_Ekz4zzl0Fpbg6po2ZwgVwQYN0Y3CC6mCFd2FrdTIB" - ) - node2.wakuPeerExchange.enrCache.add(record) - # When making any request with an invalid peer info var remotePeerInfo2 = node2.peerInfo.toRemotePeerInfo() remotePeerInfo2.peerId.data.add(255.byte) @@ -362,17 +356,17 @@ suite "Waku Peer Exchange": newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) node2 = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + node3 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) # Start and mount peer exchange - await allFutures([node1.start(), node2.start()]) + await allFutures([node1.start(), node2.start(), node3.start()]) await allFutures([node1.mountPeerExchange(), node2.mountPeerExchange()]) - # Mock that we have discovered these enrs - var enr1 = enr.Record() - check enr1.fromUri( - "enr:-Iu4QGNuTvNRulF3A4Kb9YHiIXLr0z_CpvWkWjWKU-o95zUPR_In02AWek4nsSk7G_-YDcaT4bDRPzt5JIWvFqkXSNcBgmlkgnY0gmlwhE0WsGeJc2VjcDI1NmsxoQKp9VzU2FAh7fwOwSpg1M_Ekz4zzl0Fpbg6po2ZwgVwQYN0Y3CC6mCFd2FrdTIB" - ) - node1.wakuPeerExchange.enrCache.add(enr1) + # Simulate node1 discovering node3 via Discv5 + var info3 = node3.peerInfo.toRemotePeerInfo() + info3.enr = some(node3.enr) + node1.peerManager.addPeer(info3, PeerOrigin.Discv5) # Create connection let connOpt = await node2.peerManager.dialPeer( @@ -396,7 +390,7 @@ suite "Waku Peer Exchange": check: decodedBuff.get().response.status_code == PeerExchangeResponseStatusCode.SUCCESS decodedBuff.get().response.peerInfos.len == 1 - decodedBuff.get().response.peerInfos[0].enr == enr1.raw + decodedBuff.get().response.peerInfos[0].enr == node3.enr.raw asyncTest "RateLimit as expected": let @@ -404,9 +398,11 @@ suite "Waku Peer Exchange": newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) node2 = newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) + node3 = + newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) # Start and mount peer exchange - await allFutures([node1.start(), node2.start()]) + await allFutures([node1.start(), node2.start(), node3.start()]) await allFutures( [ node1.mountPeerExchange(rateLimit = (1, 150.milliseconds)), @@ -414,6 +410,11 @@ suite "Waku Peer Exchange": ] ) + # Simulate node1 discovering nodeA via Discv5 + var info3 = node3.peerInfo.toRemotePeerInfo() + info3.enr = some(node3.enr) + node1.peerManager.addPeer(info3, PeerOrigin.Discv5) + # Create connection let connOpt = await node2.peerManager.dialPeer( node1.switch.peerInfo.toRemotePeerInfo(), WakuPeerExchangeCodec @@ -421,19 +422,6 @@ suite "Waku Peer Exchange": require: connOpt.isSome - # Create some enr and add to peer exchange (simulating disv5) - var enr1, enr2 = enr.Record() - check enr1.fromUri( - "enr:-Iu4QGNuTvNRulF3A4Kb9YHiIXLr0z_CpvWkWjWKU-o95zUPR_In02AWek4nsSk7G_-YDcaT4bDRPzt5JIWvFqkXSNcBgmlkgnY0gmlwhE0WsGeJc2VjcDI1NmsxoQKp9VzU2FAh7fwOwSpg1M_Ekz4zzl0Fpbg6po2ZwgVwQYN0Y3CC6mCFd2FrdTIB" - ) - check enr2.fromUri( - "enr:-Iu4QGJllOWlviPIh_SGR-VVm55nhnBIU5L-s3ran7ARz_4oDdtJPtUs3Bc5aqZHCiPQX6qzNYF2ARHER0JPX97TFbEBgmlkgnY0gmlwhE0WsGeJc2VjcDI1NmsxoQP3ULycvday4EkvtVu0VqbBdmOkbfVLJx8fPe0lE_dRkIN0Y3CC6mCFd2FrdTIB" - ) - - # Mock that we have discovered these enrs - node1.wakuPeerExchange.enrCache.add(enr1) - node1.wakuPeerExchange.enrCache.add(enr2) - await sleepAsync(150.milliseconds) # Request 2 peer from px. Test all request variants diff --git a/tests/waku_relay/test_wakunode_relay.nim b/tests/waku_relay/test_wakunode_relay.nim index 2b4f32617..a687119bd 100644 --- a/tests/waku_relay/test_wakunode_relay.nim +++ b/tests/waku_relay/test_wakunode_relay.nim @@ -1,7 +1,7 @@ {.used.} import - std/[os, sequtils, sysrand, math], + std/[os, strutils, sequtils, sysrand, math], stew/byteutils, testutils/unittests, chronos, @@ -450,7 +450,8 @@ suite "WakuNode - Relay": await sleepAsync(500.millis) let res = await node2.publish(some($shard), message) - assert res.isOk(), $res.error + check res.isErr() + check contains($res.error, "NoPeersToPublish") await sleepAsync(500.millis) diff --git a/tests/waku_relay/utils.nim b/tests/waku_relay/utils.nim index d5703d415..069600106 100644 --- a/tests/waku_relay/utils.nim +++ b/tests/waku_relay/utils.nim @@ -8,18 +8,14 @@ import libp2p/switch, libp2p/protocols/pubsub/pubsub +import brokers/broker_context + from std/times import epochTime import - waku/ - [ - waku_relay, - node/waku_node, - node/peer_manager, - waku_core, - waku_node, - waku_rln_relay, - ], + waku/[ + waku_relay, node/waku_node, node/peer_manager, waku_core, waku_node, waku_rln_relay + ], ../waku_store/store_utils, ../waku_archive/archive_utils, ../testlib/[wakucore, futures] diff --git a/tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json.gz b/tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json.gz new file mode 100644 index 000000000..b5fdebb74 Binary files /dev/null and b/tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json.gz differ diff --git a/tests/waku_rln_relay/rln/buffer_utils.nim b/tests/waku_rln_relay/rln/buffer_utils.nim index e38cc5c17..a5ef921f1 100644 --- a/tests/waku_rln_relay/rln/buffer_utils.nim +++ b/tests/waku_rln_relay/rln/buffer_utils.nim @@ -1,11 +1,4 @@ -import waku/waku_rln_relay/rln/rln_interface - -proc `==`*(a: Buffer, b: seq[uint8]): bool = - if a.len != uint(b.len): - return false - - let bufferArray = cast[ptr UncheckedArray[uint8]](a.ptr) - for i in 0 ..< b.len: - if bufferArray[i] != b[i]: - return false - return true +# buffer_utils.nim — intentionally empty. +# The v0.9 Buffer type and toBuffer helper were removed in the zerokit v2.0.1 +# migration. This file is kept as a placeholder so that any future test imports +# do not break the build; the content that was here is no longer needed. diff --git a/tests/waku_rln_relay/rln/test_rln_interface.nim b/tests/waku_rln_relay/rln/test_rln_interface.nim index 7aedf587f..7b8ea3878 100644 --- a/tests/waku_rln_relay/rln/test_rln_interface.nim +++ b/tests/waku_rln_relay/rln/test_rln_interface.nim @@ -1,17 +1,36 @@ -import testutils/unittests +import testutils/unittests, results -import waku/waku_rln_relay/rln/rln_interface, ./buffer_utils +import waku/waku_rln_relay/rln/rln_interface +import waku/waku_rln_relay/rln/wrappers -suite "Buffer": - suite "toBuffer": +suite "Vec_uint8": + suite "toVecUint8": test "valid": # Given let bytes: seq[byte] = @[0x01, 0x02, 0x03] - # When - let buffer = bytes.toBuffer() + # When — wrap as a Vec_uint8 view then read the bytes back + var vec = toVecUint8(bytes) + let roundtrip = vecToSeq(vec) - # Then - let expectedBuffer: seq[uint8] = @[1, 2, 3] + # Then — byte values are preserved check: - buffer == expectedBuffer + roundtrip == bytes + +suite "RlnConfig": + suite "createRLNInstance": + test "ok": + # When we create the RLN instance (stateless build — no tree_depth arg) + let rlnRes = createRLNInstance() + + # Then it succeeds + check: + rlnRes.isOk() + + test "default": + # When we create the RLN instance + let rlnRes = createRLNInstance() + + # Then it succeeds + check: + rlnRes.isOk() diff --git a/tests/waku_rln_relay/rln/test_wrappers.nim b/tests/waku_rln_relay/rln/test_wrappers.nim index 29e24aae5..8cd9251c0 100644 --- a/tests/waku_rln_relay/rln/test_wrappers.nim +++ b/tests/waku_rln_relay/rln/test_wrappers.nim @@ -1,37 +1,6 @@ -import - std/options, - testutils/unittests, - chronicles, - chronos, - eth/keys, - bearssl, - stew/[results], - metrics, - metrics/chronos_httpserver +import testutils/unittests, results -import - waku/waku_rln_relay, - waku/waku_rln_relay/rln, - waku/waku_rln_relay/rln/wrappers, - ./waku_rln_relay_utils, - ../../testlib/[simple_mock, assertions], - ../../waku_keystore/utils, - ../../testlib/testutils - -from std/times import epochTime - -const Empty32Array = default(array[32, byte]) - -proc valid(x: seq[byte]): bool = - if x.len != 32: - error "Length should be 32", length = x.len - return false - - if x == Empty32Array: - error "Should not be empty array", array = x - return false - - return true +import waku/waku_rln_relay/rln, waku/waku_rln_relay/rln/wrappers, ./waku_rln_relay_utils suite "membershipKeyGen": test "ok": @@ -41,60 +10,20 @@ suite "membershipKeyGen": # Then it contains valid identity credentials let identityCredentials = identityCredentialsRes.get() + proc nonEmpty(x: seq[byte]): bool = + x.len == 32 and x != newSeq[byte](32) + check: - identityCredentials.idTrapdoor.valid() - identityCredentials.idNullifier.valid() - identityCredentials.idSecretHash.valid() - identityCredentials.idCommitment.valid() - - test "done is false": - # Given the key_gen function fails - let backup = key_gen - mock(key_gen): - proc keyGenMock(ctx: ptr RLN, output_buffer: ptr Buffer): bool = - return false - - keyGenMock - - # When we generate the membership keys - let identityCredentialsRes = membershipKeyGen() - - # Then it fails - check: - identityCredentialsRes.error() == "error in key generation" - - # Cleanup - mock(key_gen): - backup - - test "generatedKeys length is not 128": - # Given the key_gen function succeeds with wrong values - let backup = key_gen - mock(key_gen): - proc keyGenMock(ctx: ptr RLN, output_buffer: ptr Buffer): bool = - echo "# RUNNING MOCK" - output_buffer.len = 0 - output_buffer.ptr = cast[ptr uint8](newSeq[byte](0)) - return true - - keyGenMock - - # When we generate the membership keys - let identityCredentialsRes = membershipKeyGen() - - # Then it fails - check: - identityCredentialsRes.error() == "keysBuffer is of invalid length" - - # Cleanup - mock(key_gen): - backup + identityCredentials.idTrapdoor.nonEmpty() + identityCredentials.idNullifier.nonEmpty() + identityCredentials.idSecretHash.nonEmpty() + identityCredentials.idCommitment.nonEmpty() suite "RlnConfig": suite "createRLNInstance": test "ok": - # When we create the RLN instance - let rlnRes: RLNResult = createRLNInstance(15) + # When we create the RLN instance (stateless build — no tree_depth arg) + let rlnRes = createRLNInstance() # Then it succeeds check: @@ -102,30 +31,8 @@ suite "RlnConfig": test "default": # When we create the RLN instance - let rlnRes: RLNResult = createRLNInstance() + let rlnRes = createRLNInstance() # Then it succeeds check: rlnRes.isOk() - - test "new_circuit fails": - # Given the new_circuit function fails - let backup = new_circuit - mock(new_circuit): - proc newCircuitMock( - tree_height: uint, input_buffer: ptr Buffer, ctx: ptr (ptr RLN) - ): bool = - return false - - newCircuitMock - - # When we create the RLN instance - let rlnRes: RLNResult = createRLNInstance(15) - - # Then it fails - check: - rlnRes.error() == "error in parameters generation" - - # Cleanup - mock(new_circuit): - backup diff --git a/tests/waku_rln_relay/test_rln_contract_deployment.nim b/tests/waku_rln_relay/test_rln_contract_deployment.nim new file mode 100644 index 000000000..5a9624ce8 --- /dev/null +++ b/tests/waku_rln_relay/test_rln_contract_deployment.nim @@ -0,0 +1,29 @@ +{.used.} + +{.push raises: [].} + +import std/[options, os], results, testutils/unittests, chronos, web3 + +import + waku/[ + waku_rln_relay, + waku_rln_relay/conversion_utils, + waku_rln_relay/group_manager/on_chain/group_manager, + ], + ./utils_onchain + +suite "Token and RLN Contract Deployment": + test "anvil should dump state to file on exit": + # git will ignore this file, if the contract has been updated and the state file needs to be regenerated then this file can be renamed to replace the one in the repo (tests/waku_rln_relay/anvil_state/tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json) + let testStateFile = some("tests/waku_rln_relay/anvil_state/anvil_state.ignore.json") + let anvilProc = runAnvil(stateFile = testStateFile, dumpStateOnExit = true) + let manager = waitFor setupOnchainGroupManager(deployContracts = true) + + stopAnvil(anvilProc) + + check: + fileExists(testStateFile.get()) + + #The test should still pass even if thie compression fails + compressGzipFile(testStateFile.get(), testStateFile.get() & ".gz").isOkOr: + error "Failed to compress state file", error = error diff --git a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim index cf697961a..6b5b81532 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -3,7 +3,7 @@ {.push raises: [].} import - std/[options, sequtils, deques, random, locks, osproc], + std/[options, sequtils, deques, random, locks, osproc, algorithm], results, stew/byteutils, testutils/unittests, @@ -33,8 +33,8 @@ suite "Onchain group manager": var manager {.threadVar.}: OnchainGroupManager setup: - anvilProc = runAnvil() - manager = waitFor setupOnchainGroupManager() + anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH)) + manager = waitFor setupOnchainGroupManager(deployContracts = false) teardown: stopAnvil(anvilProc) @@ -74,10 +74,11 @@ suite "Onchain group manager": raiseAssert "Expected error when keystore file doesn't exist" test "trackRootChanges: should guard against uninitialized state": - try: - discard manager.trackRootChanges() - except CatchableError: - check getCurrentExceptionMsg().len == 38 + let initializedResult = waitFor manager.trackRootChanges() + + check: + initializedResult.isErr() + initializedResult.error == "OnchainGroupManager is not initialized" test "trackRootChanges: should sync to the state of the group": let credentials = generateCredentials() @@ -86,10 +87,8 @@ suite "Onchain group manager": let merkleRootBefore = waitFor manager.fetchMerkleRoot() - try: - waitFor manager.register(credentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + (waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error discard waitFor withTimeout(trackRootChanges(manager), 15.seconds) @@ -110,13 +109,11 @@ suite "Onchain group manager": let merkleRootBefore = waitFor manager.fetchMerkleRoot() - try: - for i in 0 ..< credentials.len(): - info "Registering credential", index = i, credential = credentials[i] - waitFor manager.register(credentials[i], UserMessageLimit(20)) - discard waitFor manager.updateRoots() - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + for i in 0 ..< credentials.len(): + info "Registering credential", index = i, credential = credentials[i] + (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: + assert false, "Failed to register credential " & $i & ": " & error + discard waitFor manager.updateRoots() let merkleRootAfter = waitFor manager.fetchMerkleRoot() @@ -127,16 +124,15 @@ suite "Onchain group manager": test "register: should guard against uninitialized state": let dummyCommitment = default(IDCommitment) - try: - waitFor manager.register( - RateCommitment( - idCommitment: dummyCommitment, userMessageLimit: UserMessageLimit(20) - ) + let res = waitFor manager.register( + RateCommitment( + idCommitment: dummyCommitment, userMessageLimit: UserMessageLimit(20) ) - except CatchableError: - assert true - except Exception: - assert false, "exception raised: " & getCurrentExceptionMsg() + ) + + check: + res.isErr() + res.error == "OnchainGroupManager is not initialized" test "register: should register successfully": # TODO :- similar to ```trackRootChanges: should fetch history correctly``` @@ -146,11 +142,8 @@ suite "Onchain group manager": let idCredentials = generateCredentials() let merkleRootBefore = waitFor manager.fetchMerkleRoot() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let merkleRootAfter = waitFor manager.fetchMerkleRoot() @@ -177,26 +170,25 @@ suite "Onchain group manager": manager.onRegister(callback) - try: + ( waitFor manager.register( RateCommitment( idCommitment: idCommitment, userMessageLimit: UserMessageLimit(20) ) ) - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + ).isOkOr: + assert false, "error returned when calling register: " & error waitFor fut test "withdraw: should guard against uninitialized state": let idSecretHash = generateCredentials().idSecretHash - try: - waitFor manager.withdraw(idSecretHash) - except CatchableError: - assert true - except Exception: - assert false, "exception raised: " & getCurrentExceptionMsg() + let res = waitFor manager.withdraw(idSecretHash) + + check: + res.isErr() + res.error == "OnchainGroupManager is not initialized" test "validateRoot: should validate good root": let idCredentials = generateCredentials() @@ -217,10 +209,8 @@ suite "Onchain group manager": (waitFor manager.init()).isOkOr: raiseAssert $error - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned : " & getCurrentExceptionMsg() waitFor fut @@ -263,6 +253,9 @@ suite "Onchain group manager": manager.merkleProofCache = newSeq[byte](640) for i in 0 ..< 640: manager.merkleProofCache[i] = byte(rand(255)) + # chunk[0] becomes the MSB after reversal in group_manager; must be < 0x30 + for i in 0 ..< 20: + manager.merkleProofCache[i * 32] = 0 let messageBytes = "Hello".toBytes() @@ -299,10 +292,8 @@ suite "Onchain group manager": manager.onRegister(callback) - try: - waitFor manager.register(credentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + (waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error waitFor fut let rootUpdated = waitFor manager.updateRoots() @@ -337,11 +328,8 @@ suite "Onchain group manager": let idCredential = generateCredentials() - try: - waitFor manager.register(idCredential, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling startGroupSync: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredential, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let messageBytes = "Hello".toBytes() @@ -350,6 +338,9 @@ suite "Onchain group manager": manager.merkleProofCache = newSeq[byte](640) for i in 0 ..< 640: manager.merkleProofCache[i] = byte(rand(255)) + # chunk[0] becomes the MSB after reversal in group_manager; must be < 0x30 + for i in 0 ..< 20: + manager.merkleProofCache[i * 32] = 0 let epoch = default(Epoch) info "epoch in bytes", epochHex = epoch.inHex() @@ -395,14 +386,12 @@ suite "Onchain group manager": return callback - try: - manager.onRegister(generateCallback(futures, credentials)) + manager.onRegister(generateCallback(futures, credentials)) - for i in 0 ..< credentials.len(): - waitFor manager.register(credentials[i], UserMessageLimit(20)) - discard waitFor manager.updateRoots() - except Exception, CatchableError: - assert false, "exception raised: " & getCurrentExceptionMsg() + for i in 0 ..< credentials.len(): + (waitFor manager.register(credentials[i], UserMessageLimit(20))).isOkOr: + assert false, "Failed to register credential " & $i & ": " & error + discard waitFor manager.updateRoots() waitFor allFutures(futures) @@ -436,3 +425,81 @@ suite "Onchain group manager": check: isReady == true + + test "proof roundtrip: generateRlnProofWithWitness -> verifyRlnProof": + ## Smoke test: proof gen -> wire serialize -> deserialize -> ffi_verify_with_roots. + let credentials = generateCredentials() + + (waitFor manager.init()).isOkOr: + raiseAssert $error + + (waitFor manager.register(credentials, UserMessageLimit(20))).isOkOr: + assert false, "register failed: " & error + + discard waitFor manager.updateRoots() + let roots = manager.validRoots.items().toSeq() + require: + roots.len > 0 + + let proofElements = (waitFor manager.fetchMerkleProofElements()).valueOr: + raiseAssert "fetchMerkleProofElements failed: " & error + + let signal = "Hello, RLN!".toBytes() + let epoch = default(Epoch) + + # Build RLNWitnessInput the same way group_manager.generateProof does. + var pathElements = newSeq[byte]() + for i in 0 ..< proofElements.len div 32: + pathElements.add(proofElements[i * 32 .. (i + 1) * 32 - 1].reversed()) + + let xCfr = hashToFieldLe(signal).valueOr: + raiseAssert "hashToFieldLe failed: " & error + defer: + ffi_cfr_free(xCfr) + let x = cfrToBytesLe(xCfr).valueOr: + raiseAssert "cfrToBytesLe failed: " & error + + let extNullifier = generateExternalNullifier(epoch, DefaultRlnIdentifier).valueOr: + raiseAssert "generateExternalNullifier failed: " & error + + let witness = RLNWitnessInput( + identity_secret: seqToField(credentials.idSecretHash), + user_message_limit: uint64ToField(uint64(UserMessageLimit(20))), + message_id: uint64ToField(uint64(MessageId(1))), + path_elements: pathElements, + identity_path_index: uint64ToIndex(manager.membershipIndex.get(), 20), + x: x, + external_nullifier: extNullifier, + ) + + # Step 1: generate proof via the FFI wrapper + let proof = generateRlnProofWithWitness( + manager.rlnInstance, witness, epoch, DefaultRlnIdentifier + ).valueOr: + raiseAssert "generateRlnProofWithWitness failed: " & error + + let zeroField = default(array[32, byte]) + check: + proof.merkleRoot != zeroField + proof.nullifier != zeroField + + # Step 2: serialize -> deserialize -> verify (the actual roundtrip) + let verified = verifyRlnProof(manager.rlnInstance, proof, signal, roots).valueOr: + raiseAssert "verifyRlnProof failed: " & error + check verified == true + + # Step 3: wrong signal -> x mismatch -> false + let wrongSignalVerified = verifyRlnProof( + manager.rlnInstance, proof, "wrong".toBytes(), roots + ).valueOr: + raiseAssert "verifyRlnProof (wrong signal) failed: " & error + check wrongSignalVerified == false + + # Step 4: bad root -> root not in set -> false + # byte[31] in LE is the MSB; 0x01 < 0x30 so this is a canonical field element. + var badRoot: MerkleNode + for i in 0 ..< 32: + badRoot[i] = 0x01 + let badRootVerified = verifyRlnProof(manager.rlnInstance, proof, signal, @[badRoot]).valueOr: + raiseAssert "verifyRlnProof (bad root) failed: " & error + check badRootVerified == false diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index 0bbb448e1..7694b8112 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -1,13 +1,16 @@ {.used.} import - std/[options, os, sequtils, tempfiles, strutils, osproc], + std/[options, os, sequtils, tempfiles, strutils, osproc, algorithm], stew/byteutils, testutils/unittests, chronos, chronicles, stint, libp2p/crypto/crypto + +import brokers/broker_context + import waku/[ waku_core, @@ -27,29 +30,22 @@ suite "Waku rln relay": var manager {.threadVar.}: OnchainGroupManager setup: - anvilProc = runAnvil() - manager = waitFor setupOnchainGroupManager() + anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH)) + manager = waitFor setupOnchainGroupManager(deployContracts = false) teardown: stopAnvil(anvilProc) - test "key_gen Nim Wrappers": - let merkleDepth: csize_t = 20 + test "ffi_extended_key_gen raw FFI": + # When we call the raw key-generation FFI + var vec = ffi_extended_key_gen() - # keysBufferPtr will hold the generated identity credential i.e., id trapdoor, nullifier, secret hash and commitment - var keysBuffer: Buffer - let - keysBufferPtr = addr(keysBuffer) - done = key_gen(keysBufferPtr, true) - require: - # check whether the keys are generated successfully - done - - let generatedKeys = cast[ptr array[4 * 32, byte]](keysBufferPtr.`ptr`)[] + # Then it returns exactly 4 field elements + # (idTrapdoor, idNullifier, idSecretHash, idCommitment — each 32 bytes) + defer: + ffi_vec_cfr_free(vec) check: - # the id trapdoor, nullifier, secert hash and commitment together are 4*32 bytes - generatedKeys.len == 4 * 32 - info "generated keys: ", generatedKeys + int(ffi_vec_cfr_len(addr vec)) == 4 test "membership Key Generation": let idCredentialsRes = membershipKeyGen() @@ -70,53 +66,6 @@ suite "Waku rln relay": info "the generated identity credential: ", idCredential - test "hash Nim Wrappers": - # create an RLN instance - let rlnInstance = createRLNInstanceWrapper() - require: - rlnInstance.isOk() - - # prepare the input - let - msg = "Hello".toBytes() - hashInput = encodeLengthPrefix(msg) - hashInputBuffer = toBuffer(hashInput) - - # prepare other inputs to the hash function - let outputBuffer = default(Buffer) - - let hashSuccess = sha256(unsafeAddr hashInputBuffer, unsafeAddr outputBuffer, true) - require: - hashSuccess - let outputArr = cast[ptr array[32, byte]](outputBuffer.`ptr`)[] - - check: - "1e32b3ab545c07c8b4a7ab1ca4f46bc31e4fdc29ac3b240ef1d54b4017a26e4c" == - outputArr.inHex() - - let - hashOutput = cast[ptr array[32, byte]](outputBuffer.`ptr`)[] - hashOutputHex = hashOutput.toHex() - - info "hash output", hashOutputHex - - test "sha256 hash utils": - # create an RLN instance - let rlnInstance = createRLNInstanceWrapper() - require: - rlnInstance.isOk() - let rln = rlnInstance.get() - - # prepare the input - let msg = "Hello".toBytes() - - let hashRes = sha256(msg) - - check: - hashRes.isOk() - "1e32b3ab545c07c8b4a7ab1ca4f46bc31e4fdc29ac3b240ef1d54b4017a26e4c" == - hashRes.get().inHex() - test "poseidon hash utils": # create an RLN instance let rlnInstance = createRLNInstanceWrapper() @@ -124,19 +73,22 @@ suite "Waku rln relay": rlnInstance.isOk() let rln = rlnInstance.get() - # prepare the input - let msg = - @[ - "126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc".toBytes(), - "1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1".toBytes(), - ] + # prepare the input — hex-decoded then reversed to little-endian field elements + let + left = hexToSeqByte( + "126f4c026cd731979365f79bd345a46d673c5a3f6f588bdc718e6356d02b6fdc" + ) + .reversed() + right = hexToSeqByte( + "1f0e5db2b69d599166ab16219a97b82b662085c93220382b39f9f911d3b943b1" + ) + .reversed() - let hashRes = poseidon(msg) + let hashRes = poseidon(left, right) - # Value taken from zerokit check: hashRes.isOk() - "28a15a991fe3d2a014485c7fa905074bfb55c0909112f865ded2be0a26a932c3" == + "180543bc9afb81d9c2282df9c9946f87b4596cf6d3fec2cc32b6637427685353" == hashRes.get().inHex() test "RateLimitProof Protobuf encode/init test": @@ -280,17 +232,16 @@ suite "Waku rln relay": let index = MembershipIndex(5) let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = index) - let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: - raiseAssert $error + var wakuRlnRelay: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: + raiseAssert $error let manager = cast[OnchainGroupManager](wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let epoch1 = wakuRlnRelay.getCurrentEpoch() @@ -337,17 +288,16 @@ suite "Waku rln relay": let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = index) - let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: - raiseAssert $error + var wakuRlnRelay: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: + raiseAssert $error let manager = cast[OnchainGroupManager](wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error # usually it's 20 seconds but we set it to 1 for testing purposes which make the test faster wakuRlnRelay.rlnMaxTimestampGap = 1 @@ -387,31 +337,29 @@ suite "Waku rln relay": asyncTest "multiple senders with same external nullifier": let index1 = MembershipIndex(5) let rlnConf1 = getWakuRlnConfig(manager = manager, index = index1) - let wakuRlnRelay1 = (await WakuRlnRelay.new(rlnConf1)).valueOr: - raiseAssert "failed to create waku rln relay: " & $error + var wakuRlnRelay1: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay1 = (await WakuRlnRelay.new(rlnConf1)).valueOr: + raiseAssert "failed to create waku rln relay: " & $error let manager1 = cast[OnchainGroupManager](wakuRlnRelay1.groupManager) let idCredentials1 = generateCredentials() - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error let index2 = MembershipIndex(6) let rlnConf2 = getWakuRlnConfig(manager = manager, index = index2) - let wakuRlnRelay2 = (await WakuRlnRelay.new(rlnConf2)).valueOr: - raiseAssert "failed to create waku rln relay: " & $error + var wakuRlnRelay2: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay2 = (await WakuRlnRelay.new(rlnConf2)).valueOr: + raiseAssert "failed to create waku rln relay: " & $error let manager2 = cast[OnchainGroupManager](wakuRlnRelay2.groupManager) let idCredentials2 = generateCredentials() - try: - waitFor manager2.register(idCredentials2, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager2.register(idCredentials2, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error # get the current epoch time let epoch = wakuRlnRelay1.getCurrentEpoch() @@ -495,7 +443,7 @@ suite "Waku rln relay": password = password, appInfo = RLNAppInfo, ) - .isOk() + .isOk() let readKeystoreRes = getMembershipCredentials( path = filepath, @@ -533,9 +481,10 @@ suite "Waku rln relay": let wakuRlnConfig = getWakuRlnConfig( manager = manager, index = index, epochSizeSec = rlnEpochSizeSec.uint64 ) - - let wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: - raiseAssert $error + var wakuRlnRelay: WakuRlnRelay + lockNewGlobalBrokerContext: + wakuRlnRelay = (await WakuRlnRelay.new(wakuRlnConfig)).valueOr: + raiseAssert $error let rlnMaxEpochGap = wakuRlnRelay.rlnMaxEpochGap let testProofMetadata = default(ProofMetadata) diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim index 7308ae257..414a445fa 100644 --- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim +++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim @@ -7,7 +7,9 @@ import chronicles, chronos, libp2p/switch, - libp2p/protocols/pubsub/pubsub + libp2p/protocols/pubsub/pubsub, + brokers/broker_context + import waku/[waku_core, waku_node, waku_rln_relay], ../testlib/[wakucore, futures, wakunode, testutils], @@ -30,75 +32,74 @@ procSuite "WakuNode - RLN relay": var manager {.threadVar.}: OnchainGroupManager setup: - anvilProc = runAnvil() - manager = waitFor setupOnchainGroupManager() + anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH)) + manager = waitFor setupOnchainGroupManager(deployContracts = false) teardown: stopAnvil(anvilProc) asyncTest "testing rln-relay with valid proof": - let - # publisher node - nodeKey1 = generateSecp256k1Key() + var node1, node2, node3: WakuNode # publisher node + let contentTopic = ContentTopic("/waku/2/default-content/proto") + # set up three nodes + lockNewGlobalBrokerContext: + let nodeKey1 = generateSecp256k1Key() node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + + # mount rlnrelay in off-chain mode + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() + + # Registration is mandatory before sending messages with rln-relay + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() + + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error + + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node1", rootUpdated1 + + lockNewGlobalBrokerContext: # Relay node - nodeKey2 = generateSecp256k1Key() + let nodeKey2 = generateSecp256k1Key() node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + # mount rlnrelay in off-chain mode + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() + + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node2", rootUpdated2 + + lockNewGlobalBrokerContext: # Subscriber - nodeKey3 = generateSecp256k1Key() + let nodeKey3 = generateSecp256k1Key() node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) - contentTopic = ContentTopic("/waku/2/default-content/proto") + (await node3.mountRelay()).isOkOr: + assert false, "Failed to mount relay" - # set up three nodes - # node1 - (await node1.mountRelay()).isOkOr: - assert false, "Failed to mount relay" + let wakuRlnConfig3 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - # mount rlnrelay in off-chain mode - let wakuRlnConfig1 = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + await node3.mountRlnRelay(wakuRlnConfig3) + await node3.start() - await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() - - # Registration is mandatory before sending messages with rln-relay - let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) - let idCredentials1 = generateCredentials() - - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() - - let rootUpdated1 = waitFor manager1.updateRoots() - info "Updated root for node1", rootUpdated1 - - # node 2 - (await node2.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - # mount rlnrelay in off-chain mode - let wakuRlnConfig2 = getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) - - await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - - let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) - let rootUpdated2 = waitFor manager2.updateRoots() - info "Updated root for node2", rootUpdated2 - - # node 3 - (await node3.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - - let wakuRlnConfig3 = getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - - await node3.mountRlnRelay(wakuRlnConfig3) - await node3.start() - - let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) - let rootUpdated3 = waitFor manager3.updateRoots() - info "Updated root for node3", rootUpdated3 + let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) + let rootUpdated3 = waitFor manager3.updateRoots() + info "Updated root for node3", rootUpdated3 # connect them together await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) @@ -135,8 +136,10 @@ procSuite "WakuNode - RLN relay": WakuMessage(payload: @payload, contentTopic: contentTopic, timestamp: now()) doAssert( node1.wakuRlnRelay - .unsafeAppendRLNProof(message, node1.wakuRlnRelay.getCurrentEpoch(), MessageId(0)) - .isOk() + .unsafeAppendRLNProof( + message, node1.wakuRlnRelay.getCurrentEpoch(), MessageId(0) + ) + .isOk() ) info " Nodes participating in the test", @@ -156,44 +159,69 @@ procSuite "WakuNode - RLN relay": asyncTest "testing rln-relay is applied in all rln shards/content topics": # create 3 nodes - let nodes = toSeq(0 ..< 3).mapIt( - newTestWakuNode(generateSecp256k1Key(), parseIpAddress("0.0.0.0"), Port(0)) - ) - await allFutures(nodes.mapIt(it.start())) + var node1, node2, node3: WakuNode + lockNewGlobalBrokerContext: + let nodeKey1 = generateSecp256k1Key() + node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() + + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error + + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node", node = 1, rootUpdated = rootUpdated1 + lockNewGlobalBrokerContext: + let nodeKey2 = generateSecp256k1Key() + node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let idCredentials2 = generateCredentials() + + (waitFor manager2.register(idCredentials2, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error + + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node", node = 2, rootUpdated = rootUpdated2 + lockNewGlobalBrokerContext: + let nodeKey3 = generateSecp256k1Key() + node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) + (await node3.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig3 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) + await node3.mountRlnRelay(wakuRlnConfig3) + await node3.start() + let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) + let idCredentials3 = generateCredentials() + + (waitFor manager3.register(idCredentials3, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error + + let rootUpdated3 = waitFor manager3.updateRoots() + info "Updated root for node", node = 3, rootUpdated = rootUpdated3 let shards = @[RelayShard(clusterId: 0, shardId: 0), RelayShard(clusterId: 0, shardId: 1)] - let contentTopics = - @[ - ContentTopic("/waku/2/content-topic-a/proto"), - ContentTopic("/waku/2/content-topic-b/proto"), - ] - - # set up three nodes - await allFutures(nodes.mapIt(it.mountRelay())) - - # mount rlnrelay in off-chain mode - for index, node in nodes: - let wakuRlnConfig = - getWakuRlnConfig(manager = manager, index = MembershipIndex(index + 1)) - - await node.mountRlnRelay(wakuRlnConfig) - await node.start() - let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) - let idCredentials = generateCredentials() - - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() - - let rootUpdated = waitFor manager.updateRoots() - info "Updated root for node", node = index + 1, rootUpdated = rootUpdated + let contentTopics = @[ + ContentTopic("/waku/2/content-topic-a/proto"), + ContentTopic("/waku/2/content-topic-b/proto"), + ] # connect them together - await nodes[0].connectToNodes(@[nodes[1].switch.peerInfo.toRemotePeerInfo()]) - await nodes[2].connectToNodes(@[nodes[1].switch.peerInfo.toRemotePeerInfo()]) + await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) + await node3.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) var rxMessagesTopic1 = 0 var rxMessagesTopic2 = 0 @@ -211,15 +239,15 @@ procSuite "WakuNode - RLN relay": ): Future[void] {.async, gcsafe.} = await sleepAsync(0.milliseconds) - nodes[0].subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), simpleHandler).isOkOr: - assert false, "Failed to subscribe to pubsub topic in nodes[0]: " & $error - nodes[1].subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), simpleHandler).isOkOr: - assert false, "Failed to subscribe to pubsub topic in nodes[1]: " & $error + node1.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), simpleHandler).isOkOr: + assert false, "Failed to subscribe to pubsub topic in node1: " & $error + node2.subscribe((kind: PubsubSub, topic: DefaultPubsubTopic), simpleHandler).isOkOr: + assert false, "Failed to subscribe to pubsub topic in node2: " & $error # mount the relay handlers - nodes[2].subscribe((kind: PubsubSub, topic: $shards[0]), relayHandler).isOkOr: + node3.subscribe((kind: PubsubSub, topic: $shards[0]), relayHandler).isOkOr: assert false, "Failed to subscribe to pubsub topic: " & $error - nodes[2].subscribe((kind: PubsubSub, topic: $shards[1]), relayHandler).isOkOr: + node3.subscribe((kind: PubsubSub, topic: $shards[1]), relayHandler).isOkOr: assert false, "Failed to subscribe to pubsub topic: " & $error await sleepAsync(1000.millis) @@ -236,8 +264,8 @@ procSuite "WakuNode - RLN relay": contentTopic: contentTopics[0], ) - nodes[0].wakuRlnRelay.unsafeAppendRLNProof( - message, nodes[0].wakuRlnRelay.getCurrentEpoch(), MessageId(i.uint8) + node1.wakuRlnRelay.unsafeAppendRLNProof( + message, node1.wakuRlnRelay.getCurrentEpoch(), MessageId(i.uint8) ).isOkOr: raiseAssert $error messages1.add(message) @@ -249,8 +277,8 @@ procSuite "WakuNode - RLN relay": contentTopic: contentTopics[1], ) - nodes[1].wakuRlnRelay.unsafeAppendRLNProof( - message, nodes[1].wakuRlnRelay.getCurrentEpoch(), MessageId(i.uint8) + node2.wakuRlnRelay.unsafeAppendRLNProof( + message, node2.wakuRlnRelay.getCurrentEpoch(), MessageId(i.uint8) ).isOkOr: raiseAssert $error messages2.add(message) @@ -258,9 +286,9 @@ procSuite "WakuNode - RLN relay": # publish 3 messages from node[0] (last 2 are spam, window is 10 secs) # publish 3 messages from node[1] (last 2 are spam, window is 10 secs) for msg in messages1: - discard await nodes[0].publish(some($shards[0]), msg) + discard await node1.publish(some($shards[0]), msg) for msg in messages2: - discard await nodes[1].publish(some($shards[1]), msg) + discard await node2.publish(some($shards[1]), msg) # wait for gossip to propagate await sleepAsync(5000.millis) @@ -271,70 +299,67 @@ procSuite "WakuNode - RLN relay": rxMessagesTopic1 == 3 rxMessagesTopic2 == 3 - await allFutures(nodes.mapIt(it.stop())) + await node1.stop() + await node2.stop() + await node3.stop() asyncTest "testing rln-relay with invalid proof": - let + var node1, node2, node3: WakuNode + let contentTopic = ContentTopic("/waku/2/default-content/proto") + lockNewGlobalBrokerContext: # publisher node - nodeKey1 = generateSecp256k1Key() + let nodeKey1 = generateSecp256k1Key() node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + + # mount rlnrelay in off-chain mode + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() + + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() + + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error + + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node1", rootUpdated1 + lockNewGlobalBrokerContext: # Relay node - nodeKey2 = generateSecp256k1Key() + let nodeKey2 = generateSecp256k1Key() node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + # mount rlnrelay in off-chain mode + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() + + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node2", rootUpdated2 + lockNewGlobalBrokerContext: # Subscriber - nodeKey3 = generateSecp256k1Key() + let nodeKey3 = generateSecp256k1Key() node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) + (await node3.mountRelay()).isOkOr: + assert false, "Failed to mount relay" - contentTopic = ContentTopic("/waku/2/default-content/proto") + let wakuRlnConfig3 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - # set up three nodes - # node1 - (await node1.mountRelay()).isOkOr: - assert false, "Failed to mount relay" + await node3.mountRlnRelay(wakuRlnConfig3) + await node3.start() - # mount rlnrelay in off-chain mode - let wakuRlnConfig1 = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) - - await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() - - let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) - let idCredentials1 = generateCredentials() - - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() - - let rootUpdated1 = waitFor manager1.updateRoots() - info "Updated root for node1", rootUpdated1 - - # node 2 - (await node2.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - # mount rlnrelay in off-chain mode - let wakuRlnConfig2 = getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) - - await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - - let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) - let rootUpdated2 = waitFor manager2.updateRoots() - info "Updated root for node2", rootUpdated2 - - # node 3 - (await node3.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - - let wakuRlnConfig3 = getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - - await node3.mountRlnRelay(wakuRlnConfig3) - await node3.start() - - let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) - let rootUpdated3 = waitFor manager3.updateRoots() - info "Updated root for node3", rootUpdated3 + let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) + let rootUpdated3 = waitFor manager3.updateRoots() + info "Updated root for node3", rootUpdated3 # connect them together await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) @@ -390,72 +415,67 @@ procSuite "WakuNode - RLN relay": await node3.stop() asyncTest "testing rln-relay double-signaling detection": - let + var node1, node2, node3: WakuNode + let contentTopic = ContentTopic("/waku/2/default-content/proto") + lockNewGlobalBrokerContext: # publisher node - nodeKey1 = generateSecp256k1Key() + let nodeKey1 = generateSecp256k1Key() node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + + # mount rlnrelay in off-chain mode + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() + + # Registration is mandatory before sending messages with rln-relay + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() + + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error + + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node1", rootUpdated1 + lockNewGlobalBrokerContext: # Relay node - nodeKey2 = generateSecp256k1Key() + let nodeKey2 = generateSecp256k1Key() node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + + # mount rlnrelay in off-chain mode + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() + + # Registration is mandatory before sending messages with rln-relay + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node2", rootUpdated2 + lockNewGlobalBrokerContext: # Subscriber - nodeKey3 = generateSecp256k1Key() + let nodeKey3 = generateSecp256k1Key() node3 = newTestWakuNode(nodeKey3, parseIpAddress("0.0.0.0"), Port(0)) + (await node3.mountRelay()).isOkOr: + assert false, "Failed to mount relay" - contentTopic = ContentTopic("/waku/2/default-content/proto") + # mount rlnrelay in off-chain mode + let wakuRlnConfig3 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - # set up three nodes - # node1 - (await node1.mountRelay()).isOkOr: - assert false, "Failed to mount relay" + await node3.mountRlnRelay(wakuRlnConfig3) + await node3.start() - # mount rlnrelay in off-chain mode - let wakuRlnConfig1 = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) - - await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() - - # Registration is mandatory before sending messages with rln-relay - let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) - let idCredentials1 = generateCredentials() - - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() - - let rootUpdated1 = waitFor manager1.updateRoots() - info "Updated root for node1", rootUpdated1 - - # node 2 - (await node2.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - - # mount rlnrelay in off-chain mode - let wakuRlnConfig2 = getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) - - await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - - # Registration is mandatory before sending messages with rln-relay - let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) - let rootUpdated2 = waitFor manager2.updateRoots() - info "Updated root for node2", rootUpdated2 - - # node 3 - (await node3.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - - # mount rlnrelay in off-chain mode - let wakuRlnConfig3 = getWakuRlnConfig(manager = manager, index = MembershipIndex(3)) - - await node3.mountRlnRelay(wakuRlnConfig3) - await node3.start() - - # Registration is mandatory before sending messages with rln-relay - let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) - let rootUpdated3 = waitFor manager3.updateRoots() - info "Updated root for node3", rootUpdated3 + # Registration is mandatory before sending messages with rln-relay + let manager3 = cast[OnchainGroupManager](node3.wakuRlnRelay.groupManager) + let rootUpdated3 = waitFor manager3.updateRoots() + info "Updated root for node3", rootUpdated3 # connect the nodes together node1 <-> node2 <-> node3 await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) @@ -565,49 +585,46 @@ procSuite "WakuNode - RLN relay": xasyncTest "clearNullifierLog: should clear epochs > MaxEpochGap": ## This is skipped because is flaky and made CI randomly fail but is useful to run manually # Given two nodes + var node1, node2: WakuNode let contentTopic = ContentTopic("/waku/2/default-content/proto") shardSeq = @[DefaultRelayShard] - nodeKey1 = generateSecp256k1Key() - node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) - nodeKey2 = generateSecp256k1Key() - node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) epochSizeSec: uint64 = 5 # This means rlnMaxEpochGap = 4 + lockNewGlobalBrokerContext: + let nodeKey1 = generateSecp256k1Key() + node1 = newTestWakuNode(nodeKey1, parseIpAddress("0.0.0.0"), Port(0)) + (await node1.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig1 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + await node1.mountRlnRelay(wakuRlnConfig1) + await node1.start() - # Given both nodes mount relay and rlnrelay - (await node1.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - let wakuRlnConfig1 = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) - await node1.mountRlnRelay(wakuRlnConfig1) - await node1.start() + # Registration is mandatory before sending messages with rln-relay + let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) + let idCredentials1 = generateCredentials() - # Registration is mandatory before sending messages with rln-relay - let manager1 = cast[OnchainGroupManager](node1.wakuRlnRelay.groupManager) - let idCredentials1 = generateCredentials() + (waitFor manager1.register(idCredentials1, UserMessageLimit(20))).isOkOr: + assert false, "error returned when calling register: " & error - try: - waitFor manager1.register(idCredentials1, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + let rootUpdated1 = waitFor manager1.updateRoots() + info "Updated root for node1", rootUpdated1 + lockNewGlobalBrokerContext: + let nodeKey2 = generateSecp256k1Key() + node2 = newTestWakuNode(nodeKey2, parseIpAddress("0.0.0.0"), Port(0)) + (await node2.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + let wakuRlnConfig2 = + getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) + await node2.mountRlnRelay(wakuRlnConfig2) + await node2.start() - let rootUpdated1 = waitFor manager1.updateRoots() - info "Updated root for node1", rootUpdated1 - - # Mount rlnrelay in node2 in off-chain mode - (await node2.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - let wakuRlnConfig2 = getWakuRlnConfig(manager = manager, index = MembershipIndex(2)) - await node2.mountRlnRelay(wakuRlnConfig2) - await node2.start() - - # Registration is mandatory before sending messages with rln-relay - let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) - let rootUpdated2 = waitFor manager2.updateRoots() - info "Updated root for node2", rootUpdated2 + # Registration is mandatory before sending messages with rln-relay + let manager2 = cast[OnchainGroupManager](node2.wakuRlnRelay.groupManager) + let rootUpdated2 = waitFor manager2.updateRoots() + info "Updated root for node2", rootUpdated2 # Given the two nodes are started and connected - waitFor allFutures(node1.start(), node2.start()) await node1.connectToNodes(@[node2.switch.peerInfo.toRemotePeerInfo()]) # Given some messages diff --git a/tests/waku_rln_relay/utils.nim b/tests/waku_rln_relay/utils.nim index a4247ab44..8aed18f9b 100644 --- a/tests/waku_rln_relay/utils.nim +++ b/tests/waku_rln_relay/utils.nim @@ -24,7 +24,6 @@ proc deployContract*( tr.`from` = Opt.some(web3.defaultAccount) let sData = code & contractInput tr.data = Opt.some(hexToSeqByte(sData)) - tr.gas = Opt.some(Quantity(3000000000000)) if gasPrice != 0: tr.gasPrice = Opt.some(gasPrice.Quantity) diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim index 85f627aa0..db07d3cd6 100644 --- a/tests/waku_rln_relay/utils_onchain.nim +++ b/tests/waku_rln_relay/utils_onchain.nim @@ -3,7 +3,7 @@ {.push raises: [].} import - std/[options, os, osproc, deques, streams, strutils, tempfiles, strformat], + std/[options, os, osproc, streams, strutils, strformat], results, stew/byteutils, testutils/unittests, @@ -14,7 +14,6 @@ import web3/conversions, web3/eth_api_types, json_rpc/rpcclient, - json, libp2p/crypto/crypto, eth/keys, results @@ -24,25 +23,19 @@ import waku_rln_relay, waku_rln_relay/protocol_types, waku_rln_relay/constants, - waku_rln_relay/contract, waku_rln_relay/rln, ], - ../testlib/common, - ./utils + ../testlib/common const CHAIN_ID* = 1234'u256 -template skip0xPrefix(hexStr: string): int = - ## Returns the index of the first meaningful char in `hexStr` by skipping - ## "0x" prefix - if hexStr.len > 1 and hexStr[0] == '0' and hexStr[1] in {'x', 'X'}: 2 else: 0 - -func strip0xPrefix(s: string): string = - let prefixLen = skip0xPrefix(s) - if prefixLen != 0: - s[prefixLen .. ^1] - else: - s +# Path to the file which Anvil loads at startup to initialize the chain with pre-deployed contracts, an account funded with tokens and approved for spending +const DEFAULT_ANVIL_STATE_PATH* = + "tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json.gz" +# The contract address of the TestStableToken used for the RLN Membership registration fee +const TOKEN_ADDRESS* = "0x5FbDB2315678afecb367f032d93F642f64180aa3" +# The contract address used ti interact with the WakuRLNV2 contract via the proxy +const WAKU_RLNV2_PROXY_ADDRESS* = "0x5fc8d32690cc91d4c39d9d3abcbd16989f875707" proc generateCredentials*(): IdentityCredential = let credRes = membershipKeyGen() @@ -82,6 +75,10 @@ proc getForgePath(): string = forgePath = joinPath(forgePath, ".foundry/bin/forge") return $forgePath +template execForge(cmd: string): tuple[output: string, exitCode: int] = + # unset env vars that affect e.g. "forge script" before running forge + execCmdEx("unset ETH_FROM ETH_PASSWORD && " & cmd) + contract(ERC20Token): proc allowance(owner: Address, spender: Address): UInt256 {.view.} proc balanceOf(account: Address): UInt256 {.view.} @@ -102,7 +99,7 @@ proc sendMintCall( recipientAddress: Address, amountTokens: UInt256, recipientBalanceBeforeExpectedTokens: Option[UInt256] = none(UInt256), -): Future[TxHash] {.async.} = +): Future[void] {.async.} = let doBalanceAssert = recipientBalanceBeforeExpectedTokens.isSome() if doBalanceAssert: @@ -138,7 +135,7 @@ proc sendMintCall( tx.data = Opt.some(byteutils.hexToSeqByte(mintCallData)) trace "Sending mint call" - let txHash = await web3.send(tx) + discard await web3.send(tx) let balanceOfSelector = "0x70a08231" let balanceCallData = balanceOfSelector & paddedAddress @@ -153,8 +150,6 @@ proc sendMintCall( assert balanceAfterMint == balanceAfterExpectedTokens, fmt"Balance is {balanceAfterMint} after transfer but expected {balanceAfterExpectedTokens}" - return txHash - # Check how many tokens a spender (the RLN contract) is allowed to spend on behalf of the owner (account which wishes to register a membership) proc checkTokenAllowance( web3: Web3, tokenAddress: Address, owner: Address, spender: Address @@ -225,11 +220,14 @@ proc deployTestToken*( # Deploy TestToken contract let forgeCmdTestToken = fmt"""cd {submodulePath} && {forgePath} script test/TestToken.sol --broadcast -vvv --rpc-url http://localhost:8540 --tc TestTokenFactory --private-key {pk} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" - let (outputDeployTestToken, exitCodeDeployTestToken) = execCmdEx(forgeCmdTestToken) + let (outputDeployTestToken, exitCodeDeployTestToken) = execForge(forgeCmdTestToken) trace "Executed forge command to deploy TestToken contract", output = outputDeployTestToken if exitCodeDeployTestToken != 0: - return error("Forge command to deploy TestToken contract failed") + error "Forge command to deploy TestToken contract failed", + error = outputDeployTestToken + return + err("Forge command to deploy TestToken contract failed: " & outputDeployTestToken) # Parse the command output to find contract address let testTokenAddress = getContractAddressFromDeployScriptOutput(outputDeployTestToken).valueOr: @@ -351,7 +349,7 @@ proc executeForgeContractDeployScripts*( let forgeCmdPriceCalculator = fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployPriceCalculator --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" let (outputDeployPriceCalculator, exitCodeDeployPriceCalculator) = - execCmdEx(forgeCmdPriceCalculator) + execForge(forgeCmdPriceCalculator) trace "Executed forge command to deploy LinearPriceCalculator contract", output = outputDeployPriceCalculator if exitCodeDeployPriceCalculator != 0: @@ -368,7 +366,7 @@ proc executeForgeContractDeployScripts*( let forgeCmdWakuRln = fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployWakuRlnV2 --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" - let (outputDeployWakuRln, exitCodeDeployWakuRln) = execCmdEx(forgeCmdWakuRln) + let (outputDeployWakuRln, exitCodeDeployWakuRln) = execForge(forgeCmdWakuRln) trace "Executed forge command to deploy WakuRlnV2 contract", output = outputDeployWakuRln if exitCodeDeployWakuRln != 0: @@ -388,7 +386,7 @@ proc executeForgeContractDeployScripts*( # Deploy Proxy contract let forgeCmdProxy = fmt"""cd {submodulePath} && {forgePath} script script/Deploy.s.sol --broadcast -vvvv --rpc-url http://localhost:8540 --tc DeployProxy --private-key {privateKey} && rm -rf broadcast/*/*/run-1*.json && rm -rf cache/*/*/run-1*.json""" - let (outputDeployProxy, exitCodeDeployProxy) = execCmdEx(forgeCmdProxy) + let (outputDeployProxy, exitCodeDeployProxy) = execForge(forgeCmdProxy) trace "Executed forge command to deploy proxy contract", output = outputDeployProxy if exitCodeDeployProxy != 0: error "Forge command to deploy Proxy failed", error = outputDeployProxy @@ -480,31 +478,125 @@ proc getAnvilPath*(): string = anvilPath = joinPath(anvilPath, ".foundry/bin/anvil") return $anvilPath +proc decompressGzipFile*( + compressedPath: string, targetPath: string +): Result[void, string] = + ## Decompress a gzipped file using the gunzip command-line utility + let cmd = fmt"gunzip -c {compressedPath} > {targetPath}" + + try: + let (output, exitCode) = execCmdEx(cmd) + if exitCode != 0: + return err( + "Failed to decompress '" & compressedPath & "' to '" & targetPath & "': " & + output + ) + except OSError as e: + return err("Failed to execute gunzip command: " & e.msg) + except IOError as e: + return err("Failed to execute gunzip command: " & e.msg) + + ok() + +proc compressGzipFile*(sourcePath: string, targetPath: string): Result[void, string] = + ## Compress a file with gzip using the gzip command-line utility + let cmd = fmt"gzip -c {sourcePath} > {targetPath}" + + try: + let (output, exitCode) = execCmdEx(cmd) + if exitCode != 0: + return err( + "Failed to compress '" & sourcePath & "' to '" & targetPath & "': " & output + ) + except OSError as e: + return err("Failed to execute gzip command: " & e.msg) + except IOError as e: + return err("Failed to execute gzip command: " & e.msg) + + ok() + # Runs Anvil daemon -proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process = +proc runAnvil*( + port: int = 8540, + chainId: string = "1234", + stateFile: Option[string] = none(string), + dumpStateOnExit: bool = false, +): Process = # Passed options are # --port Port to listen on. # --gas-limit Sets the block gas limit in WEI. # --balance The default account balance, specified in ether. # --chain-id Chain ID of the network. + # --load-state Initialize the chain from a previously saved state snapshot (read-only) + # --dump-state Dump the state on exit to the given file (write-only) + # Values used are representative of Linea Sepolia testnet # See anvil documentation https://book.getfoundry.sh/reference/anvil/ for more details try: let anvilPath = getAnvilPath() info "Anvil path", anvilPath - let runAnvil = startProcess( - anvilPath, - args = [ - "--port", - $port, - "--gas-limit", - "300000000000000", - "--balance", - "1000000000", - "--chain-id", - $chainId, - ], - options = {poUsePath}, - ) + + var args = @[ + "--port", + $port, + "--gas-limit", + "30000000", + "--gas-price", + "7", + "--base-fee", + "7", + "--balance", + "10000000000", + "--chain-id", + $chainId, + "--disable-min-priority-fee", + ] + + # Add state file argument if provided + if stateFile.isSome(): + var statePath = stateFile.get() + info "State file parameter provided", + statePath = statePath, + dumpStateOnExit = dumpStateOnExit, + absolutePath = absolutePath(statePath) + + # Check if the file is gzip compressed and handle decompression + if statePath.endsWith(".gz"): + let decompressedPath = statePath[0 .. ^4] # Remove .gz extension + debug "Gzip compressed state file detected", + compressedPath = statePath, decompressedPath = decompressedPath + + if not fileExists(decompressedPath): + decompressGzipFile(statePath, decompressedPath).isOkOr: + error "Failed to decompress state file", error = error + return nil + + statePath = decompressedPath + + if dumpStateOnExit: + # Ensure the directory exists + let stateDir = parentDir(statePath) + if not dirExists(stateDir): + createDir(stateDir) + # Fresh deployment: start clean and dump state on exit + args.add("--dump-state") + args.add(statePath) + debug "Anvil configured to dump state on exit", path = statePath + else: + # Using cache: only load state, don't overwrite it (preserves clean cached state) + if fileExists(statePath): + args.add("--load-state") + args.add(statePath) + debug "Anvil configured to load state file (read-only)", path = statePath + else: + warn "State file does not exist, anvil will start fresh", + path = statePath, absolutePath = absolutePath(statePath) + else: + info "No state file provided, anvil will start fresh without state persistence" + + info "Starting anvil with arguments", args = args.join(" ") + + let runAnvil = + startProcess(anvilPath, args = args, options = {poUsePath, poStdErrToStdOut}) let anvilPID = runAnvil.processID # We read stdout from Anvil to see when daemon is ready @@ -516,7 +608,13 @@ proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process = anvilStartLog.add(cmdline) if cmdline.contains("Listening on 127.0.0.1:" & $port): break + else: + error "Anvil daemon exited (closed output)", + pid = anvilPID, startLog = anvilStartLog + return except Exception, CatchableError: + warn "Anvil daemon stdout reading error; assuming it started OK", + pid = anvilPID, startLog = anvilStartLog, err = getCurrentExceptionMsg() break info "Anvil daemon is running and ready", pid = anvilPID, startLog = anvilStartLog return runAnvil @@ -536,7 +634,14 @@ proc stopAnvil*(runAnvil: Process) {.used.} = # Send termination signals when not defined(windows): discard execCmdEx(fmt"kill -TERM {anvilPID}") - discard execCmdEx(fmt"kill -9 {anvilPID}") + # Wait for graceful shutdown to allow state dumping + sleep(200) + # Only force kill if process is still running + let checkResult = execCmdEx(fmt"kill -0 {anvilPID} 2>/dev/null") + if checkResult.exitCode == 0: + info "Anvil process still running after TERM signal, sending KILL", + anvilPID = anvilPID + discard execCmdEx(fmt"kill -9 {anvilPID}") else: discard execCmdEx(fmt"taskkill /F /PID {anvilPID}") @@ -547,52 +652,100 @@ proc stopAnvil*(runAnvil: Process) {.used.} = info "Error stopping Anvil daemon", anvilPID = anvilPID, error = e.msg proc setupOnchainGroupManager*( - ethClientUrl: string = EthClient, amountEth: UInt256 = 10.u256 + ethClientUrl: string = EthClient, + amountEth: UInt256 = 10.u256, + deployContracts: bool = true, ): Future[OnchainGroupManager] {.async.} = + ## Setup an onchain group manager for testing + ## If deployContracts is false, it will assume that the Anvil testnet already has the required contracts deployed, this significantly speeds up test runs. + ## To run Anvil with a cached state file containing pre-deployed contracts, see runAnvil documentation. + ## + ## To generate/update the cached state file: + ## 1. Call runAnvil with stateFile and dumpStateOnExit=true + ## 2. Run setupOnchainGroupManager with deployContracts=true to deploy contracts + ## 3. The state will be saved to the specified file when anvil exits + ## 4. Commit this file to git + ## + ## To use cached state: + ## 1. Call runAnvil with stateFile and dumpStateOnExit=false + ## 2. Anvil loads state in read-only mode (won't overwrite the cached file) + ## 3. Call setupOnchainGroupManager with deployContracts=false + ## 4. Tests run fast using pre-deployed contracts let rlnInstanceRes = createRlnInstance() check: rlnInstanceRes.isOk() let rlnInstance = rlnInstanceRes.get() - # connect to the eth client let web3 = await newWeb3(ethClientUrl) let accounts = await web3.provider.eth_accounts() web3.defaultAccount = accounts[1] - let (privateKey, acc) = createEthAccount(web3) + var privateKey: keys.PrivateKey + var acc: Address + var testTokenAddress: Address + var contractAddress: Address - # we just need to fund the default account - # the send procedure returns a tx hash that we don't use, hence discard - discard await sendEthTransfer( - web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256) - ) + if not deployContracts: + info "Using contract addresses from constants" - let testTokenAddress = (await deployTestToken(privateKey, acc, web3)).valueOr: - assert false, "Failed to deploy test token contract: " & $error - return + testTokenAddress = Address(hexToByteArray[20](TOKEN_ADDRESS)) + contractAddress = Address(hexToByteArray[20](WAKU_RLNV2_PROXY_ADDRESS)) - # mint the token from the generated account - discard await sendMintCall( - web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256), some(0.u256) - ) + (privateKey, acc) = createEthAccount(web3) - let contractAddress = (await executeForgeContractDeployScripts(privateKey, acc, web3)).valueOr: - assert false, "Failed to deploy RLN contract: " & $error - return + # Fund the test account + discard await sendEthTransfer(web3, web3.defaultAccount, acc, ethToWei(1000.u256)) - # If the generated account wishes to register a membership, it needs to approve the contract to spend its tokens - let tokenApprovalResult = await approveTokenAllowanceAndVerify( - web3, - acc, - privateKey, - testTokenAddress, - contractAddress, - ethToWei(200.u256), - some(0.u256), - ) + # Mint tokens to the test account + await sendMintCall( + web3, web3.defaultAccount, testTokenAddress, acc, ethToWei(1000.u256) + ) - assert tokenApprovalResult.isOk, tokenApprovalResult.error() + # Approve the contract to spend tokens + let tokenApprovalResult = await approveTokenAllowanceAndVerify( + web3, acc, privateKey, testTokenAddress, contractAddress, ethToWei(200.u256) + ) + assert tokenApprovalResult.isOk(), tokenApprovalResult.error + else: + info "Performing Token and RLN contracts deployment" + (privateKey, acc) = createEthAccount(web3) + + # fund the default account + discard await sendEthTransfer( + web3, web3.defaultAccount, acc, ethToWei(1000.u256), some(0.u256) + ) + + testTokenAddress = (await deployTestToken(privateKey, acc, web3)).valueOr: + assert false, "Failed to deploy test token contract: " & $error + return + + # mint the token from the generated account + await sendMintCall( + web3, + web3.defaultAccount, + testTokenAddress, + acc, + ethToWei(1000.u256), + some(0.u256), + ) + + contractAddress = (await executeForgeContractDeployScripts(privateKey, acc, web3)).valueOr: + assert false, "Failed to deploy RLN contract: " & $error + return + + # If the generated account wishes to register a membership, it needs to approve the contract to spend its tokens + let tokenApprovalResult = await approveTokenAllowanceAndVerify( + web3, + acc, + privateKey, + testTokenAddress, + contractAddress, + ethToWei(200.u256), + some(0.u256), + ) + + assert tokenApprovalResult.isOk(), tokenApprovalResult.error let manager = OnchainGroupManager( ethClientUrls: @[ethClientUrl], diff --git a/tests/waku_store/test_client.nim b/tests/waku_store/test_client.nim index 38b07bdf4..d9c94a10c 100644 --- a/tests/waku_store/test_client.nim +++ b/tests/waku_store/test_client.nim @@ -1,6 +1,6 @@ {.used.} -import std/options, testutils/unittests, chronos, libp2p/crypto/crypto +import std/[options, sets], testutils/unittests, chronos, libp2p/crypto/crypto import waku/[node/peer_manager, waku_core, waku_store, waku_store/client, common/paging], @@ -35,24 +35,23 @@ suite "Store Client": hash1 = computeMessageHash(DefaultPubsubTopic, message1) hash2 = computeMessageHash(DefaultPubsubTopic, message2) hash3 = computeMessageHash(DefaultPubsubTopic, message3) - messageSeq = - @[ - WakuMessageKeyValue( - messageHash: hash1, - message: some(message1), - pubsubTopic: some(DefaultPubsubTopic), - ), - WakuMessageKeyValue( - messageHash: hash2, - message: some(message2), - pubsubTopic: some(DefaultPubsubTopic), - ), - WakuMessageKeyValue( - messageHash: hash3, - message: some(message3), - pubsubTopic: some(DefaultPubsubTopic), - ), - ] + messageSeq = @[ + WakuMessageKeyValue( + messageHash: hash1, + message: some(message1), + pubsubTopic: some(DefaultPubsubTopic), + ), + WakuMessageKeyValue( + messageHash: hash2, + message: some(message2), + pubsubTopic: some(DefaultPubsubTopic), + ), + WakuMessageKeyValue( + messageHash: hash3, + message: some(message3), + pubsubTopic: some(DefaultPubsubTopic), + ), + ] handlerFuture = newHistoryFuture() handler = proc(req: StoreQueryRequest): Future[StoreQueryResult] {.async, gcsafe.} = var request = req @@ -224,3 +223,24 @@ suite "Store Client": not await handlerFuture.withTimeout(FUTURE_TIMEOUT) queryResponse.isErr() queryResponse.error.kind == ErrorCode.PEER_DIAL_FAILURE + + asyncTest "queryToAny shuffles peers across calls": + # Register several fake store peers (no servers running) so every dial + # fails. PEER_DIAL_FAILURE carries the peerId of the last peer tried in + # the shuffled order, so observing different "last" peerIds across calls + # confirms shuffle is active inside queryToAny. + for _ in 0 ..< 3: + let fakeSwitch = newTestSwitch() + let peerInfo = fakeSwitch.peerInfo.toRemotePeerInfo() + peerInfo.protocols = @[WakuStoreCodec] + clientSwitch.peerStore.addPeer(peerInfo) + + var observedLastPeers: HashSet[string] + for _ in 0 ..< 20: + let res = await client.queryToAny(storeQuery) + check: + res.isErr() + res.error.kind == ErrorCode.PEER_DIAL_FAILURE + observedLastPeers.incl(res.error.address) + + check observedLastPeers.len >= 2 diff --git a/tests/waku_store/test_resume.nim b/tests/waku_store/test_resume.nim index 93e07ec0e..eb11b8f8e 100644 --- a/tests/waku_store/test_resume.nim +++ b/tests/waku_store/test_resume.nim @@ -50,19 +50,18 @@ suite "Store Resume - End to End": var clientDriver {.threadvar.}: ArchiveDriver asyncSetup: - let messages = - @[ - fakeWakuMessage(@[byte 00]), - fakeWakuMessage(@[byte 01]), - fakeWakuMessage(@[byte 02]), - fakeWakuMessage(@[byte 03]), - fakeWakuMessage(@[byte 04]), - fakeWakuMessage(@[byte 05]), - fakeWakuMessage(@[byte 06]), - fakeWakuMessage(@[byte 07]), - fakeWakuMessage(@[byte 08]), - fakeWakuMessage(@[byte 09]), - ] + let messages = @[ + fakeWakuMessage(@[byte 00]), + fakeWakuMessage(@[byte 01]), + fakeWakuMessage(@[byte 02]), + fakeWakuMessage(@[byte 03]), + fakeWakuMessage(@[byte 04]), + fakeWakuMessage(@[byte 05]), + fakeWakuMessage(@[byte 06]), + fakeWakuMessage(@[byte 07]), + fakeWakuMessage(@[byte 08]), + fakeWakuMessage(@[byte 09]), + ] let serverKey = generateSecp256k1Key() diff --git a/tests/waku_store/test_wakunode_store.nim b/tests/waku_store/test_wakunode_store.nim index b20309079..fa73cd16d 100644 --- a/tests/waku_store/test_wakunode_store.nim +++ b/tests/waku_store/test_wakunode_store.nim @@ -32,19 +32,18 @@ import procSuite "WakuNode - Store": ## Fixtures let timeOrigin = now() - let msgListA = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let msgListA = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] let hashes = msgListA.mapIt(computeMessageHash(DefaultPubsubTopic, it)) @@ -374,6 +373,12 @@ procSuite "WakuNode - Store": waitFor allFutures(client.stop(), server.stop()) test "Store protocol queries overrun request rate limitation": + when defined(macosx): + # on macos CI, this test is resulting a code 200 (OK) instead of a 429 error + # means the runner is somehow too slow to cause a request limit failure + skip() + return + ## Setup let serverKey = generateSecp256k1Key() @@ -386,7 +391,7 @@ procSuite "WakuNode - Store": let mountArchiveRes = server.mountArchive(archiveA) assert mountArchiveRes.isOk(), mountArchiveRes.error - waitFor server.mountStore((3, 500.millis)) + waitFor server.mountStore((3, 200.millis)) client.mountStoreClient() @@ -413,11 +418,11 @@ procSuite "WakuNode - Store": for count in 0 ..< 3: waitFor successProc() - waitFor sleepAsync(20.millis) + waitFor sleepAsync(1.millis) waitFor failsProc() - waitFor sleepAsync(500.millis) + waitFor sleepAsync(200.millis) for count in 0 ..< 3: waitFor successProc() diff --git a/tests/waku_store_legacy/store_utils.nim b/tests/waku_store_legacy/store_utils.nim deleted file mode 100644 index a70ca9376..000000000 --- a/tests/waku_store_legacy/store_utils.nim +++ /dev/null @@ -1,33 +0,0 @@ -{.used.} - -import std/options, chronos - -import - waku/[node/peer_manager, waku_core, waku_store_legacy, waku_store_legacy/client], - ../testlib/[common, wakucore] - -proc newTestWakuStore*( - switch: Switch, handler: HistoryQueryHandler -): Future[WakuStore] {.async.} = - let - peerManager = PeerManager.new(switch) - proto = WakuStore.new(peerManager, rng, handler) - - await proto.start() - switch.mount(proto) - - return proto - -proc newTestWakuStoreClient*(switch: Switch): WakuStoreClient = - let peerManager = PeerManager.new(switch) - WakuStoreClient.new(peerManager, rng) - -proc computeHistoryCursor*( - pubsubTopic: PubsubTopic, message: WakuMessage -): HistoryCursor = - HistoryCursor( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - storeTime: message.timestamp, - digest: computeDigest(message), - ) diff --git a/tests/waku_store_legacy/test_all.nim b/tests/waku_store_legacy/test_all.nim deleted file mode 100644 index b495310f2..000000000 --- a/tests/waku_store_legacy/test_all.nim +++ /dev/null @@ -1,8 +0,0 @@ -{.used.} - -import - ./test_client, - ./test_resume, - ./test_rpc_codec, - ./test_waku_store, - ./test_wakunode_store diff --git a/tests/waku_store_legacy/test_client.nim b/tests/waku_store_legacy/test_client.nim deleted file mode 100644 index 2a8616375..000000000 --- a/tests/waku_store_legacy/test_client.nim +++ /dev/null @@ -1,214 +0,0 @@ -{.used.} - -import std/options, testutils/unittests, chronos, libp2p/crypto/crypto - -import - waku/[ - node/peer_manager, - waku_core, - waku_store_legacy, - waku_store_legacy/client, - common/paging, - ], - ../testlib/[wakucore, testasync, futures], - ./store_utils - -suite "Store Client": - var message1 {.threadvar.}: WakuMessage - var message2 {.threadvar.}: WakuMessage - var message3 {.threadvar.}: WakuMessage - var messageSeq {.threadvar.}: seq[WakuMessage] - var handlerFuture {.threadvar.}: Future[HistoryQuery] - var handler {.threadvar.}: HistoryQueryHandler - var historyQuery {.threadvar.}: HistoryQuery - - var serverSwitch {.threadvar.}: Switch - var clientSwitch {.threadvar.}: Switch - - var server {.threadvar.}: WakuStore - var client {.threadvar.}: WakuStoreClient - - var serverPeerInfo {.threadvar.}: RemotePeerInfo - var clientPeerInfo {.threadvar.}: RemotePeerInfo - - asyncSetup: - message1 = fakeWakuMessage(contentTopic = DefaultContentTopic) - message2 = fakeWakuMessage(contentTopic = DefaultContentTopic) - message3 = fakeWakuMessage(contentTopic = DefaultContentTopic) - messageSeq = @[message1, message2, message3] - handlerFuture = newLegacyHistoryFuture() - handler = proc(req: HistoryQuery): Future[HistoryResult] {.async, gcsafe.} = - handlerFuture.complete(req) - return ok(HistoryResponse(messages: messageSeq)) - historyQuery = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - direction: PagingDirection.FORWARD, - requestId: "customRequestId", - ) - - serverSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - server = await newTestWakuStore(serverSwitch, handler = handler) - client = newTestWakuStoreClient(clientSwitch) - - await allFutures(serverSwitch.start(), clientSwitch.start()) - - ## The following sleep is aimed to prevent macos failures in CI - #[ -2024-05-16T13:24:45.5106200Z INF 2024-05-16 13:24:45.509+00:00 Stopping AutonatService topics="libp2p autonatservice" tid=53712 file=service.nim:203 -2024-05-16T13:24:45.5107960Z WRN 2024-05-16 13:24:45.509+00:00 service is already stopped topics="libp2p switch" tid=53712 file=switch.nim:86 -2024-05-16T13:24:45.5109010Z . (1.68s) -2024-05-16T13:24:45.5109320Z Store Client (0.00s) -2024-05-16T13:24:45.5109870Z SIGSEGV: Illegal storage access. (Attempt to read from nil?) -2024-05-16T13:24:45.5111470Z stack trace: (most recent call last) - ]# - await sleepAsync(500.millis) - - serverPeerInfo = serverSwitch.peerInfo.toRemotePeerInfo() - clientPeerInfo = clientSwitch.peerInfo.toRemotePeerInfo() - - asyncTeardown: - await allFutures(serverSwitch.stop(), clientSwitch.stop()) - - suite "HistoryQuery Creation and Execution": - asyncTest "Valid Queries": - # When a valid query is sent to the server - let queryResponse = await client.query(historyQuery, peer = serverPeerInfo) - - # Then the query is processed successfully - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == historyQuery - queryResponse.get().messages == messageSeq - - asyncTest "Invalid Queries": - # TODO: IMPROVE: We can't test "actual" invalid queries because - # it directly depends on the handler implementation, to achieve - # proper coverage we'd need an example implementation. - - # Given some invalid queries - let - invalidQuery1 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[], - direction: PagingDirection.FORWARD, - requestId: "reqId1", - ) - invalidQuery2 = HistoryQuery( - pubsubTopic: PubsubTopic.none(), - contentTopics: @[DefaultContentTopic], - direction: PagingDirection.FORWARD, - requestId: "reqId2", - ) - invalidQuery3 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - pageSize: 0, - requestId: "reqId3", - ) - invalidQuery4 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - pageSize: 0, - requestId: "reqId4", - ) - invalidQuery5 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - startTime: some(0.Timestamp), - endTime: some(0.Timestamp), - requestId: "reqId5", - ) - invalidQuery6 = HistoryQuery( - pubsubTopic: some(DefaultPubsubTopic), - contentTopics: @[DefaultContentTopic], - startTime: some(0.Timestamp), - endTime: some(-1.Timestamp), - requestId: "reqId6", - ) - - # When the query is sent to the server - let queryResponse1 = await client.query(invalidQuery1, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery1 - queryResponse1.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse2 = await client.query(invalidQuery2, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery2 - queryResponse2.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse3 = await client.query(invalidQuery3, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery3 - queryResponse3.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse4 = await client.query(invalidQuery4, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery4 - queryResponse4.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse5 = await client.query(invalidQuery5, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery5 - queryResponse5.get().messages == messageSeq - - # When the query is sent to the server - handlerFuture = newLegacyHistoryFuture() - let queryResponse6 = await client.query(invalidQuery6, peer = serverPeerInfo) - - # Then the query is not processed - assert await handlerFuture.withTimeout(FUTURE_TIMEOUT) - check: - handlerFuture.read() == invalidQuery6 - queryResponse6.get().messages == messageSeq - - suite "Verification of HistoryResponse Payload": - asyncTest "Positive Responses": - # When a valid query is sent to the server - let queryResponse = await client.query(historyQuery, peer = serverPeerInfo) - - # Then the query is processed successfully, and is of the expected type - check: - await handlerFuture.withTimeout(FUTURE_TIMEOUT) - type(queryResponse.get()) is HistoryResponse - - asyncTest "Negative Responses - PeerDialFailure": - # Given a stopped peer - let - otherServerSwitch = newTestSwitch() - otherServerPeerInfo = otherServerSwitch.peerInfo.toRemotePeerInfo() - - # When a query is sent to the stopped peer - let queryResponse = await client.query(historyQuery, peer = otherServerPeerInfo) - - # Then the query is not processed - check: - not await handlerFuture.withTimeout(FUTURE_TIMEOUT) - queryResponse.isErr() - queryResponse.error.kind == HistoryErrorKind.PEER_DIAL_FAILURE diff --git a/tests/waku_store_legacy/test_resume.nim b/tests/waku_store_legacy/test_resume.nim deleted file mode 100644 index 53e48834e..000000000 --- a/tests/waku_store_legacy/test_resume.nim +++ /dev/null @@ -1,342 +0,0 @@ -{.used.} - -when defined(waku_exp_store_resume): - # TODO: Review store resume test cases (#1282) - # Ongoing changes to test code base had ruin this test meanwhile, need to investigate and fix - - import - std/[options, tables, sets], - testutils/unittests, - chronos, - chronicles, - libp2p/crypto/crypto - import - waku/[ - common/databases/db_sqlite, - waku_archive_legacy/driver, - waku_archive_legacy/driver/sqlite_driver/sqlite_driver, - node/peer_manager, - waku_core, - waku_core/message/digest, - waku_store_legacy, - ], - ../waku_store_legacy/store_utils, - ../waku_archive_legacy/archive_utils, - ./testlib/common, - ./testlib/switch - - procSuite "Waku Store - resume store": - ## Fixtures - let storeA = block: - let store = newTestMessageStore() - let msgList = - @[ - fakeWakuMessage( - payload = @[byte 0], contentTopic = ContentTopic("2"), ts = ts(0) - ), - fakeWakuMessage( - payload = @[byte 1], contentTopic = ContentTopic("1"), ts = ts(1) - ), - fakeWakuMessage( - payload = @[byte 2], contentTopic = ContentTopic("2"), ts = ts(2) - ), - fakeWakuMessage( - payload = @[byte 3], contentTopic = ContentTopic("1"), ts = ts(3) - ), - fakeWakuMessage( - payload = @[byte 4], contentTopic = ContentTopic("2"), ts = ts(4) - ), - fakeWakuMessage( - payload = @[byte 5], contentTopic = ContentTopic("1"), ts = ts(5) - ), - fakeWakuMessage( - payload = @[byte 6], contentTopic = ContentTopic("2"), ts = ts(6) - ), - fakeWakuMessage( - payload = @[byte 7], contentTopic = ContentTopic("1"), ts = ts(7) - ), - fakeWakuMessage( - payload = @[byte 8], contentTopic = ContentTopic("2"), ts = ts(8) - ), - fakeWakuMessage( - payload = @[byte 9], contentTopic = ContentTopic("1"), ts = ts(9) - ), - ] - - for msg in msgList: - require store - .put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - .isOk() - - store - - let storeB = block: - let store = newTestMessageStore() - let msgList2 = - @[ - fakeWakuMessage( - payload = @[byte 0], contentTopic = ContentTopic("2"), ts = ts(0) - ), - fakeWakuMessage( - payload = @[byte 11], contentTopic = ContentTopic("1"), ts = ts(1) - ), - fakeWakuMessage( - payload = @[byte 12], contentTopic = ContentTopic("2"), ts = ts(2) - ), - fakeWakuMessage( - payload = @[byte 3], contentTopic = ContentTopic("1"), ts = ts(3) - ), - fakeWakuMessage( - payload = @[byte 4], contentTopic = ContentTopic("2"), ts = ts(4) - ), - fakeWakuMessage( - payload = @[byte 5], contentTopic = ContentTopic("1"), ts = ts(5) - ), - fakeWakuMessage( - payload = @[byte 13], contentTopic = ContentTopic("2"), ts = ts(6) - ), - fakeWakuMessage( - payload = @[byte 14], contentTopic = ContentTopic("1"), ts = ts(7) - ), - ] - - for msg in msgList2: - require store - .put( - DefaultPubsubTopic, - msg, - computeDigest(msg), - computeMessageHash(DefaultPubsubTopic, msg), - msg.timestamp, - ) - .isOk() - - store - - asyncTest "multiple query to multiple peers with pagination": - ## Setup - let - serverSwitchA = newTestSwitch() - serverSwitchB = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures( - serverSwitchA.start(), serverSwitchB.start(), clientSwitch.start() - ) - - let - serverA = await newTestWakuStoreNode(serverSwitchA, store = testStore) - serverB = await newTestWakuStoreNode(serverSwitchB, store = testStore) - client = newTestWakuStoreClient(clientSwitch) - - ## Given - let peers = - @[ - serverSwitchA.peerInfo.toRemotePeerInfo(), - serverSwitchB.peerInfo.toRemotePeerInfo(), - ] - let req = HistoryQuery(contentTopics: @[DefaultContentTopic], pageSize: 5) - - ## When - let res = await client.queryLoop(req, peers) - - ## Then - check: - res.isOk() - - let response = res.tryGet() - check: - response.len == 10 - - ## Cleanup - await allFutures(clientSwitch.stop(), serverSwitchA.stop(), serverSwitchB.stop()) - - asyncTest "resume message history": - ## Setup - let - serverSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures(serverSwitch.start(), clientSwitch.start()) - - let - server = await newTestWakuStore(serverSwitch, store = storeA) - client = await newTestWakuStore(clientSwitch) - - client.setPeer(serverSwitch.peerInfo.toRemotePeerInfo()) - - ## When - let res = await client.resume() - - ## Then - check res.isOk() - - let resumedMessagesCount = res.tryGet() - let storedMessagesCount = client.store.getMessagesCount().tryGet() - check: - resumedMessagesCount == 10 - storedMessagesCount == 10 - - ## Cleanup - await allFutures(clientSwitch.stop(), serverSwitch.stop()) - - asyncTest "resume history from a list of candidates - offline peer": - ## Setup - let - clientSwitch = newTestSwitch() - offlineSwitch = newTestSwitch() - - await clientSwitch.start() - - let client = await newTestWakuStore(clientSwitch) - - ## Given - let peers = @[offlineSwitch.peerInfo.toRemotePeerInfo()] - - ## When - let res = await client.resume(some(peers)) - - ## Then - check res.isErr() - - ## Cleanup - await clientSwitch.stop() - - asyncTest "resume history from a list of candidates - online and offline peers": - ## Setup - let - offlineSwitch = newTestSwitch() - serverASwitch = newTestSwitch() - serverBSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures( - serverASwitch.start(), serverBSwitch.start(), clientSwitch.start() - ) - - let - serverA = await newTestWakuStore(serverASwitch, store = storeA) - serverB = await newTestWakuStore(serverBSwitch, store = storeB) - client = await newTestWakuStore(clientSwitch) - - ## Given - let peers = - @[ - offlineSwitch.peerInfo.toRemotePeerInfo(), - serverASwitch.peerInfo.toRemotePeerInfo(), - serverBSwitch.peerInfo.toRemotePeerInfo(), - ] - - ## When - let res = await client.resume(some(peers)) - - ## Then - # `client` is expected to retrieve 14 messages: - # - The store mounted on `serverB` holds 10 messages (see `storeA` fixture) - # - The store mounted on `serverB` holds 7 messages (see `storeB` fixture) - # Both stores share 3 messages, resulting in 14 unique messages in total - check res.isOk() - - let restoredMessagesCount = res.tryGet() - let storedMessagesCount = client.store.getMessagesCount().tryGet() - check: - restoredMessagesCount == 14 - storedMessagesCount == 14 - - ## Cleanup - await allFutures(serverASwitch.stop(), serverBSwitch.stop(), clientSwitch.stop()) - - suite "WakuNode - waku store": - asyncTest "Resume proc fetches the history": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - await allFutures(client.start(), server.start()) - - let driver = newSqliteArchiveDriver() - server.mountArchive(some(driver), none(MessageValidator), none(RetentionPolicy)) - await server.mountStore() - - let clientStore = StoreQueueRef.new() - await client.mountStore(store = clientStore) - client.mountStoreClient(store = clientStore) - - ## Given - let message = fakeWakuMessage() - require server.wakuStore.store.put(DefaultPubsubTopic, message).isOk() - - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - await client.resume(some(@[serverPeer])) - - # Then - check: - client.wakuStore.store.getMessagesCount().tryGet() == 1 - - ## Cleanup - await allFutures(client.stop(), server.stop()) - - asyncTest "Resume proc discards duplicate messages": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - await allFutures(server.start(), client.start()) - await server.mountStore(store = StoreQueueRef.new()) - - let clientStore = StoreQueueRef.new() - await client.mountStore(store = clientStore) - client.mountStoreClient(store = clientStore) - - ## Given - let timeOrigin = now() - let - msg1 = fakeWakuMessage( - payload = "hello world1", ts = (timeOrigin + getNanoSecondTime(1)) - ) - msg2 = fakeWakuMessage( - payload = "hello world2", ts = (timeOrigin + getNanoSecondTime(2)) - ) - msg3 = fakeWakuMessage( - payload = "hello world3", ts = (timeOrigin + getNanoSecondTime(3)) - ) - - require server.wakuStore.store.put(DefaultPubsubTopic, msg1).isOk() - require server.wakuStore.store.put(DefaultPubsubTopic, msg2).isOk() - - # Insert the same message in both node's store - let - receivedTime3 = now() + getNanosecondTime(10) - digest3 = computeDigest(msg3) - require server.wakuStore.store - .put(DefaultPubsubTopic, msg3, digest3, receivedTime3) - .isOk() - require client.wakuStore.store - .put(DefaultPubsubTopic, msg3, digest3, receivedTime3) - .isOk() - - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - await client.resume(some(@[serverPeer])) - - ## Then - check: - # If the duplicates are discarded properly, then the total number of messages after resume should be 3 - client.wakuStore.store.getMessagesCount().tryGet() == 3 - - await allFutures(client.stop(), server.stop()) diff --git a/tests/waku_store_legacy/test_rpc_codec.nim b/tests/waku_store_legacy/test_rpc_codec.nim deleted file mode 100644 index 6897bab41..000000000 --- a/tests/waku_store_legacy/test_rpc_codec.nim +++ /dev/null @@ -1,185 +0,0 @@ -{.used.} - -import std/options, testutils/unittests, chronos -import - waku/[ - common/protobuf, - common/paging, - waku_core, - waku_store_legacy/rpc, - waku_store_legacy/rpc_codec, - ], - ../testlib/wakucore - -procSuite "Waku Store - RPC codec": - test "PagingIndexRPC protobuf codec": - ## Given - let index = PagingIndexRPC.compute( - fakeWakuMessage(), receivedTime = ts(), pubsubTopic = DefaultPubsubTopic - ) - - ## When - let encodedIndex = index.encode() - let decodedIndexRes = PagingIndexRPC.decode(encodedIndex.buffer) - - ## Then - check: - decodedIndexRes.isOk() - - let decodedIndex = decodedIndexRes.tryGet() - check: - # The fields of decodedIndex must be the same as the original index - decodedIndex == index - - test "PagingIndexRPC protobuf codec - empty index": - ## Given - let emptyIndex = PagingIndexRPC() - - let encodedIndex = emptyIndex.encode() - let decodedIndexRes = PagingIndexRPC.decode(encodedIndex.buffer) - - ## Then - check: - decodedIndexRes.isOk() - - let decodedIndex = decodedIndexRes.tryGet() - check: - # Check the correctness of init and encode for an empty PagingIndexRPC - decodedIndex == emptyIndex - - test "PagingInfoRPC protobuf codec": - ## Given - let - index = PagingIndexRPC.compute( - fakeWakuMessage(), receivedTime = ts(), pubsubTopic = DefaultPubsubTopic - ) - pagingInfo = PagingInfoRPC( - pageSize: some(1'u64), - cursor: some(index), - direction: some(PagingDirection.FORWARD), - ) - - ## When - let pb = pagingInfo.encode() - let decodedPagingInfo = PagingInfoRPC.decode(pb.buffer) - - ## Then - check: - decodedPagingInfo.isOk() - - check: - # The fields of decodedPagingInfo must be the same as the original pagingInfo - decodedPagingInfo.value == pagingInfo - decodedPagingInfo.value.direction == pagingInfo.direction - - test "PagingInfoRPC protobuf codec - empty paging info": - ## Given - let emptyPagingInfo = PagingInfoRPC() - - ## When - let pb = emptyPagingInfo.encode() - let decodedEmptyPagingInfo = PagingInfoRPC.decode(pb.buffer) - - ## Then - check: - decodedEmptyPagingInfo.isOk() - - check: - # check the correctness of init and encode for an empty PagingInfoRPC - decodedEmptyPagingInfo.value == emptyPagingInfo - - test "HistoryQueryRPC protobuf codec": - ## Given - let - index = PagingIndexRPC.compute( - fakeWakuMessage(), receivedTime = ts(), pubsubTopic = DefaultPubsubTopic - ) - pagingInfo = PagingInfoRPC( - pageSize: some(1'u64), - cursor: some(index), - direction: some(PagingDirection.BACKWARD), - ) - query = HistoryQueryRPC( - contentFilters: - @[ - HistoryContentFilterRPC(contentTopic: DefaultContentTopic), - HistoryContentFilterRPC(contentTopic: DefaultContentTopic), - ], - pagingInfo: some(pagingInfo), - startTime: some(Timestamp(10)), - endTime: some(Timestamp(11)), - ) - - ## When - let pb = query.encode() - let decodedQuery = HistoryQueryRPC.decode(pb.buffer) - - ## Then - check: - decodedQuery.isOk() - - check: - # the fields of decoded query decodedQuery must be the same as the original query query - decodedQuery.value == query - - test "HistoryQueryRPC protobuf codec - empty history query": - ## Given - let emptyQuery = HistoryQueryRPC() - - ## When - let pb = emptyQuery.encode() - let decodedEmptyQuery = HistoryQueryRPC.decode(pb.buffer) - - ## Then - check: - decodedEmptyQuery.isOk() - - check: - # check the correctness of init and encode for an empty HistoryQueryRPC - decodedEmptyQuery.value == emptyQuery - - test "HistoryResponseRPC protobuf codec": - ## Given - let - message = fakeWakuMessage() - index = PagingIndexRPC.compute( - message, receivedTime = ts(), pubsubTopic = DefaultPubsubTopic - ) - pagingInfo = PagingInfoRPC( - pageSize: some(1'u64), - cursor: some(index), - direction: some(PagingDirection.BACKWARD), - ) - res = HistoryResponseRPC( - messages: @[message], - pagingInfo: some(pagingInfo), - error: HistoryResponseErrorRPC.INVALID_CURSOR, - ) - - ## When - let pb = res.encode() - let decodedRes = HistoryResponseRPC.decode(pb.buffer) - - ## Then - check: - decodedRes.isOk() - - check: - # the fields of decoded response decodedRes must be the same as the original response res - decodedRes.value == res - - test "HistoryResponseRPC protobuf codec - empty history response": - ## Given - let emptyRes = HistoryResponseRPC() - - ## When - let pb = emptyRes.encode() - let decodedEmptyRes = HistoryResponseRPC.decode(pb.buffer) - - ## Then - check: - decodedEmptyRes.isOk() - - check: - # check the correctness of init and encode for an empty HistoryResponseRPC - decodedEmptyRes.value == emptyRes diff --git a/tests/waku_store_legacy/test_waku_store.nim b/tests/waku_store_legacy/test_waku_store.nim deleted file mode 100644 index b8dc835c8..000000000 --- a/tests/waku_store_legacy/test_waku_store.nim +++ /dev/null @@ -1,113 +0,0 @@ -{.used.} - -import testutils/unittests, chronos, libp2p/crypto/crypto - -import - waku/[ - common/paging, - node/peer_manager, - waku_core, - waku_store_legacy, - waku_store_legacy/client, - ], - ../testlib/wakucore, - ./store_utils - -suite "Waku Store - query handler legacy": - asyncTest "history query handler should be called": - ## Setup - let - serverSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures(serverSwitch.start(), clientSwitch.start()) - - ## Given - let serverPeerInfo = serverSwitch.peerInfo.toRemotePeerInfo() - - let msg = fakeWakuMessage(contentTopic = DefaultContentTopic) - - var queryHandlerFut = newFuture[(HistoryQuery)]() - - let queryHandler = proc( - req: HistoryQuery - ): Future[HistoryResult] {.async, gcsafe.} = - queryHandlerFut.complete(req) - return ok(HistoryResponse(messages: @[msg])) - - let - server = await newTestWakuStore(serverSwitch, handler = queryhandler) - client = newTestWakuStoreClient(clientSwitch) - - let req = HistoryQuery( - contentTopics: @[DefaultContentTopic], - direction: PagingDirection.FORWARD, - requestId: "reqId", - ) - - ## When - let queryRes = await client.query(req, peer = serverPeerInfo) - - ## Then - check: - not queryHandlerFut.failed() - queryRes.isOk() - - let request = queryHandlerFut.read() - check: - request == req - - let response = queryRes.tryGet() - check: - response.messages.len == 1 - response.messages == @[msg] - - ## Cleanup - await allFutures(serverSwitch.stop(), clientSwitch.stop()) - - asyncTest "history query handler should be called and return an error": - ## Setup - let - serverSwitch = newTestSwitch() - clientSwitch = newTestSwitch() - - await allFutures(serverSwitch.start(), clientSwitch.start()) - - ## Given - let serverPeerInfo = serverSwitch.peerInfo.toRemotePeerInfo() - - var queryHandlerFut = newFuture[(HistoryQuery)]() - let queryHandler = proc( - req: HistoryQuery - ): Future[HistoryResult] {.async, gcsafe.} = - queryHandlerFut.complete(req) - return err(HistoryError(kind: HistoryErrorKind.BAD_REQUEST)) - - let - server = await newTestWakuStore(serverSwitch, handler = queryhandler) - client = newTestWakuStoreClient(clientSwitch) - - let req = HistoryQuery( - contentTopics: @[DefaultContentTopic], - direction: PagingDirection.FORWARD, - requestId: "reqId", - ) - - ## When - let queryRes = await client.query(req, peer = serverPeerInfo) - - ## Then - check: - not queryHandlerFut.failed() - queryRes.isErr() - - let request = queryHandlerFut.read() - check: - request == req - - let error = queryRes.tryError() - check: - error.kind == HistoryErrorKind.BAD_REQUEST - - ## Cleanup - await allFutures(serverSwitch.stop(), clientSwitch.stop()) diff --git a/tests/waku_store_legacy/test_wakunode_store.nim b/tests/waku_store_legacy/test_wakunode_store.nim deleted file mode 100644 index 549033e98..000000000 --- a/tests/waku_store_legacy/test_wakunode_store.nim +++ /dev/null @@ -1,316 +0,0 @@ -{.used.} - -import - std/net, - testutils/unittests, - chronos, - libp2p/crypto/crypto, - libp2p/peerid, - libp2p/multiaddress, - libp2p/switch, - libp2p/protocols/pubsub/pubsub, - libp2p/protocols/pubsub/gossipsub -import - waku/[ - common/paging, - waku_core, - waku_core/message/digest, - node/peer_manager, - waku_archive_legacy, - waku_filter_v2, - waku_filter_v2/client, - waku_store_legacy, - waku_node, - ], - ../waku_store_legacy/store_utils, - ../waku_archive_legacy/archive_utils, - ../testlib/wakucore, - ../testlib/wakunode - -procSuite "WakuNode - Store Legacy": - ## Fixtures - let timeOrigin = now() - let msgListA = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] - - let archiveA = block: - let driver = newSqliteArchiveDriver() - - for msg in msgListA: - let msg_digest = waku_archive_legacy.computeDigest(msg) - let msg_hash = computeMessageHash(DefaultPubsubTopic, msg) - require ( - waitFor driver.put(DefaultPubsubTopic, msg, msg_digest, msg_hash, msg.timestamp) - ).isOk() - - driver - - test "Store protocol returns expected messages": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start()) - - let mountArchiveRes = server.mountLegacyArchive(archiveA) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - - client.mountLegacyStoreClient() - - ## Given - let req = HistoryQuery(contentTopics: @[DefaultContentTopic]) - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - let queryRes = waitFor client.query(req, peer = serverPeer) - - ## Then - check queryRes.isOk() - - let response = queryRes.get() - check: - response.messages == msgListA - - # Cleanup - waitFor allFutures(client.stop(), server.stop()) - - test "Store node history response - forward pagination": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start()) - - let mountArchiveRes = server.mountLegacyArchive(archiveA) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - - client.mountLegacyStoreClient() - - ## Given - let req = HistoryQuery( - contentTopics: @[DefaultContentTopic], - pageSize: 7, - direction: PagingDirection.FORWARD, - ) - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - var nextReq = req # copy - - var pages = newSeq[seq[WakuMessage]](2) - var cursors = newSeq[Option[HistoryCursor]](2) - - for i in 0 ..< 2: - let res = waitFor client.query(nextReq, peer = serverPeer) - require res.isOk() - - # Keep query response content - let response = res.get() - pages[i] = response.messages - cursors[i] = response.cursor - - # Set/update the request cursor - nextReq.cursor = cursors[i] - - ## Then - check: - cursors[0] == some(computeHistoryCursor(DefaultPubsubTopic, msgListA[6])) - cursors[1] == none(HistoryCursor) - - check: - pages[0] == msgListA[0 .. 6] - pages[1] == msgListA[7 .. 9] - - # Cleanup - waitFor allFutures(client.stop(), server.stop()) - - test "Store node history response - backward pagination": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start()) - - let mountArchiveRes = server.mountLegacyArchive(archiveA) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - - client.mountLegacyStoreClient() - - ## Given - let req = HistoryQuery( - contentTopics: @[DefaultContentTopic], - pageSize: 7, - direction: PagingDirection.BACKWARD, - ) - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - var nextReq = req # copy - - var pages = newSeq[seq[WakuMessage]](2) - var cursors = newSeq[Option[HistoryCursor]](2) - - for i in 0 ..< 2: - let res = waitFor client.query(nextReq, peer = serverPeer) - require res.isOk() - - # Keep query response content - let response = res.get() - pages[i] = response.messages - cursors[i] = response.cursor - - # Set/update the request cursor - nextReq.cursor = cursors[i] - - ## Then - check: - cursors[0] == some(computeHistoryCursor(DefaultPubsubTopic, msgListA[3])) - cursors[1] == none(HistoryCursor) - - check: - pages[0] == msgListA[3 .. 9] - pages[1] == msgListA[0 .. 2] - - # Cleanup - waitFor allFutures(client.stop(), server.stop()) - - test "Store protocol returns expected message when relay is disabled and filter enabled": - ## See nwaku issue #937: 'Store: ability to decouple store from relay' - ## Setup - let - filterSourceKey = generateSecp256k1Key() - filterSource = - newTestWakuNode(filterSourceKey, parseIpAddress("0.0.0.0"), Port(0)) - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start(), filterSource.start()) - - waitFor filterSource.mountFilter() - let driver = newSqliteArchiveDriver() - - let mountArchiveRes = server.mountLegacyArchive(driver) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - waitFor server.mountFilterClient() - client.mountLegacyStoreClient() - - ## Given - let message = fakeWakuMessage() - let - serverPeer = server.peerInfo.toRemotePeerInfo() - filterSourcePeer = filterSource.peerInfo.toRemotePeerInfo() - - ## Then - let filterFut = newFuture[(PubsubTopic, WakuMessage)]() - proc filterHandler( - pubsubTopic: PubsubTopic, msg: WakuMessage - ) {.async, gcsafe, closure.} = - await server.wakuLegacyArchive.handleMessage(pubsubTopic, msg) - filterFut.complete((pubsubTopic, msg)) - - server.wakuFilterClient.registerPushHandler(filterHandler) - let resp = waitFor server.filterSubscribe( - some(DefaultPubsubTopic), DefaultContentTopic, peer = filterSourcePeer - ) - - waitFor sleepAsync(100.millis) - - waitFor filterSource.wakuFilter.handleMessage(DefaultPubsubTopic, message) - - # Wait for the server filter to receive the push message - require waitFor filterFut.withTimeout(5.seconds) - - let res = waitFor client.query( - HistoryQuery(contentTopics: @[DefaultContentTopic]), peer = serverPeer - ) - - ## Then - check res.isOk() - - let response = res.get() - check: - response.messages.len == 1 - response.messages[0] == message - - let (handledPubsubTopic, handledMsg) = filterFut.read() - check: - handledPubsubTopic == DefaultPubsubTopic - handledMsg == message - - ## Cleanup - waitFor allFutures(client.stop(), server.stop(), filterSource.stop()) - - test "history query should return INVALID_CURSOR if the cursor has empty data in the request": - ## Setup - let - serverKey = generateSecp256k1Key() - server = newTestWakuNode(serverKey, parseIpAddress("0.0.0.0"), Port(0)) - clientKey = generateSecp256k1Key() - client = newTestWakuNode(clientKey, parseIpAddress("0.0.0.0"), Port(0)) - - waitFor allFutures(client.start(), server.start()) - - let mountArchiveRes = server.mountLegacyArchive(archiveA) - assert mountArchiveRes.isOk(), mountArchiveRes.error - - waitFor server.mountLegacyStore() - - client.mountLegacyStoreClient() - - ## Forcing a bad cursor with empty digest data - var data: array[32, byte] = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, - ] - let cursor = HistoryCursor( - pubsubTopic: "pubsubTopic", - senderTime: now(), - storeTime: now(), - digest: waku_archive_legacy.MessageDigest(data: data), - ) - - ## Given - let req = HistoryQuery(contentTopics: @[DefaultContentTopic], cursor: some(cursor)) - let serverPeer = server.peerInfo.toRemotePeerInfo() - - ## When - let queryRes = waitFor client.query(req, peer = serverPeer) - - ## Then - check not queryRes.isOk() - - check queryRes.error == - "legacy store client query error: BAD_REQUEST: invalid cursor" - - # Cleanup - waitFor allFutures(client.stop(), server.stop()) diff --git a/tests/waku_store_sync/sync_utils.nim b/tests/waku_store_sync/sync_utils.nim index fe62e02a1..888b10a83 100644 --- a/tests/waku_store_sync/sync_utils.nim +++ b/tests/waku_store_sync/sync_utils.nim @@ -45,7 +45,7 @@ proc newTestWakuRecon*( let proto = res.get() - proto.start() + await proto.start() switch.mount(proto) return proto @@ -55,7 +55,7 @@ proc newTestWakuTransfer*( idsTx: AsyncQueue[(SyncID, PubsubTopic, ContentTopic)], wantsRx: AsyncQueue[PeerId], needsRx: AsyncQueue[(PeerId, WakuMessageHash)], -): SyncTransfer = +): Future[SyncTransfer] {.async.} = let peerManager = PeerManager.new(switch) let proto = SyncTransfer.new( @@ -66,7 +66,7 @@ proc newTestWakuTransfer*( remoteNeedsRx = needsRx, ) - proto.start() + await proto.start() switch.mount(proto) return proto diff --git a/tests/waku_store_sync/test_protocol.nim b/tests/waku_store_sync/test_protocol.nim index bd13716a2..3ffa7ad4a 100644 --- a/tests/waku_store_sync/test_protocol.nim +++ b/tests/waku_store_sync/test_protocol.nim @@ -63,8 +63,8 @@ suite "Waku Sync: reconciliation": clientPeerInfo = clientSwitch.peerInfo.toRemotePeerInfo() asyncTeardown: - server.stop() - client.stop() + await server.stop() + await client.stop() await allFutures(serverSwitch.stop(), clientSwitch.stop()) @@ -372,7 +372,7 @@ suite "Waku Sync: reconciliation": const msgCount = 400_000 diffCount = 100_000 - tol = 1000 + tol = 10_000 var diffMsgHashes: HashSet[WakuMessageHash] var missingIdx: HashSet[int] @@ -506,7 +506,7 @@ suite "Waku Sync: reconciliation": let (_, deliveredHash) = await remoteNeeds.get() check deliveredHash in diffMsgHashes - asyncTest "sync 2 nodes, 40 msgs: 18 in-window diff, 20 out-window ignored": + #[ asyncTest "sync 2 nodes, 40 msgs: 17 in-window diff, 20 out-window ignored": server = await newTestWakuRecon( serverSwitch, @[], @[], DefaultSyncRange, idsChannel, localWants, remoteNeeds ) @@ -515,10 +515,10 @@ suite "Waku Sync: reconciliation": ) const - diffInWin = 18 + diffInWin = 17 diffOutWin = 20 stepOutNs = 100_000_000'u64 - outOffsetNs = 2_300_000_000'u64 # for 20 mesg they sent 2 seconds earlier + outOffsetNs = 3_000_000_000'u64 # for 20 mesg they sent 2 seconds earlier randomize() @@ -561,8 +561,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -572,7 +572,7 @@ suite "Waku Sync: reconciliation": for _ in 0 ..< diffInWin: let (_, deliveredHashes) = await remoteNeeds.popFirst() check deliveredHashes in inWinHashes - check deliveredHashes notin outWinHashes + check deliveredHashes notin outWinHashes ]# asyncTest "hash-fingerprint collision, same timestamp – stable sort": server = await newTestWakuRecon( @@ -610,8 +610,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -657,8 +657,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -701,8 +701,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -736,8 +736,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -773,8 +773,8 @@ suite "Waku Sync: reconciliation": ) defer: - server.stop() - client.stop() + await server.stop() + await client.stop() let res = await client.storeSynchronization(some(serverPeerInfo)) assert res.isOk(), $res.error @@ -848,8 +848,8 @@ suite "Waku Sync: transfer": remoteNeedsRx = clientRemoteNeeds, ) - server.start() - client.start() + await server.start() + await client.start() serverSwitch.mount(server) clientSwitch.mount(client) @@ -861,8 +861,8 @@ suite "Waku Sync: transfer": clientPeermanager.addPeer(serverPeerInfo) asyncTeardown: - server.stop() - client.stop() + await server.stop() + await client.stop() await allFutures(serverSwitch.stop(), clientSwitch.stop()) diff --git a/tests/waku_store_sync/test_range_split.nim b/tests/waku_store_sync/test_range_split.nim index 546f2cfa5..fe5252416 100644 --- a/tests/waku_store_sync/test_range_split.nim +++ b/tests/waku_store_sync/test_range_split.nim @@ -119,12 +119,11 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(wholeRange, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - wholeRange, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint( + wholeRange, @[DefaultPubsubTopic], @[DefaultContentTopic] + ) + ], itemSets: @[], ) @@ -180,12 +179,11 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(sliceWhole, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - sliceWhole, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint( + sliceWhole, @[DefaultPubsubTopic], @[DefaultContentTopic] + ) + ], itemSets: @[], ) @@ -207,12 +205,11 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(subSlice, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - subSlice, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint( + subSlice, @[DefaultPubsubTopic], @[DefaultContentTopic] + ) + ], itemSets: @[], ) @@ -272,12 +269,9 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(slice, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - slice, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint(slice, @[DefaultPubsubTopic], @[DefaultContentTopic]) + ], itemSets: @[], ) diff --git a/tests/waku_store_sync/test_state_transition.nim b/tests/waku_store_sync/test_state_transition.nim index d94d6bed2..2e6bb30c3 100644 --- a/tests/waku_store_sync/test_state_transition.nim +++ b/tests/waku_store_sync/test_state_transition.nim @@ -44,12 +44,9 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(whole, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - whole, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint(whole, @[DefaultPubsubTopic], @[DefaultContentTopic]) + ], itemSets: @[], ) let rep1 = local.processPayload(p1, s1, r1) @@ -131,15 +128,10 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(sliceA, RangeType.Fingerprint), (sliceB, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - sliceA, @[DefaultPubsubTopic], @[DefaultContentTopic] - ), - remote.computeFingerprint( - sliceB, @[DefaultPubsubTopic], @[DefaultContentTopic] - ), - ], + fingerprints: @[ + remote.computeFingerprint(sliceA, @[DefaultPubsubTopic], @[DefaultContentTopic]), + remote.computeFingerprint(sliceB, @[DefaultPubsubTopic], @[DefaultContentTopic]), + ], itemSets: @[], ) let reply = local.processPayload(payload, s, r) @@ -180,12 +172,9 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(slice, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - slice, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint(slice, @[DefaultPubsubTopic], @[DefaultContentTopic]) + ], itemSets: @[], ) let reply = local.processPayload(p, toS, toR) @@ -236,12 +225,9 @@ suite "Waku Sync – reconciliation": pubsubTopics: @[DefaultPubsubTopic], contentTopics: @[DefaultContentTopic], ranges: @[(s, RangeType.Fingerprint)], - fingerprints: - @[ - remote.computeFingerprint( - s, @[DefaultPubsubTopic], @[DefaultContentTopic] - ) - ], + fingerprints: @[ + remote.computeFingerprint(s, @[DefaultPubsubTopic], @[DefaultContentTopic]) + ], itemSets: @[], ), sendQ, diff --git a/tests/wakunode2/test_app.nim b/tests/wakunode2/test_app.nim index b16880787..7621ab1e7 100644 --- a/tests/wakunode2/test_app.nim +++ b/tests/wakunode2/test_app.nim @@ -1,14 +1,13 @@ {.used.} import + std/json, testutils/unittests, chronicles, chronos, - libp2p/crypto/crypto, - libp2p/crypto/secp, - libp2p/multiaddress, - libp2p/switch -import ../testlib/wakucore, ../testlib/wakunode + libp2p/[crypto/crypto, crypto/secp, multiaddress, switch], + tests/testlib/[wakucore, wakunode], + waku/factory/conf_builder/conf_builder include waku/factory/waku, waku/common/enr/typed_record @@ -21,7 +20,7 @@ suite "Wakunode2 - Waku": raiseAssert error ## When - let version = waku.version + let version = waku.stateInfo.getNodeInfoItem(NodeInfoId.Version) ## Then check: @@ -60,7 +59,8 @@ suite "Wakunode2 - Waku initialization": not node.wakuRendezvous.isNil() ## Cleanup - waitFor waku.stop() + (waitFor waku.stop()).isOkOr: + raiseAssert error test "app properly handles dynamic port configuration": ## Given @@ -96,4 +96,48 @@ suite "Wakunode2 - Waku initialization": typedNodeEnr.get().tcp.get() != 0 ## Cleanup - waitFor waku.stop() + (waitFor waku.stop()).isOkOr: + raiseAssert error + + test "explicit port=0 triggers auto-bind across all services": + var builder = defaultTestWakuConfBuilder() + builder.withP2pTcpPort(Port(0)) + builder.discv5Conf.withEnabled(true) + builder.discv5Conf.withUdpPort(Port(0)) + builder.restServerConf.withEnabled(true) + builder.restServerConf.withRelayCacheCapacity(50'u32) + builder.restServerConf.withPort(Port(0)) + builder.metricsServerConf.withEnabled(true) + builder.metricsServerConf.withHttpPort(Port(0)) + builder.webSocketConf.withEnabled(true) + builder.webSocketConf.withWebSocketPort(Port(0)) + + let conf = builder.build().valueOr: + raiseAssert error + + check: + conf.endpointConf.p2pTcpPort == Port(0) + conf.discv5Conf.get().udpPort == Port(0) + conf.restServerConf.get().port == Port(0) + conf.metricsServerConf.get().httpPort == Port(0) + conf.webSocketConf.get().port == Port(0) + + var waku = (waitFor Waku.new(conf)).valueOr: + raiseAssert error + defer: + (waitFor waku.stop()).isOkOr: + raiseAssert error + + (waitFor startWaku(addr waku)).isOkOr: + raiseAssert error + + let portsJson = waku.stateInfo.getNodeInfoItem(NodeInfoId.MyBoundPorts) + let parsed = parseJson(portsJson) + + check: + parsed.kind == JObject + parsed["tcp"].getInt() != 0 + parsed["webSocket"].getInt() != 0 + parsed["rest"].getInt() != 0 + parsed["discv5Udp"].getInt() != 0 + parsed["metrics"].getInt() != 0 diff --git a/tests/wakunode2/test_cli_args.nim b/tests/wakunode2/test_cli_args.nim index dabc78083..d08544c2c 100644 --- a/tests/wakunode2/test_cli_args.nim +++ b/tests/wakunode2/test_cli_args.nim @@ -1,7 +1,7 @@ {.used.} import - std/options, + std/[options, strutils], testutils/unittests, chronos, libp2p/crypto/[crypto, secp], @@ -23,9 +23,8 @@ import suite "Waku external config - default values": test "Default sharding value": ## Setup - let defaultShardingMode = AutoSharding - let defaultNumShardsInCluster = 1.uint16 - let defaultSubscribeShards = @[0.uint16] + let defaultShardingMode = StaticSharding + let defaultSubscribeShards: seq[uint16] = @[] ## Given let preConfig = defaultWakuNodeConf().get() @@ -37,7 +36,6 @@ suite "Waku external config - default values": ## Then let conf = res.get() check conf.shardingConf.kind == defaultShardingMode - check conf.shardingConf.numShardsInCluster == defaultNumShardsInCluster check conf.subscribeShards == defaultSubscribeShards test "Default shards value in static sharding": @@ -141,43 +139,6 @@ suite "Waku external config - apply preset": ## Then assert res.isErr(), "Invalid shard was accepted" - test "Apply TWN preset when cluster id = 1": - ## Setup - let expectedConf = NetworkConf.TheWakuNetworkConf() - - ## Given - let preConfig = WakuNodeConf( - cmd: noCommand, - clusterId: 1.uint16, - relay: true, - ethClientUrls: @["http://someaddress".EthRpcUrl], - ) - - ## When - let res = preConfig.toWakuConf() - assert res.isOk(), $res.error - - ## Then - let conf = res.get() - check conf.maxMessageSizeBytes == - uint64(parseCorrectMsgSize(expectedConf.maxMessageSize)) - check conf.clusterId == expectedConf.clusterId - check conf.rlnRelayConf.isSome() == expectedConf.rlnRelay - if conf.rlnRelayConf.isSome(): - let rlnRelayConf = conf.rlnRelayConf.get() - check rlnRelayConf.ethContractAddress == expectedConf.rlnRelayEthContractAddress - check rlnRelayConf.dynamic == expectedConf.rlnRelayDynamic - check rlnRelayConf.chainId == expectedConf.rlnRelayChainId - check rlnRelayConf.epochSizeSec == expectedConf.rlnEpochSizeSec - check rlnRelayConf.userMessageLimit == expectedConf.rlnRelayUserMessageLimit - check conf.shardingConf.kind == expectedConf.shardingConf.kind - check conf.shardingConf.numShardsInCluster == - expectedConf.shardingConf.numShardsInCluster - check conf.discv5Conf.isSome() == expectedConf.discv5Discovery - if conf.discv5Conf.isSome(): - let discv5Conf = conf.discv5Conf.get() - check discv5Conf.bootstrapNodes == expectedConf.discv5BootstrapNodes - suite "Waku external config - node key": test "Passed node key is used": ## Setup @@ -249,7 +210,7 @@ suite "Waku external config - Shards": let vRes = wakuConf.validate() assert vRes.isOk(), $vRes.error - test "Imvalid shard is passed without num shards": + test "Any shard is valid without num shards in static sharding mode": ## Setup ## Given @@ -259,7 +220,88 @@ suite "Waku external config - Shards": let res = wakuNodeConf.toWakuConf() ## Then - assert res.isErr(), "Invalid shard was accepted" + let wakuConf = res.get() + let vRes = wakuConf.validate() + assert vRes.isOk(), $vRes.error + +suite "Waku external config - store retention policy": + test "Default retention policy": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + # storeMessageRetentionPolicy keeps its default: "time:<2 days in seconds>" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == + @["time:" & $2.days.seconds] + + test "Single custom retention policy": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "capacity:50000" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == @["capacity:50000"] + + test "Retention policies with whitespace around semicolons and colons": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "time:3600 ; capacity:10000 ; size : 30GB" + + ## When + let res = conf.toWakuConf() + + ## Then + assert res.isOk(), $res.error + let wakuConf = res.get() + require wakuConf.storeServiceConf.isSome() + check wakuConf.storeServiceConf.get().retentionPolicies == + @["time:3600", "capacity:10000", "size:30GB"] + + test "Invalid retention policy type returns error": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "foo:1234" + + ## When + let res = conf.toWakuConf() + + ## Then + check res.isErr() + check res.error.contains("unknown retention policy type") + + test "Duplicated retention policy type returns error": + ## Given + var conf = defaultWakuNodeConf().get() + conf.store = true + conf.storeMessageDbUrl = "sqlite://test.db" + conf.storeMessageRetentionPolicy = "time:3600;time:7200;capacity:10000" + + ## When + let res = conf.toWakuConf() + + ## Then + check res.isErr() + check res.error.contains("duplicated retention policy type") suite "Waku external config - http url parsing": test "Basic HTTP URLs without authentication": diff --git a/tests/wakunode_rest/test_rest_admin.nim b/tests/wakunode_rest/test_rest_admin.nim index e47207a42..ef82b8dfc 100644 --- a/tests/wakunode_rest/test_rest_admin.nim +++ b/tests/wakunode_rest/test_rest_admin.nim @@ -14,12 +14,12 @@ import waku_node, waku_filter_v2/client, node/peer_manager, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/admin/types, - waku_api/rest/admin/handlers as admin_api, - waku_api/rest/admin/client as admin_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/admin/types, + rest_api/endpoint/admin/handlers as admin_rest_interface, + rest_api/endpoint/admin/client as admin_rest_client, waku_relay, waku_peer_exchange, ], @@ -65,7 +65,7 @@ suite "Waku v2 Rest API - Admin": ): Future[void] {.async, gcsafe.} = await sleepAsync(0.milliseconds) - let shard = RelayShard(clusterId: clusterId, shardId: 0) + let shard = RelayShard(clusterId: clusterId, shardId: 5) node1.subscribe((kind: PubsubSub, topic: $shard), simpleHandler).isOkOr: assert false, "Failed to subscribe to topic: " & $error node2.subscribe((kind: PubsubSub, topic: $shard), simpleHandler).isOkOr: @@ -212,6 +212,18 @@ suite "Waku v2 Rest API - Admin": let conn2 = await node1.peerManager.connectPeer(peerInfo2) let conn3 = await node1.peerManager.connectPeer(peerInfo3) + var count = 0 + while count < 20: + ## Wait ~1s at most for the peer store to update shard info + let getRes = await client.getPeers() + if getRes.data.allIt(it.shards == @[5.uint16]): + break + + count.inc() + await sleepAsync(50.milliseconds) + + assert count < 20, "Timeout waiting for shards to be updated in peer store" + # Check successful connections check: conn2 == true diff --git a/tests/wakunode_rest/test_rest_cors.nim b/tests/wakunode_rest/test_rest_cors.nim index 58e70aa25..0393b0d72 100644 --- a/tests/wakunode_rest/test_rest_cors.nim +++ b/tests/wakunode_rest/test_rest_cors.nim @@ -11,8 +11,8 @@ import waku/[ waku_node, node/waku_node as waku_node2, - waku_api/rest/server, - waku_api/rest/debug/handlers as debug_api, + rest_api/endpoint/server, + rest_api/endpoint/debug/handlers as debug_rest_interface, ], ../testlib/common, ../testlib/wakucore, diff --git a/tests/wakunode_rest/test_rest_debug.nim b/tests/wakunode_rest/test_rest_debug.nim index 9add57cbe..1171f5878 100644 --- a/tests/wakunode_rest/test_rest_debug.nim +++ b/tests/wakunode_rest/test_rest_debug.nim @@ -1,6 +1,7 @@ {.used.} import + std/options, testutils/unittests, presto, presto/client as presto_client, @@ -12,11 +13,11 @@ import waku_node, node/waku_node as waku_node2, # TODO: Remove after moving `git_version` to the app code. - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/debug/handlers as debug_api, - waku_api/rest/debug/client as debug_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/debug/handlers as debug_rest_interface, + rest_api/endpoint/debug/client as debug_rest_client, ], ../testlib/common, ../testlib/wakucore, diff --git a/tests/wakunode_rest/test_rest_debug_serdes.nim b/tests/wakunode_rest/test_rest_debug_serdes.nim index 13b791dc9..d3232e571 100644 --- a/tests/wakunode_rest/test_rest_debug_serdes.nim +++ b/tests/wakunode_rest/test_rest_debug_serdes.nim @@ -1,7 +1,7 @@ {.used.} import results, stew/byteutils, testutils/unittests, json_serialization -import waku/waku_api/rest/serdes, waku/waku_api/rest/debug/types +import waku/rest_api/endpoint/serdes, waku/rest_api/endpoint/debug/types suite "Waku v2 REST API - Debug - serialization": suite "DebugWakuInfo - decode": diff --git a/tests/wakunode_rest/test_rest_filter.nim b/tests/wakunode_rest/test_rest_filter.nim index f8dbf429a..1a4731d6a 100644 --- a/tests/wakunode_rest/test_rest_filter.nim +++ b/tests/wakunode_rest/test_rest_filter.nim @@ -9,21 +9,21 @@ import libp2p/crypto/crypto import waku/[ - waku_api/message_cache, + rest_api/message_cache, waku_core, waku_node, node/peer_manager, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/filter/types, - waku_api/rest/filter/handlers as filter_api, - waku_api/rest/filter/client as filter_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/filter/types, + rest_api/endpoint/filter/handlers as filter_rest_interface, + rest_api/endpoint/filter/client as filter_rest_client, waku_relay, waku_filter_v2/subscriptions, waku_filter_v2/common, - waku_api/rest/relay/handlers as relay_api, - waku_api/rest/relay/client as relay_api_client, + rest_api/endpoint/relay/handlers as relay_rest_interface, + rest_api/endpoint/relay/client as relay_rest_client, ], ../testlib/wakucore, ../testlib/wakunode @@ -176,12 +176,11 @@ suite "Waku v2 Rest API - Filter V2": ) discard await restFilterTest.client.filterPostSubscriptions(requestBody) - let contentFilters = - @[ - ContentTopic("1"), - ContentTopic("2"), - ContentTopic("3"), # ,ContentTopic("4") # Keep this subscription for check - ] + let contentFilters = @[ + ContentTopic("1"), + ContentTopic("2"), + ContentTopic("3"), # ,ContentTopic("4") # Keep this subscription for check + ] let requestBodyUnsub = FilterUnsubscribeRequest( requestId: "4321", diff --git a/tests/wakunode_rest/test_rest_health.nim b/tests/wakunode_rest/test_rest_health.nim index ec70b0874..0bdb93123 100644 --- a/tests/wakunode_rest/test_rest_health.nim +++ b/tests/wakunode_rest/test_rest_health.nim @@ -10,14 +10,15 @@ import libp2p/crypto/crypto import waku/[ + common/waku_protocol, waku_node, node/waku_node as waku_node2, # TODO: Remove after moving `git_version` to the app code. - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/health/handlers as health_api, - waku_api/rest/health/client as health_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/health/handlers as health_rest_interface, + rest_api/endpoint/health/client as health_rest_client, waku_rln_relay, node/health_monitor, ], @@ -41,8 +42,8 @@ suite "Waku v2 REST API - health": var manager {.threadVar.}: OnchainGroupManager setup: - anvilProc = runAnvil() - manager = waitFor setupOnchainGroupManager() + anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH)) + manager = waitFor setupOnchainGroupManager(deployContracts = false) teardown: stopAnvil(anvilProc) @@ -50,33 +51,22 @@ suite "Waku v2 REST API - health": asyncTest "Get node health info - GET /health": # Given let node = testWakuNode() - let healthMonitor = NodeHealthMonitor() await node.start() (await node.mountRelay()).isOkOr: assert false, "Failed to mount relay" - healthMonitor.setOverallHealth(HealthStatus.INITIALIZING) - var restPort = Port(0) let restAddress = parseIpAddress("0.0.0.0") let restServer = WakuRestServerRef.init(restAddress, restPort).tryGet() restPort = restServer.httpServer.address.port # update with bound port for client use + let healthMonitor = NodeHealthMonitor.new(node) + installHealthApiHandler(restServer.router, healthMonitor) restServer.start() let client = newRestHttpClient(initTAddress(restAddress, restPort)) - # When - var response = await client.healthCheck() - - # Then - check: - response.status == 200 - $response.contentType == $MIMETYPE_JSON - response.data == - HealthReport(nodeHealth: HealthStatus.INITIALIZING, protocolsHealth: @[]) - - # now kick in rln (currently the only check for health) + # kick in rln (currently the only check for health) await node.mountRlnRelay( getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) ) @@ -84,51 +74,42 @@ suite "Waku v2 REST API - health": node.mountLightPushClient() await node.mountFilterClient() - healthMonitor.setNodeToHealthMonitor(node) + # We don't have a Waku, so we need to set the overall health to READY here in its behalf healthMonitor.setOverallHealth(HealthStatus.READY) + # When - response = await client.healthCheck() + var response = await client.healthCheck() + let report = response.data # Then check: response.status == 200 $response.contentType == $MIMETYPE_JSON - response.data.nodeHealth == HealthStatus.READY - response.data.protocolsHealth.len() == 15 - response.data.protocolsHealth[0].protocol == "Relay" - response.data.protocolsHealth[0].health == HealthStatus.NOT_READY - response.data.protocolsHealth[0].desc == some("No connected peers") - response.data.protocolsHealth[1].protocol == "Rln Relay" - response.data.protocolsHealth[1].health == HealthStatus.READY - response.data.protocolsHealth[2].protocol == "Lightpush" - response.data.protocolsHealth[2].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[3].protocol == "Legacy Lightpush" - response.data.protocolsHealth[3].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[4].protocol == "Filter" - response.data.protocolsHealth[4].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[5].protocol == "Store" - response.data.protocolsHealth[5].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[6].protocol == "Legacy Store" - response.data.protocolsHealth[6].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[7].protocol == "Peer Exchange" - response.data.protocolsHealth[7].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[8].protocol == "Rendezvous" - response.data.protocolsHealth[8].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[9].protocol == "Mix" - response.data.protocolsHealth[9].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[10].protocol == "Lightpush Client" - response.data.protocolsHealth[10].health == HealthStatus.NOT_READY - response.data.protocolsHealth[10].desc == + report.nodeHealth == HealthStatus.READY + report.protocolsHealth.len() == 13 + + report.getHealth(RelayProtocol).health == HealthStatus.NOT_READY + report.getHealth(RelayProtocol).desc == some("No connected peers") + + report.getHealth(RlnRelayProtocol).health == HealthStatus.READY + + report.getHealth(LightpushProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(LegacyLightpushProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(FilterProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(StoreProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(PeerExchangeProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(RendezvousProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(MixProtocol).health == HealthStatus.NOT_MOUNTED + + report.getHealth(LightpushClientProtocol).health == HealthStatus.NOT_READY + report.getHealth(LightpushClientProtocol).desc == some("No Lightpush service peer available yet") - response.data.protocolsHealth[11].protocol == "Legacy Lightpush Client" - response.data.protocolsHealth[11].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[12].protocol == "Store Client" - response.data.protocolsHealth[12].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[13].protocol == "Legacy Store Client" - response.data.protocolsHealth[13].health == HealthStatus.NOT_MOUNTED - response.data.protocolsHealth[14].protocol == "Filter Client" - response.data.protocolsHealth[14].health == HealthStatus.NOT_READY - response.data.protocolsHealth[14].desc == + + report.getHealth(LegacyLightpushClientProtocol).health == HealthStatus.NOT_MOUNTED + report.getHealth(StoreClientProtocol).health == HealthStatus.NOT_MOUNTED + + report.getHealth(FilterClientProtocol).health == HealthStatus.NOT_READY + report.getHealth(FilterClientProtocol).desc == some("No Filter service peer available yet") await restServer.stop() diff --git a/tests/wakunode_rest/test_rest_lightpush.nim b/tests/wakunode_rest/test_rest_lightpush.nim index b09c72ee3..deba7de22 100644 --- a/tests/wakunode_rest/test_rest_lightpush.nim +++ b/tests/wakunode_rest/test_rest_lightpush.nim @@ -10,17 +10,17 @@ import import waku/[ - waku_api/message_cache, + rest_api/message_cache, waku_core, waku_node, node/peer_manager, waku_lightpush/common, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/lightpush/types, - waku_api/rest/lightpush/handlers as lightpush_api, - waku_api/rest/lightpush/client as lightpush_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/lightpush/types, + rest_api/endpoint/lightpush/handlers as lightpush_rest_interface, + rest_api/endpoint/lightpush/client as lightpush_rest_client, waku_relay, common/rate_limit/setting, ], @@ -61,7 +61,7 @@ proc init( assert false, "Failed to mount relay: " & $error (await testSetup.serviceNode.mountRelay()).isOkOr: assert false, "Failed to mount relay: " & $error - await testSetup.serviceNode.mountLightPush(rateLimit) + check (await testSetup.serviceNode.mountLightPush(rateLimit)).isOk() testSetup.pushNode.mountLightPushClient() testSetup.serviceNode.peerManager.addServicePeer( diff --git a/tests/wakunode_rest/test_rest_lightpush_legacy.nim b/tests/wakunode_rest/test_rest_lightpush_legacy.nim index fea51554b..4043eeed9 100644 --- a/tests/wakunode_rest/test_rest_lightpush_legacy.nim +++ b/tests/wakunode_rest/test_rest_lightpush_legacy.nim @@ -10,17 +10,17 @@ import import waku/[ - waku_api/message_cache, + rest_api/message_cache, waku_core, waku_node, node/peer_manager, waku_lightpush_legacy/common, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/legacy_lightpush/types, - waku_api/rest/legacy_lightpush/handlers as lightpush_api, - waku_api/rest/legacy_lightpush/client as lightpush_api_client, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/legacy_lightpush/types, + rest_api/endpoint/legacy_lightpush/handlers as lightpush_rest_interface, + rest_api/endpoint/legacy_lightpush/client as lightpush_rest_client, waku_relay, common/rate_limit/setting, ], @@ -61,7 +61,7 @@ proc init( assert false, "Failed to mount relay" (await testSetup.serviceNode.mountRelay()).isOkOr: assert false, "Failed to mount relay" - await testSetup.serviceNode.mountLegacyLightPush(rateLimit) + check (await testSetup.serviceNode.mountLegacyLightPush(rateLimit)).isOk() testSetup.pushNode.mountLegacyLightPushClient() testSetup.serviceNode.peerManager.addServicePeer( diff --git a/tests/wakunode_rest/test_rest_relay.nim b/tests/wakunode_rest/test_rest_relay.nim index 99470dbc8..a98b75520 100644 --- a/tests/wakunode_rest/test_rest_relay.nim +++ b/tests/wakunode_rest/test_rest_relay.nim @@ -7,18 +7,19 @@ import presto, presto/client as presto_client, libp2p/crypto/crypto +import brokers/broker_context import waku/[ common/base64, waku_core, waku_node, - waku_api/message_cache, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/relay/types, - waku_api/rest/relay/handlers as relay_api, - waku_api/rest/relay/client as relay_api_client, + rest_api/message_cache, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/relay/types, + rest_api/endpoint/relay/handlers as relay_rest_interface, + rest_api/endpoint/relay/client as relay_rest_client, waku_relay, waku_rln_relay, ], @@ -41,8 +42,8 @@ suite "Waku v2 Rest API - Relay": var manager {.threadVar.}: OnchainGroupManager setup: - anvilProc = runAnvil() - manager = waitFor setupOnchainGroupManager() + anvilProc = runAnvil(stateFile = some(DEFAULT_ANVIL_STATE_PATH)) + manager = waitFor setupOnchainGroupManager(deployContracts = false) teardown: stopAnvil(anvilProc) @@ -192,15 +193,14 @@ suite "Waku v2 Rest API - Relay": let pubSubTopic = "/waku/2/rs/0/0" - var messages = - @[ - fakeWakuMessage( - contentTopic = "content-topic-x", - payload = toBytes("TEST-1"), - meta = toBytes("test-meta"), - ephemeral = true, - ) - ] + var messages = @[ + fakeWakuMessage( + contentTopic = "content-topic-x", + payload = toBytes("TEST-1"), + meta = toBytes("test-meta"), + ephemeral = true, + ) + ] # Prevent duplicate messages for i in 0 ..< 2: @@ -263,15 +263,12 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated @@ -347,12 +344,11 @@ suite "Waku v2 Rest API - Relay": installRelayApiHandlers(restServer.router, node, cache) restServer.start() - let contentTopics = - @[ - ContentTopic("/app-1/2/default-content/proto"), - ContentTopic("/app-2/2/default-content/proto"), - ContentTopic("/app-3/2/default-content/proto"), - ] + let contentTopics = @[ + ContentTopic("/app-1/2/default-content/proto"), + ContentTopic("/app-2/2/default-content/proto"), + ContentTopic("/app-3/2/default-content/proto"), + ] # When let client = newRestHttpClient(initTAddress(restAddress, restPort)) @@ -393,13 +389,12 @@ suite "Waku v2 Rest API - Relay": restPort = restServer.httpServer.address.port # update with bound port for client use - let contentTopics = - @[ - ContentTopic("/waku/2/default-content1/proto"), - ContentTopic("/waku/2/default-content2/proto"), - ContentTopic("/waku/2/default-content3/proto"), - ContentTopic("/waku/2/default-contentX/proto"), - ] + let contentTopics = @[ + ContentTopic("/waku/2/default-content1/proto"), + ContentTopic("/waku/2/default-content2/proto"), + ContentTopic("/waku/2/default-content3/proto"), + ContentTopic("/waku/2/default-contentX/proto"), + ] let cache = MessageCache.init() cache.contentSubscribe(contentTopics[0]) @@ -453,10 +448,9 @@ suite "Waku v2 Rest API - Relay": let contentTopic = DefaultContentTopic - var messages = - @[ - fakeWakuMessage(contentTopic = DefaultContentTopic, payload = toBytes("TEST-1")) - ] + var messages = @[ + fakeWakuMessage(contentTopic = DefaultContentTopic, payload = toBytes("TEST-1")) + ] # Prevent duplicate messages for i in 0 ..< 2: @@ -505,24 +499,47 @@ suite "Waku v2 Rest API - Relay": asyncTest "Post a message to a content topic - POST /relay/v1/auto/messages/{topic}": ## "Relay API: publish and subscribe/unsubscribe": # Given - let node = testWakuNode() - (await node.mountRelay()).isOkOr: - assert false, "Failed to mount relay" - require node.mountAutoSharding(1, 8).isOk + var meshNode: WakuNode + lockNewGlobalBrokerContext: + meshNode = testWakuNode() + (await meshNode.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + require meshNode.mountAutoSharding(1, 8).isOk - let wakuRlnConfig = getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + let wakuRlnConfig = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) - await node.mountRlnRelay(wakuRlnConfig) - await node.start() - # Registration is mandatory before sending messages with rln-relay + await meshNode.mountRlnRelay(wakuRlnConfig) + await meshNode.start() + const testPubsubTopic = PubsubTopic("/waku/2/rs/1/0") + proc dummyHandler( + topic: PubsubTopic, msg: WakuMessage + ): Future[void] {.async, gcsafe.} = + discard + + meshNode.subscribe((kind: ContentSub, topic: DefaultContentTopic), dummyHandler).isOkOr: + raiseAssert "Failed to subscribe meshNode: " & error + + var node: WakuNode + lockNewGlobalBrokerContext: + node = testWakuNode() + (await node.mountRelay()).isOkOr: + assert false, "Failed to mount relay" + require node.mountAutoSharding(1, 8).isOk + + let wakuRlnConfig = + getWakuRlnConfig(manager = manager, index = MembershipIndex(1)) + + await node.mountRlnRelay(wakuRlnConfig) + await node.start() + await node.connectToNodes(@[meshNode.peerInfo.toRemotePeerInfo()]) + + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated @@ -586,15 +603,12 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated @@ -648,15 +662,12 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated @@ -723,15 +734,12 @@ suite "Waku v2 Rest API - Relay": await node.mountRlnRelay(wakuRlnConfig) await node.start() - # Registration is mandatory before sending messages with rln-relay + # Registration is mandatory before sending messages with rln-relay let manager = cast[OnchainGroupManager](node.wakuRlnRelay.groupManager) let idCredentials = generateCredentials() - try: - waitFor manager.register(idCredentials, UserMessageLimit(20)) - except Exception, CatchableError: - assert false, - "exception raised when calling register: " & getCurrentExceptionMsg() + (waitFor manager.register(idCredentials, UserMessageLimit(20))).isOkOr: + assert false, "Failed to register identity credentials" & getCurrentExceptionMsg() let rootUpdated = waitFor manager.updateRoots() info "Updated root for node", rootUpdated diff --git a/tests/wakunode_rest/test_rest_relay_serdes.nim b/tests/wakunode_rest/test_rest_relay_serdes.nim index 086aba22b..21d21e281 100644 --- a/tests/wakunode_rest/test_rest_relay_serdes.nim +++ b/tests/wakunode_rest/test_rest_relay_serdes.nim @@ -1,7 +1,9 @@ {.used.} import results, stew/byteutils, unittest2, json_serialization -import waku/[common/base64, waku_api/rest/serdes, waku_api/rest/relay/types, waku_core] +import + waku/ + [common/base64, rest_api/endpoint/serdes, rest_api/endpoint/relay/types, waku_core] suite "Waku v2 Rest API - Relay - serialization": suite "RelayWakuMessage - decode": diff --git a/tests/wakunode_rest/test_rest_serdes.nim b/tests/wakunode_rest/test_rest_serdes.nim index 719742bf8..2237e9216 100644 --- a/tests/wakunode_rest/test_rest_serdes.nim +++ b/tests/wakunode_rest/test_rest_serdes.nim @@ -1,9 +1,9 @@ {.used.} import results, stew/byteutils, chronicles, unittest2, json_serialization -import waku/waku_api/rest/serdes, waku/waku_api/rest/debug/types +import waku/rest_api/endpoint/serdes, waku/rest_api/endpoint/debug/types -# TODO: Decouple this test suite from the `debug_api` module by defining +# TODO: Decouple this test suite from the `debug_rest_interface` module by defining # private custom types for this test suite module suite "Waku v2 Rest API - Serdes": suite "decode": diff --git a/tests/wakunode_rest/test_rest_store.nim b/tests/wakunode_rest/test_rest_store.nim index b8882328b..01ccea9dd 100644 --- a/tests/wakunode_rest/test_rest_store.nim +++ b/tests/wakunode_rest/test_rest_store.nim @@ -17,12 +17,12 @@ import waku_core/time, waku_node, node/peer_manager, - waku_api/rest/server, - waku_api/rest/client, - waku_api/rest/responses, - waku_api/rest/store/handlers as store_api, - waku_api/rest/store/client as store_api_client, - waku_api/rest/store/types, + rest_api/endpoint/server, + rest_api/endpoint/client, + rest_api/endpoint/responses, + rest_api/endpoint/store/handlers as store_rest_interface, + rest_api/endpoint/store/client as store_rest_client, + rest_api/endpoint/store/types, waku_archive, waku_archive/driver/queue_driver, waku_archive/driver/sqlite_driver, @@ -34,7 +34,7 @@ import ../testlib/wakunode logScope: - topics = "waku node rest store_api test" + topics = "waku node rest store_rest_interface test" proc put( store: ArchiveDriver, pubsubTopic: PubsubTopic, message: WakuMessage @@ -115,17 +115,16 @@ procSuite "Waku Rest API - Store v3": await sleepAsync(1.seconds()) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 1, byte 2], ts = 2), - fakeWakuMessage(@[byte 1], ts = 3), - fakeWakuMessage(@[byte 1], ts = 4), - fakeWakuMessage(@[byte 1], ts = 5), - fakeWakuMessage(@[byte 1], ts = 6), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("c2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 1, byte 2], ts = 2), + fakeWakuMessage(@[byte 1], ts = 3), + fakeWakuMessage(@[byte 1], ts = 4), + fakeWakuMessage(@[byte 1], ts = 5), + fakeWakuMessage(@[byte 1], ts = 6), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("c2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -191,17 +190,16 @@ procSuite "Waku Rest API - Store v3": peerSwitch.mount(node.wakuStore) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 1, byte 2], ts = 2), - fakeWakuMessage(@[byte 1], ts = 3), - fakeWakuMessage(@[byte 1], ts = 4), - fakeWakuMessage(@[byte 1], ts = 5), - fakeWakuMessage(@[byte 1], ts = 6), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("c2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 1, byte 2], ts = 2), + fakeWakuMessage(@[byte 1], ts = 3), + fakeWakuMessage(@[byte 1], ts = 4), + fakeWakuMessage(@[byte 1], ts = 5), + fakeWakuMessage(@[byte 1], ts = 6), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("c2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -262,19 +260,18 @@ procSuite "Waku Rest API - Store v3": # Now prime it with some history before tests let timeOrigin = wakucore.now() - let msgList = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let msgList = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -357,12 +354,11 @@ procSuite "Waku Rest API - Store v3": peerSwitch.mount(node.wakuStore) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("2"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -431,12 +427,11 @@ procSuite "Waku Rest API - Store v3": peerSwitch.mount(node.wakuStore) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -485,7 +480,7 @@ procSuite "Waku Rest API - Store v3": $response.contentType == $MIMETYPE_TEXT response.data.messages.len == 0 response.data.statusDesc == - "Failed parsing remote peer info [MultiAddress.init [multiaddress: Invalid MultiAddress, must start with `/`]]" + "Failed parsing remote peer info: MultiAddress.init [multiaddress: Invalid MultiAddress, must start with `/`]" await restServer.stop() await restServer.closeWait() @@ -521,12 +516,11 @@ procSuite "Waku Rest API - Store v3": peerSwitch.mount(node.wakuStore) # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -594,12 +588,11 @@ procSuite "Waku Rest API - Store v3": await node.mountStore() # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage(@[byte 0], contentTopic = ContentTopic("ct1"), ts = 0), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -640,14 +633,13 @@ procSuite "Waku Rest API - Store v3": await node.mountStore() # Now prime it with some history before tests - let msgList = - @[ - fakeWakuMessage( - @[byte 0], contentTopic = ContentTopic("ct1"), ts = 0, meta = (@[byte 8]) - ), - fakeWakuMessage(@[byte 1], ts = 1), - fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), - ] + let msgList = @[ + fakeWakuMessage( + @[byte 0], contentTopic = ContentTopic("ct1"), ts = 0, meta = (@[byte 8]) + ), + fakeWakuMessage(@[byte 1], ts = 1), + fakeWakuMessage(@[byte 9], contentTopic = ContentTopic("ct2"), ts = 9), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() @@ -757,19 +749,18 @@ procSuite "Waku Rest API - Store v3": # Now prime it with some history before tests let timeOrigin = wakucore.now() - let msgList = - @[ - fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), - fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), - fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), - fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), - fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), - fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), - fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), - fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), - fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), - fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), - ] + let msgList = @[ + fakeWakuMessage(@[byte 00], ts = ts(00, timeOrigin)), + fakeWakuMessage(@[byte 01], ts = ts(10, timeOrigin)), + fakeWakuMessage(@[byte 02], ts = ts(20, timeOrigin)), + fakeWakuMessage(@[byte 03], ts = ts(30, timeOrigin)), + fakeWakuMessage(@[byte 04], ts = ts(40, timeOrigin)), + fakeWakuMessage(@[byte 05], ts = ts(50, timeOrigin)), + fakeWakuMessage(@[byte 06], ts = ts(60, timeOrigin)), + fakeWakuMessage(@[byte 07], ts = ts(70, timeOrigin)), + fakeWakuMessage(@[byte 08], ts = ts(80, timeOrigin)), + fakeWakuMessage(@[byte 09], ts = ts(90, timeOrigin)), + ] for msg in msgList: require (await driver.put(DefaultPubsubTopic, msg)).isOk() diff --git a/tools/confutils/cli_args.nim b/tools/confutils/cli_args.nim index fd1e3a576..f965c3a06 100644 --- a/tools/confutils/cli_args.nim +++ b/tools/confutils/cli_args.nim @@ -30,11 +30,14 @@ import waku_core/message/default_values, waku_mix, ], - ../../tools/rln_keystore_generator/rln_keystore_generator + ../../tools/rln_keystore_generator/rln_keystore_generator, + ./entry_nodes import ./envvar as confEnvvarDefs, ./envvar_net as confEnvvarNet -export confTomlDefs, confTomlNet, confEnvvarDefs, confEnvvarNet, ProtectedShard +export + confTomlDefs, confTomlNet, confEnvvarDefs, confEnvvarNet, ProtectedShard, + DefaultMaxWakuMessageSizeStr logScope: topics = "waku cli args" @@ -50,6 +53,11 @@ type StartUpCommand* = enum noCommand # default, runs waku generateRlnKeystore # generates a new RLN keystore +type WakuMode* {.pure.} = enum + noMode # default - use explicit CLI flags as-is + Core # full service node + Edge # client-only node + type WakuNodeConf* = object configFile* {. desc: "Loads configuration from a TOML file (cmd-line parameters take precedence)", @@ -148,9 +156,16 @@ type WakuNodeConf* = object .}: seq[ProtectedShard] ## General node config + mode* {. + desc: + "Node operation mode. 'Core' enables relay+service protocols. 'Edge' enables client-only protocols. Default: explicit CLI flags used.", + defaultValue: WakuMode.noMode, + name: "mode" + .}: WakuMode + preset* {. desc: - "Network preset to use. 'twn' is The RLN-protected Waku Network (cluster 1). Overrides other values.", + "Network preset to use. 'twn' is The RLN-protected Waku Network (cluster 1). 'logos.dev' is the Logos Dev Network (cluster 2). 'logos.test' is the Logos Test Network (cluster 2). Overrides other values.", defaultValue: "", name: "preset" .}: string @@ -163,7 +178,7 @@ type WakuNodeConf* = object .}: uint16 agentString* {. - defaultValue: "nwaku-" & cli_args.git_version, + defaultValue: "logos-delivery-" & cli_args.git_version, desc: "Node agent string which is used as identifier in network", name: "agent-string" .}: string @@ -204,22 +219,17 @@ type WakuNodeConf* = object .}: bool maxConnections* {. - desc: "Maximum allowed number of libp2p connections.", - defaultValue: 50, + desc: + "Maximum allowed number of libp2p connections. (Default: 150) that's recommended value for better connectivity", + defaultValue: 150, name: "max-connections" .}: int - maxRelayPeers* {. - desc: - "Deprecated. Use relay-service-ratio instead. It represents the maximum allowed number of relay peers.", - name: "max-relay-peers" - .}: Option[int] - relayServiceRatio* {. desc: "This percentage ratio represents the relay peers to service peers. For example, 60:40, tells that 60% of the max-connections will be used for relay protocol and the other 40% of max-connections will be reserved for other service protocols (e.g., filter, lightpush, store, metadata, etc.)", - name: "relay-service-ratio", - defaultValue: "60:40" # 60:40 ratio of relay to service peers + defaultValue: "50:50", + name: "relay-service-ratio" .}: string colocationLimit* {. @@ -241,7 +251,10 @@ type WakuNodeConf* = object dnsAddrsNameServers* {. desc: "DNS name server IPs to query for DNS multiaddrs resolution. Argument may be repeated.", - defaultValue: @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")], + defaultValue: @[ + IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 1, 1, 1]), + IpAddress(family: IpAddressFamily.IPv4, address_v4: [1'u8, 0, 0, 1]), + ], name: "dns-addrs-name-server" .}: seq[IpAddress] @@ -253,8 +266,7 @@ type WakuNodeConf* = object ## Circuit-relay config isRelayClient* {. - desc: - """Set the node as a relay-client. + desc: """Set the node as a relay-client. Set it to true for nodes that run behind a NAT or firewall and hence would have reachability issues.""", defaultValue: false, @@ -296,6 +308,14 @@ hence would have reachability issues.""", name: "rln-relay-dynamic" .}: bool + entryNodes* {. + desc: + "Entry node address (enrtree:, enr:, or multiaddr). " & + "Automatically classified and distributed to DNS discovery, discv5 bootstrap, " & + "and static nodes. Argument may be repeated.", + name: "entry-node" + .}: seq[string] + staticnodes* {. desc: "Peer multiaddr to directly connect with. Argument may be repeated.", name: "staticnode" @@ -311,7 +331,7 @@ hence would have reachability issues.""", numShardsInNetwork* {. desc: "Enables autosharding and set number of shards in the cluster, set to `0` to use static sharding", - defaultValue: 1, + defaultValue: 0, name: "num-shards-in-network" .}: uint16 @@ -331,12 +351,6 @@ hence would have reachability issues.""", desc: "Enable/disable waku store protocol", defaultValue: false, name: "store" .}: bool - legacyStore* {. - desc: "Enable/disable support of Waku Store v2 as a service", - defaultValue: false, - name: "legacy-store" - .}: bool - storenode* {. desc: "Peer multiaddress to query for storage", defaultValue: "", @@ -345,7 +359,7 @@ hence would have reachability issues.""", storeMessageRetentionPolicy* {. desc: - "Message store retention policy. Time retention policy: 'time:'. Capacity retention policy: 'capacity:'. Size retention policy: 'size:'. Set to 'none' to disable.", + "Message store retention policy. Multiple policies may be provided as a semicolon-separated string and are applied as a union. Time retention policy: 'time:'. Capacity retention policy: 'capacity:'. Size retention policy: 'size:'. Set to 'none' to disable. Example: 'time:3600;size:1GB;capacity:100'.", defaultValue: "time:" & $2.days.seconds, name: "store-message-retention-policy" .}: string @@ -456,18 +470,21 @@ hence would have reachability issues.""", desc: """Adds an extra effort in the delivery/reception of messages by leveraging store-v3 requests. with the drawback of consuming some more bandwidth.""", - defaultValue: false, + defaultValue: true, name: "reliability" .}: bool ## REST HTTP config rest* {. - desc: "Enable Waku REST HTTP server: true|false", defaultValue: true, name: "rest" + desc: "Enable Waku REST HTTP server: true|false", + defaultValue: false, + name: "rest" .}: bool restAddress* {. desc: "Listening address of the REST HTTP server.", - defaultValue: parseIpAddress("127.0.0.1"), + defaultValue: + IpAddress(family: IpAddressFamily.IPv4, address_v4: [127'u8, 0, 0, 1]), name: "rest-address" .}: IpAddress @@ -507,7 +524,8 @@ with the drawback of consuming some more bandwidth.""", metricsServerAddress* {. desc: "Listening address of the metrics server.", - defaultValue: parseIpAddress("127.0.0.1"), + defaultValue: + IpAddress(family: IpAddressFamily.IPv4, address_v4: [127'u8, 0, 0, 1]), name: "metrics-server-address" .}: IpAddress @@ -624,6 +642,20 @@ with the drawback of consuming some more bandwidth.""", name: "mixnode" .}: seq[MixNodePubInfo] + # Kademlia Discovery config + enableKadDiscovery* {. + desc: + "Enable extended kademlia discovery. Can be enabled without bootstrap nodes for the first node in the network.", + defaultValue: false, + name: "enable-kad-discovery" + .}: bool + + kadBootstrapNodes* {. + desc: + "Peer multiaddr for kademlia discovery bootstrap node (must include /p2p/). Argument may be repeated.", + name: "kad-bootstrap-node" + .}: seq[string] + ## websocket config websocketSupport* {. desc: "Enable websocket: true|false", @@ -658,7 +690,7 @@ with the drawback of consuming some more bandwidth.""", desc: "Rate limit settings for different protocols." & "Format: protocol:volume/period" & - " Where 'protocol' can be one of: if not defined it means a global setting" & + " Where 'protocol' can be one of: if not defined it means a global setting" & " 'volume' and period must be an integer value. " & " 'unit' must be one of - hours, minutes, seconds, milliseconds respectively. " & "Argument may be repeated.", @@ -666,6 +698,12 @@ with the drawback of consuming some more bandwidth.""", name: "rate-limit" .}: seq[string] + localStoragePath* {. + desc: "Path to store local data.", + defaultValue: "./data", + name: "local-storage-path" + .}: string + ## Parsing # NOTE: Keys are different in nim-libp2p @@ -725,12 +763,11 @@ proc parseCmdArg*(T: type ProtectedShard, p: string): T = raise newException( ValueError, "Invalid format for protected shard expected shard:publickey" ) - let publicKey = secp256k1.SkPublicKey.fromHex(elements[1]) - if publicKey.isErr: + let publicKey = secp256k1.SkPublicKey.fromHex(elements[1]).valueOr: raise newException(ValueError, "Invalid public key") if isNumber(elements[0]): - return ProtectedShard(shard: uint16.parseCmdArg(elements[0]), key: publicKey.get()) + return ProtectedShard(shard: uint16.parseCmdArg(elements[0]), key: publicKey) # TODO: Remove when removing protected-topic configuration let shard = RelayShard.parse(elements[0]).valueOr: @@ -738,7 +775,7 @@ proc parseCmdArg*(T: type ProtectedShard, p: string): T = ValueError, "Invalid pubsub topic. Pubsub topics must be in the format /waku/2/rs//", ) - return ProtectedShard(shard: shard.shardId, key: publicKey.get()) + return ProtectedShard(shard: shard.shardId, key: publicKey) proc completeCmdArg*(T: type ProtectedShard, val: string): seq[string] = return @[] @@ -748,7 +785,7 @@ proc completeCmdArg*(T: type IpAddress, val: string): seq[string] = proc defaultListenAddress*(): IpAddress = # TODO: Should probably listen on both ipv4 and ipv6 by default. - (static parseIpAddress("0.0.0.0")) + (static IpAddress(family: IpAddressFamily.IPv4, address_v4: [0'u8, 0, 0, 0])) proc defaultColocationLimit*(): int = return DefaultColocationLimit @@ -897,18 +934,33 @@ proc toNetworkConf( "TWN - The Waku Network configuration will not be applied when `--cluster-id=1` is passed in future releases. Use `--preset=twn` instead." ) lcPreset = "twn" + if clusterId.isSome() and clusterId.get() == 2: + warn( + "Logos.dev - Logos.dev configuration will not be applied when `--cluster-id=2` is passed in future releases. Use `--preset=logos.dev` instead." + ) + lcPreset = "logos.dev" case lcPreset of "": ok(none(NetworkConf)) of "twn": ok(some(NetworkConf.TheWakuNetworkConf())) + of "logos.dev", "logosdev": + ok(some(NetworkConf.LogosDevConf())) + of "logos.test", "logostest": + ok(some(NetworkConf.LogosTestConf())) else: err("Invalid --preset value passed: " & lcPreset) proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = var b = WakuConfBuilder.init() + let networkConf = toNetworkConf(n.preset, some(n.clusterId)).valueOr: + return err("Error determining cluster from preset: " & $error) + + if networkConf.isSome(): + b.withNetworkConf(networkConf.get()) + b.withLogLevel(n.logLevel) b.withLogFormat(n.logFormat) @@ -937,12 +989,6 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withProtectedShards(n.protectedShards) b.withClusterId(n.clusterId) - let networkConf = toNetworkConf(n.preset, some(n.clusterId)).valueOr: - return err("Error determining cluster from preset: " & $error) - - if networkConf.isSome(): - b.withNetworkConf(networkConf.get()) - b.withAgentString(n.agentString) if n.nodeKey.isSome(): @@ -956,9 +1002,6 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withExtMultiAddrsOnly(n.extMultiAddrsOnly) b.withMaxConnections(n.maxConnections) - if n.maxRelayPeers.isSome(): - b.withMaxRelayPeers(n.maxRelayPeers.get()) - if n.relayServiceRatio != "": b.withRelayServiceRatio(n.relayServiceRatio) b.withColocationLimit(n.colocationLimit) @@ -975,6 +1018,26 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withRelayShardedPeerManagement(n.relayShardedPeerManagement) b.withStaticNodes(n.staticNodes) + # Process entry nodes - supports enrtree:, enr:, and multiaddress formats + if n.entryNodes.len > 0: + let (enrTreeUrls, bootstrapEnrs, staticNodesFromEntry) = processEntryNodes( + n.entryNodes + ).valueOr: + return err("Failed to process entry nodes: " & error) + + # Set ENRTree URLs for DNS discovery + if enrTreeUrls.len > 0: + for url in enrTreeUrls: + b.dnsDiscoveryConf.withEnrTreeUrl(url) + + # Set ENR records as bootstrap nodes for discv5 + if bootstrapEnrs.len > 0: + b.discv5Conf.withBootstrapNodes(bootstrapEnrs) + + # Add static nodes (multiaddrs and those extracted from ENR entries) + if staticNodesFromEntry.len > 0: + b.withStaticNodes(staticNodesFromEntry) + if n.numShardsInNetwork != 0: b.withNumShardsInCluster(n.numShardsInNetwork) b.withShardingConf(AutoSharding) @@ -989,8 +1052,7 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.withContentTopics(n.contentTopics) b.storeServiceConf.withEnabled(n.store) - b.storeServiceConf.withSupportV2(n.legacyStore) - b.storeServiceConf.withRetentionPolicy(n.storeMessageRetentionPolicy) + b.storeServiceConf.withRetentionPolicies(n.storeMessageRetentionPolicy) b.storeServiceConf.withDbUrl(n.storeMessageDbUrl) b.storeServiceConf.withDbVacuum(n.storeMessageDbVacuum) b.storeServiceConf.withDbMigration(n.storeMessageDbMigration) @@ -1062,6 +1124,33 @@ proc toWakuConf*(n: WakuNodeConf): ConfResult[WakuConf] = b.webSocketConf.withKeyPath(n.websocketSecureKeyPath) b.webSocketConf.withCertPath(n.websocketSecureCertPath) - b.rateLimitConf.withRateLimits(n.rateLimits) + if n.rateLimits.len > 0: + b.rateLimitConf.withRateLimits(n.rateLimits) + + b.withLocalStoragePath(n.localStoragePath) + + b.kademliaDiscoveryConf.withEnabled(n.enableKadDiscovery) + b.kademliaDiscoveryConf.withBootstrapNodes(n.kadBootstrapNodes) + + # Mode-driven configuration overrides + case n.mode + of WakuMode.Core: + b.withRelay(true) + b.filterServiceConf.withEnabled(true) + b.withLightPush(true) + b.discv5Conf.withEnabled(true) + b.withPeerExchange(true) + b.withRendezvous(true) + b.rateLimitConf.withRateLimitsIfNotAssigned( + @["filter:100/1s", "lightpush:5/1s", "px:5/1s"] + ) + of WakuMode.Edge: + b.withPeerExchange(true) + b.withRelay(false) + b.filterServiceConf.withEnabled(false) + b.withLightPush(false) + b.storeServiceConf.withEnabled(false) + of WakuMode.noMode: + discard # use explicit CLI flags as-is return b.build() diff --git a/tools/confutils/config_option_meta.nim b/tools/confutils/config_option_meta.nim new file mode 100644 index 000000000..1880fdef5 --- /dev/null +++ b/tools/confutils/config_option_meta.nim @@ -0,0 +1,143 @@ +import std/[macros] + +type ConfigOptionMeta* = object + fieldName*: string + typeName*: string + cliName*: string + desc*: string + defaultValue*: string + command*: string + +proc getPragmaValue(pragmaNode: NimNode, pragmaName: string): string {.compileTime.} = + if pragmaNode.kind != nnkPragma: + return "" + + for item in pragmaNode: + if item.kind == nnkExprColonExpr and item[0].eqIdent(pragmaName): + return item[1].repr + + return "" + +proc getFieldName(fieldNode: NimNode): string {.compileTime.} = + case fieldNode.kind + of nnkPragmaExpr: + if fieldNode.len >= 1: + return getFieldName(fieldNode[0]) + of nnkPostfix: + if fieldNode.len >= 2: + return getFieldName(fieldNode[1]) + of nnkIdent, nnkSym: + return fieldNode.strVal + else: + discard + + return fieldNode.repr + +proc getFieldAndPragma( + fieldDef: NimNode +): tuple[fieldName, typeName: string, pragmaNode: NimNode] {.compileTime.} = + if fieldDef.kind != nnkIdentDefs: + return ("", "", newNimNode(nnkEmpty)) + + let declaredField = fieldDef[0] + var typeNode = fieldDef[1] + var pragmaNode = newNimNode(nnkEmpty) + + if declaredField.kind == nnkPragmaExpr: + pragmaNode = declaredField[1] + elif typeNode.kind == nnkPragmaExpr: + pragmaNode = typeNode[1] + typeNode = typeNode[0] + + return (getFieldName(declaredField), typeNode.repr, pragmaNode) + +proc makeMetaNode( + fieldName, typeName, cliName, desc, defaultValue, command: string +): NimNode {.compileTime.} = + result = newTree( + nnkObjConstr, + ident("ConfigOptionMeta"), + newTree(nnkExprColonExpr, ident("fieldName"), newLit(fieldName)), + newTree(nnkExprColonExpr, ident("typeName"), newLit(typeName)), + newTree(nnkExprColonExpr, ident("cliName"), newLit(cliName)), + newTree(nnkExprColonExpr, ident("desc"), newLit(desc)), + newTree(nnkExprColonExpr, ident("defaultValue"), newLit(defaultValue)), + newTree(nnkExprColonExpr, ident("command"), newLit(command)), + ) + +macro extractConfigOptionMeta*(T: typedesc): untyped = + proc findFirstRecList(n: NimNode): NimNode {.compileTime.} = + if n.kind == nnkRecList: + return n + for child in n: + let found = findFirstRecList(child) + if not found.isNil: + return found + return nil + + proc collectRecList( + recList: NimNode, metas: var seq[NimNode], commandCtx: string + ) {.compileTime.} = + for child in recList: + case child.kind + of nnkIdentDefs: + let (fieldName, typeName, pragmaNode) = getFieldAndPragma(child) + if fieldName.len == 0: + continue + let cliName = block: + let n = getPragmaValue(pragmaNode, "name") + if n.len > 0: n else: fieldName + let desc = getPragmaValue(pragmaNode, "desc") + let defaultValue = getPragmaValue(pragmaNode, "defaultValue") + metas.add( + makeMetaNode(fieldName, typeName, cliName, desc, defaultValue, commandCtx) + ) + of nnkRecCase: + let discriminator = child[0] + if discriminator.kind == nnkIdentDefs: + let (fieldName, typeName, pragmaNode) = getFieldAndPragma(discriminator) + if fieldName.len > 0: + let cliName = block: + let n = getPragmaValue(pragmaNode, "name") + if n.len > 0: n else: fieldName + let desc = getPragmaValue(pragmaNode, "desc") + let defaultValue = getPragmaValue(pragmaNode, "defaultValue") + metas.add( + makeMetaNode(fieldName, typeName, cliName, desc, defaultValue, commandCtx) + ) + + for i in 1 ..< child.len: + let branch = child[i] + case branch.kind + of nnkOfBranch: + let branchCtx = branch[0].repr + for j in 1 ..< branch.len: + if branch[j].kind == nnkRecList: + collectRecList(branch[j], metas, branchCtx) + of nnkElse: + for j in 0 ..< branch.len: + if branch[j].kind == nnkRecList: + collectRecList(branch[j], metas, commandCtx) + else: + discard + else: + discard + + let typeInst = getTypeInst(T) + var targetType = T + if typeInst.kind == nnkBracketExpr and typeInst.len >= 2: + targetType = typeInst[1] + + let typeImpl = getImpl(targetType) + let recList = findFirstRecList(typeImpl) + if recList.isNil: + return newTree(nnkPrefix, ident("@"), newNimNode(nnkBracket)) + + var metas: seq[NimNode] = @[] + collectRecList(recList, metas, "") + + let bracket = newNimNode(nnkBracket) + for node in metas: + bracket.add(node) + + result = newTree(nnkPrefix, ident("@"), bracket) diff --git a/waku/api/entry_nodes.nim b/tools/confutils/entry_nodes.nim similarity index 100% rename from waku/api/entry_nodes.nim rename to tools/confutils/entry_nodes.nim diff --git a/tools/gen-nix-deps.sh b/tools/gen-nix-deps.sh new file mode 100755 index 000000000..d24641ecd --- /dev/null +++ b/tools/gen-nix-deps.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# Generates nix/deps.nix from nimble.lock using nix-prefetch-git. +# Usage: ./tools/gen-nix-deps.sh [nimble.lock] [nix/deps.nix] +set -euo pipefail + +usage() { + cat < + +Example: + $0 nimble.lock nix/deps.nix +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage; exit 0 +fi + +if [[ $# -ne 2 ]]; then + usage; exit 1 +fi + +LOCKFILE="$1" +OUTFILE="$2" + +command -v jq >/dev/null || { echo "error: jq required"; exit 1; } +command -v nix-prefetch-git >/dev/null || { echo "error: nix-prefetch-git required"; exit 1; } + +if [[ ! -f "$LOCKFILE" ]]; then + echo "[!] $LOCKFILE not found" + echo "[*] Generating $LOCKFILE via 'nimble lock'" + nimble lock +fi + +echo "[*] Generating $OUTFILE from $LOCKFILE" +mkdir -p "$(dirname "$OUTFILE")" + +TMPFILE=$(mktemp) +trap 'rm -f "$TMPFILE"' EXIT + +cat > "$TMPFILE" <<'EOF' +# AUTOGENERATED from nimble.lock — do not edit manually. +# Regenerate with: ./tools/gen-nix-deps.sh nimble.lock nix/deps.nix +{ pkgs }: + +{ +EOF + +jq -c ' + .packages + | to_entries[] + | select(.value.downloadMethod == "git") + | select(.key != "nim" and .key != "nimble") +' "$LOCKFILE" | while read -r entry; do + name=$(jq -r '.key' <<<"$entry") + url=$(jq -r '.value.url' <<<"$entry") + rev=$(jq -r '.value.vcsRevision' <<<"$entry") + + echo " [*] Prefetching $name @ $rev" + + sha=$(nix-prefetch-git \ + --url "$url" \ + --rev "$rev" \ + --fetch-submodules \ + | jq -r '.sha256') + + cat >> "$TMPFILE" <> "$TMPFILE" <<'EOF' +} +EOF + +mv "$TMPFILE" "$OUTFILE" +echo "[✓] Wrote $OUTFILE" diff --git a/tools/rln_keystore_generator/rln_keystore_generator.nim b/tools/rln_keystore_generator/rln_keystore_generator.nim index 36a3759c9..503e8d58e 100644 --- a/tools/rln_keystore_generator/rln_keystore_generator.nim +++ b/tools/rln_keystore_generator/rln_keystore_generator.nim @@ -31,12 +31,10 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = trace "configuration", conf = $conf # 2. generate credentials - let credentialRes = membershipKeyGen() - if credentialRes.isErr(): - error "failure while generating credentials", error = credentialRes.error - quit(1) + let credential = membershipKeyGen().valueOr: + error "failure while generating credentials", error = error + quit(QuitFailure) - let credential = credentialRes.get() info "credentials", idTrapdoor = credential.idTrapdoor.inHex(), idNullifier = credential.idNullifier.inHex(), @@ -45,7 +43,7 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = if not conf.execute: info "not executing, exiting" - quit(0) + quit(QuitSuccess) var onFatalErrorAction = proc(msg: string) {.gcsafe, closure.} = ## Action to be taken when an internal error occurs during the node run. @@ -66,20 +64,22 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = try: (waitFor groupManager.init()).isOkOr: error "failure while initializing OnchainGroupManager", error = $error - quit(1) + quit(QuitFailure) # handling the exception is required since waitFor raises an exception except Exception, CatchableError: error "failure while initializing OnchainGroupManager", error = getCurrentExceptionMsg() - quit(1) + quit(QuitFailure) # 4. register on-chain try: - waitFor groupManager.register(credential, conf.userMessageLimit) + (waitFor groupManager.register(credential, conf.userMessageLimit)).isOkOr: + error "Failed to register on-chain", error = error + quit(QuitFailure) except Exception, CatchableError: error "failure while registering credentials on-chain", error = getCurrentExceptionMsg() - quit(1) + quit(QuitFailure) info "Transaction hash", txHash = groupManager.registrationTxHash.get() @@ -99,11 +99,9 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = userMessageLimit: conf.userMessageLimit, ) - let persistRes = - addMembershipCredentials(conf.credPath, keystoreCred, conf.credPassword, RLNAppInfo) - if persistRes.isErr(): - error "failed to persist credentials", error = persistRes.error - quit(1) + addMembershipCredentials(conf.credPath, keystoreCred, conf.credPassword, RLNAppInfo).isOkOr: + error "failed to persist credentials", error = error + quit(QuitFailure) info "credentials persisted", path = conf.credPath @@ -111,5 +109,5 @@ proc doRlnKeystoreGenerator*(conf: RlnKeystoreGeneratorConf) = waitFor groupManager.stop() except CatchableError: error "failure while stopping OnchainGroupManager", error = getCurrentExceptionMsg() - quit(0) # 0 because we already registered on-chain - quit(0) + quit(QuitSuccess) # 0 because we already registered on-chain + quit(QuitSuccess) diff --git a/vendor/db_connector b/vendor/db_connector deleted file mode 160000 index 74aef399e..000000000 --- a/vendor/db_connector +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 74aef399e5c232f95c9fc5c987cebac846f09d62 diff --git a/vendor/dnsclient.nim b/vendor/dnsclient.nim deleted file mode 160000 index 23214235d..000000000 --- a/vendor/dnsclient.nim +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 23214235d4784d24aceed99bbfe153379ea557c8 diff --git a/vendor/nim-bearssl b/vendor/nim-bearssl deleted file mode 160000 index 11e798b62..000000000 --- a/vendor/nim-bearssl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 11e798b62b8e6beabe958e048e9e24c7e0f9ee63 diff --git a/vendor/nim-chronicles b/vendor/nim-chronicles deleted file mode 160000 index 54f5b7260..000000000 --- a/vendor/nim-chronicles +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 54f5b726025e8c7385e3a6529d3aa27454c6e6ff diff --git a/vendor/nim-chronos b/vendor/nim-chronos deleted file mode 160000 index 0646c444f..000000000 --- a/vendor/nim-chronos +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0646c444fce7c7ed08ef6f2c9a7abfd172ffe655 diff --git a/vendor/nim-confutils b/vendor/nim-confutils deleted file mode 160000 index e214b3992..000000000 --- a/vendor/nim-confutils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e214b3992a31acece6a9aada7d0a1ad37c928f3b diff --git a/vendor/nim-dnsdisc b/vendor/nim-dnsdisc deleted file mode 160000 index b71d029f4..000000000 --- a/vendor/nim-dnsdisc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b71d029f4da4ec56974d54c04518bada00e1b623 diff --git a/vendor/nim-eth b/vendor/nim-eth deleted file mode 160000 index d9135e6c3..000000000 --- a/vendor/nim-eth +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d9135e6c3c5d6d819afdfb566aa8d958756b73a8 diff --git a/vendor/nim-faststreams b/vendor/nim-faststreams deleted file mode 160000 index c3ac3f639..000000000 --- a/vendor/nim-faststreams +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c3ac3f639ed1d62f59d3077d376a29c63ac9750c diff --git a/vendor/nim-http-utils b/vendor/nim-http-utils deleted file mode 160000 index 79cbab146..000000000 --- a/vendor/nim-http-utils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 79cbab1460f4c0cdde2084589d017c43a3d7b4f1 diff --git a/vendor/nim-json-rpc b/vendor/nim-json-rpc deleted file mode 160000 index 9665c2650..000000000 --- a/vendor/nim-json-rpc +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9665c265035f49f5ff94bbffdeadde68e19d6221 diff --git a/vendor/nim-json-serialization b/vendor/nim-json-serialization deleted file mode 160000 index b65fd6a7e..000000000 --- a/vendor/nim-json-serialization +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b65fd6a7e64c864dabe40e7dfd6c7d07db0014ac diff --git a/vendor/nim-libbacktrace b/vendor/nim-libbacktrace deleted file mode 160000 index d8bd4ce5c..000000000 --- a/vendor/nim-libbacktrace +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d8bd4ce5c46bb6d2f984f6b3f3d7380897d95ecb diff --git a/vendor/nim-libp2p b/vendor/nim-libp2p deleted file mode 160000 index 0309685cd..000000000 --- a/vendor/nim-libp2p +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0309685cd27d4bf763c8b3be86a76c33bcfe67ea diff --git a/vendor/nim-metrics b/vendor/nim-metrics deleted file mode 160000 index ecf64c607..000000000 --- a/vendor/nim-metrics +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ecf64c6078d1276d3b7d9b3d931fbdb70004db11 diff --git a/vendor/nim-minilru b/vendor/nim-minilru deleted file mode 160000 index 0c4b2bce9..000000000 --- a/vendor/nim-minilru +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0c4b2bce959591f0a862e9b541ba43c6d0cf3476 diff --git a/vendor/nim-nat-traversal b/vendor/nim-nat-traversal deleted file mode 160000 index 860e18c37..000000000 --- a/vendor/nim-nat-traversal +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 860e18c37667b5dd005b94c63264560c35d88004 diff --git a/vendor/nim-presto b/vendor/nim-presto deleted file mode 160000 index 92b1c7ff1..000000000 --- a/vendor/nim-presto +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 92b1c7ff141e6920e1f8a98a14c35c1fa098e3be diff --git a/vendor/nim-regex b/vendor/nim-regex deleted file mode 160000 index 4593305ed..000000000 --- a/vendor/nim-regex +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4593305ed1e49731fc75af1dc572dd2559aad19c diff --git a/vendor/nim-results b/vendor/nim-results deleted file mode 160000 index df8113dda..000000000 --- a/vendor/nim-results +++ /dev/null @@ -1 +0,0 @@ -Subproject commit df8113dda4c2d74d460a8fa98252b0b771bf1f27 diff --git a/vendor/nim-secp256k1 b/vendor/nim-secp256k1 deleted file mode 160000 index 9dd3df621..000000000 --- a/vendor/nim-secp256k1 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9dd3df62124aae79d564da636bb22627c53c7676 diff --git a/vendor/nim-serialization b/vendor/nim-serialization deleted file mode 160000 index 6f525d544..000000000 --- a/vendor/nim-serialization +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6f525d5447d97256750ca7856faead03e562ed20 diff --git a/vendor/nim-sqlite3-abi b/vendor/nim-sqlite3-abi deleted file mode 160000 index bdf01cf42..000000000 --- a/vendor/nim-sqlite3-abi +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bdf01cf4236fb40788f0733466cdf6708783cbac diff --git a/vendor/nim-stew b/vendor/nim-stew deleted file mode 160000 index e57400149..000000000 --- a/vendor/nim-stew +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e5740014961438610d336cd81706582dbf2c96f0 diff --git a/vendor/nim-stint b/vendor/nim-stint deleted file mode 160000 index 470b78925..000000000 --- a/vendor/nim-stint +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 470b7892561b5179ab20bd389a69217d6213fe58 diff --git a/vendor/nim-taskpools b/vendor/nim-taskpools deleted file mode 160000 index 9e8ccc754..000000000 --- a/vendor/nim-taskpools +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9e8ccc754631ac55ac2fd495e167e74e86293edb diff --git a/vendor/nim-testutils b/vendor/nim-testutils deleted file mode 160000 index 94d68e796..000000000 --- a/vendor/nim-testutils +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 94d68e796c045d5b37cabc6be32d7bfa168f8857 diff --git a/vendor/nim-toml-serialization b/vendor/nim-toml-serialization deleted file mode 160000 index fea85b27f..000000000 --- a/vendor/nim-toml-serialization +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fea85b27f0badcf617033ca1bc05444b5fd8aa7a diff --git a/vendor/nim-unicodedb b/vendor/nim-unicodedb deleted file mode 160000 index 66f245871..000000000 --- a/vendor/nim-unicodedb +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 66f2458710dc641dd4640368f9483c8a0ec70561 diff --git a/vendor/nim-unittest2 b/vendor/nim-unittest2 deleted file mode 160000 index 8b51e99b4..000000000 --- a/vendor/nim-unittest2 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8b51e99b4a57fcfb31689230e75595f024543024 diff --git a/vendor/nim-web3 b/vendor/nim-web3 deleted file mode 160000 index 81ee8ce47..000000000 --- a/vendor/nim-web3 +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 81ee8ce479d86acb73be7c4f365328e238d9b4a3 diff --git a/vendor/nim-websock b/vendor/nim-websock deleted file mode 160000 index ebe308a79..000000000 --- a/vendor/nim-websock +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ebe308a79a7b440a11dfbe74f352be86a3883508 diff --git a/vendor/nim-zlib b/vendor/nim-zlib deleted file mode 160000 index daa8723fd..000000000 --- a/vendor/nim-zlib +++ /dev/null @@ -1 +0,0 @@ -Subproject commit daa8723fd32299d4ca621c837430c29a5a11e19a diff --git a/vendor/nimbus-build-system b/vendor/nimbus-build-system deleted file mode 160000 index e6c2c9da3..000000000 --- a/vendor/nimbus-build-system +++ /dev/null @@ -1 +0,0 @@ -Subproject commit e6c2c9da39c2d368d9cf420ac22692e99715d22c diff --git a/vendor/nimcrypto b/vendor/nimcrypto deleted file mode 160000 index 721fb99ee..000000000 --- a/vendor/nimcrypto +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 721fb99ee099b632eb86dfad1f0d96ee87583774 diff --git a/vendor/nph b/vendor/nph deleted file mode 160000 index c6e03162d..000000000 --- a/vendor/nph +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c6e03162dc2820d3088660f644818d7040e95791 diff --git a/vendor/waku-rlnv2-contract b/vendor/waku-rlnv2-contract index 58c6c9f4a..d9906ef40 160000 --- a/vendor/waku-rlnv2-contract +++ b/vendor/waku-rlnv2-contract @@ -1 +1 @@ -Subproject commit 58c6c9f4a789c2d93eab33ea68461001fe3157df +Subproject commit d9906ef40f1e113fcf51de4ad27c61aa45375c2d diff --git a/vendor/zerokit b/vendor/zerokit index a4bb3feb5..5e64cb882 160000 --- a/vendor/zerokit +++ b/vendor/zerokit @@ -1 +1 @@ -Subproject commit a4bb3feb5054e6fd24827adf204493e6e173437b +Subproject commit 5e64cb8822bee65eed6cf459f95ae72b80c6ba63 diff --git a/waku.nim b/waku.nim index 18d52741e..65a017c5a 100644 --- a/waku.nim +++ b/waku.nim @@ -1,10 +1,10 @@ ## Main module for using nwaku as a Nimble library -## +## ## This module re-exports the public API for creating and managing Waku nodes ## when using nwaku as a library dependency. -import waku/api/[api, api_conf] -export api, api_conf +import waku/api +export api import waku/factory/waku export waku diff --git a/waku.nimble b/waku.nimble index c63d20246..da5b87eb6 100644 --- a/waku.nimble +++ b/waku.nimble @@ -4,84 +4,171 @@ import os mode = ScriptMode.Verbose ### Package -version = "0.36.0" +version = "0.38.1" author = "Status Research & Development GmbH" description = "Waku, Private P2P Messaging for Resource-Restricted Devices" license = "MIT or Apache License 2.0" -#bin = @["build/waku"] + +const RequiredNimVersion = "2.2.4" + ## This is the nim compiler version that we are working on. Other versions may behave differently. +const RequiredNimbleVersion = "0.22.3" + ## Enforced nimble version to ensure a reproducible flow ### Dependencies requires "nim >= 2.2.4", + "chronos >= 4.2.0", + "taskpools", + # Logging & Configuration "chronicles", "confutils", - "chronos", - "dnsdisc", - "eth", - "json_rpc", - "libbacktrace", - "nimcrypto", + # Serialization "serialization", + "json_serialization", + "toml_serialization", + "faststreams", + # Networking & P2P + "https://github.com/vacp2p/nim-libp2p.git#ff8d51857b4b79a68468e7bcc27b2026cca02996", + "eth", + "nat_traversal", + "dnsdisc", + "dnsclient", + "httputils >= 0.4.1", + "websock >= 0.3.0", + # Cryptography + "nimcrypto == 0.6.4", # 0.6.4 used in libp2p. Version 0.7.3 makes test to crash on Ubuntu. + "secp256k1", + "bearssl", + # RPC & APIs + "https://github.com/status-im/nim-json-rpc.git#43bbf499143eb45046c83ac9794c9e3280a2b8e7", + "presto", + "web3", + # Database + "db_connector", + "sqlite3_abi", + # Utilities "stew", "stint", "metrics", - "libp2p >= 1.14.2", - "web3", - "presto", "regex", + "unicodedb", "results", - "db_connector", - "minilru" + "minilru", + "zlib", + # Debug & Testing + "testutils", + "unittest2" + +# Packages not on nimble (use git URLs) +requires "https://github.com/logos-messaging/nim-ffi" + +requires "https://github.com/logos-messaging/nim-sds.git#2e9a7683f0e180bf112135fae3a3803eed8490d4" + +# brokers: pinned by URL+commit rather than the bare `brokers >= 2.0.1` +# form because the nim-lang/packages registry entry for `brokers` only +# carries metadata for the original v0.1.0 publication. Until that +# registry entry is refreshed, the local SAT solver enumerates "0.1.0" +# as the only available version and cannot satisfy `>= 2.0.1`. The URL +# pin below bypasses the registry and locks the exact commit of the +# v2.0.1 tag. Revert to the bare form once nim-lang/packages is +# updated. +requires "https://github.com/NagyZoltanPeter/nim-brokers.git#v2.0.1" + +requires "https://github.com/vacp2p/nim-lsquic" +requires "https://github.com/vacp2p/nim-jwt.git#057ec95eb5af0eea9c49bfe9025b3312c95dc5f2" + +proc getMyCPU(): string = + ## Need to set cpu more explicit manner to avoid arch issues between dependencies + when defined(macosx) and defined(arm64): + return " --cpu:arm64 --passC:\"-arch arm64\" --passL:\"-arch arm64\" " + elif defined(macosx) and defined(amd64): + return " --cpu:amd64 --passC:\"-arch x86_64\" --passL:\"-arch x86_64\" " + elif defined(arm64): + return " --cpu:arm64 " + elif defined(amd64): + return " --cpu:amd64 " + +proc getNimParams(): string = + return " " & getEnv("NIM_PARAMS") & " " ### Helper functions -proc buildModule(filePath, params = "", lang = "c"): bool = +proc buildModule(filePath, params = ""): bool = if not dirExists "build": mkDir "build" - # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" - var extra_params = params - for i in 2 ..< paramCount() - 1: - extra_params &= " " & paramStr(i) if not fileExists(filePath): echo "File to build not found: " & filePath return false - exec "nim " & lang & " --out:build/" & filepath & ".bin --mm:refc " & extra_params & + exec "nim c --out:build/" & filepath & ".bin --mm:refc " & getMyCPU() & getNimParams() & " " & params & " " & filePath # exec will raise exception if anything goes wrong return true -proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = +proc buildBinary(name: string, srcDir = "./", params = "") = if not dirExists "build": mkDir "build" - # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" - var extra_params = params - for i in 2 ..< paramCount(): - extra_params &= " " & paramStr(i) - exec "nim " & lang & " --out:build/" & name & " --mm:refc " & extra_params & " " & + exec "nim c --out:build/" & name & " --mm:refc " & getMyCPU() & getNimParams() & " " & params & " " & srcDir & name & ".nim" -proc buildLibrary(name: string, srcDir = "./", params = "", `type` = "static") = +proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static", srcFile = "libwaku.nim", mainPrefix = "libwaku") = if not dirExists "build": mkDir "build" - # allow something like "nim nimbus --verbosity:0 --hints:off nimbus.nims" - var extra_params = params - for i in 2 ..< paramCount(): - extra_params &= " " & paramStr(i) + if `type` == "static": - exec "nim c" & " --out:build/" & name & - ".a --threads:on --app:staticlib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:libwaku --skipParentCfg:on -d:discv5_protocol_id=d5waku " & - extra_params & " " & srcDir & name & ".nim" + exec "nim c" & " --out:build/" & lib_name & + " --threads:on --app:staticlib --opt:speed --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:on -d:discv5_protocol_id=d5waku " & + getMyCPU() & getNimParams() & srcDir & "/" & srcFile else: - let lib_name = (when defined(windows): toDll(name) else: name & ".so") - when defined(windows): - exec "nim c" & " --out:build/" & lib_name & - " --threads:on --app:lib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:libwaku --skipParentCfg:off -d:discv5_protocol_id=d5waku " & - extra_params & " " & srcDir & name & ".nim" - else: - exec "nim c" & " --out:build/" & lib_name & - " --threads:on --app:lib --opt:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:libwaku --skipParentCfg:on -d:discv5_protocol_id=d5waku " & - extra_params & " " & srcDir & name & ".nim" + exec "nim c" & " --out:build/" & lib_name & + " --threads:on --app:lib --opt:speed --noMain --mm:refc --header -d:metrics --nimMainPrefix:" & mainPrefix & " --skipParentCfg:off -d:discv5_protocol_id=d5waku " & + getMyCPU() & getNimParams() & " " & srcDir & "/" & srcFile + +proc buildLibDynamicWindows(libName: string, folderName: string) = + buildLibrary libName & ".dll", folderName, + """-d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE """, + "dynamic", libName & ".nim", libname + +proc buildLibDynamicLinux(libName: string, folderName: string) = + buildLibrary libName & ".so", folderName, + """-d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE """, + "dynamic", libName & ".nim", libname + +proc buildLibDynamicMac(libName: string, folderName: string) = + let sdkPath = staticExec("xcrun --show-sdk-path").strip() + when defined(arm64): + let archFlags = "--cpu:arm64 --passC:\"-arch arm64\" --passL:\"-arch arm64\" --passC:\"-isysroot " & sdkPath & "\" --passL:\"-isysroot " & sdkPath & "\"" + elif defined(amd64): + let archFlags = "--cpu:amd64 --passC:\"-arch x86_64\" --passL:\"-arch x86_64\" --passC:\"-isysroot " & sdkPath & "\" --passL:\"-isysroot " & sdkPath & "\"" + else: + {.error: "Unsupported macOS architecture".} + buildLibrary libName & ".dylib", folderName, + archFlags & " -d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE", + "dynamic", libName & ".nim", libname + +proc buildLibStaticWindows(libName: string, folderName: string) = + buildLibrary libName & ".lib", folderName, + """-d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE """, + "static", libName & ".nim", libname + +proc buildLibStaticLinux(libName: string, folderName: string) = + buildLibrary libName & ".a", folderName, + """-d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE """, + "static", libName & ".nim", libname + +proc buildLibStaticMac(libName: string, folderName: string) = + let sdkPath = staticExec("xcrun --show-sdk-path").strip() + when defined(arm64): + let archFlags = "--cpu:arm64 --passC:\"-arch arm64\" --passL:\"-arch arm64\" --passC:\"-isysroot " & sdkPath & "\" --passL:\"-isysroot " & sdkPath & "\"" + elif defined(amd64): + let archFlags = "--cpu:amd64 --passC:\"-arch x86_64\" --passL:\"-arch x86_64\" --passC:\"-isysroot " & sdkPath & "\" --passL:\"-isysroot " & sdkPath & "\"" + else: + {.error: "Unsupported macOS architecture".} + buildLibrary libName & ".a", folderName, + archFlags & " -d:chronicles_line_numbers --warning:Deprecated:off --warning:UnusedImport:on -d:chronicles_log_level=TRACE", + "static", libName & ".nim", libname + +### Mobile Android proc buildMobileAndroid(srcDir = ".", params = "") = let cpu = getEnv("CPU") @@ -91,16 +178,206 @@ proc buildMobileAndroid(srcDir = ".", params = "") = if not dirExists outDir: mkDir outDir - var extra_params = params - for i in 2 ..< paramCount(): - extra_params &= " " & paramStr(i) - exec "nim c" & " --out:" & outDir & - "/libwaku.so --threads:on --app:lib --opt:size --noMain --mm:refc -d:chronicles_sinks=textlines[dynamic] --header --passL:-L" & - outdir & " --passL:-lrln --passL:-llog --cpu:" & cpu & " --os:android -d:androidNDK " & - extra_params & " " & srcDir & "/libwaku.nim" + "/liblogosdelivery.so --threads:on --app:lib --opt:speed --noMain --mm:refc -d:chronicles_sinks=textlines[dynamic] --header -d:chronosEventEngine=epoll --passL:-L" & + outdir & " --passL:-lrln --passL:-llog --cpu:" & cpu & " --nimMainPrefix:liblogosdelivery --os:android -d:androidNDK " & params & + getNimParams() & " " & srcDir & "/liblogosdelivery.nim" -proc test(name: string, params = "-d:chronicles_log_level=DEBUG", lang = "c") = +task libLogosDeliveryAndroid, "Build the mobile bindings for Android": + let srcDir = "./library" + buildMobileAndroid srcDir, "-d:chronicles_log_level=ERROR" + +### Mobile iOS + +import std/sequtils + +proc buildMobileIOS(srcDir = ".", params = "") = + echo "Building iOS liblogosdelivery library" + + let iosArch = getEnv("IOS_ARCH") + let iosSdk = getEnv("IOS_SDK") + let sdkPath = getEnv("IOS_SDK_PATH") + + if sdkPath.len == 0: + quit "Error: IOS_SDK_PATH not set. Set it to the path of the iOS SDK" + + # Get nimble package paths + let bearsslPath = gorge("nimble path bearssl").strip() + let secp256k1Path = gorge("nimble path secp256k1").strip() + let natTraversalPath = gorge("nimble path nat_traversal").strip() + + # Get Nim standard library path + let nimPath = gorge("nim --fullhelp 2>&1 | head -1 | sed 's/.*\\[//' | sed 's/\\].*//'").strip() + let nimLibPath = nimPath.parentDir.parentDir / "lib" + + # Use SDK name in path to differentiate device vs simulator + let outDir = "build/ios/" & iosSdk & "-" & iosArch + if not dirExists outDir: + mkDir outDir + + var extra_params = params + let args = commandLineParams() + for arg in args: + extra_params &= " " & arg + + let cpu = if iosArch == "arm64": "arm64" else: "amd64" + + # The output static library + let nimcacheDir = outDir & "/nimcache" + let objDir = outDir & "/obj" + let vendorObjDir = outDir & "/vendor_obj" + let aFile = outDir & "/liblogosdelivery.a" + + if not dirExists objDir: + mkDir objDir + if not dirExists vendorObjDir: + mkDir vendorObjDir + + let clangBase = "clang -arch " & iosArch & " -isysroot " & sdkPath & + " -mios-version-min=18.0 -fembed-bitcode -fPIC -O2" + + # Generate C sources from Nim (no linking) + exec "nim c" & + " --nimcache:" & nimcacheDir & + " --os:ios --cpu:" & cpu & + " --compileOnly:on" & + " --noMain --mm:refc" & + " --threads:on --opt:size --header" & + " -d:metrics -d:discv5_protocol_id=d5waku" & + " --nimMainPrefix:liblogosdelivery --skipParentCfg:on" & + " --cc:clang" & + " " & extra_params & + " " & srcDir & "/liblogosdelivery.nim" + + # Compile vendor C libraries for iOS + + # --- BearSSL --- + echo "Compiling BearSSL for iOS..." + let bearSslSrcDir = bearsslPath / "bearssl/csources/src" + let bearSslIncDir = bearsslPath / "bearssl/csources/inc" + for path in walkDirRec(bearSslSrcDir): + if path.endsWith(".c"): + let relPath = path.replace(bearSslSrcDir & "/", "").replace("/", "_") + let baseName = relPath.changeFileExt("o") + let oFile = vendorObjDir / ("bearssl_" & baseName) + if not fileExists(oFile): + exec clangBase & " -I" & bearSslIncDir & " -I" & bearSslSrcDir & " -c " & path & " -o " & oFile + + # --- secp256k1 --- + echo "Compiling secp256k1 for iOS..." + let secp256k1Dir = secp256k1Path / "vendor/secp256k1" + let secp256k1Flags = " -I" & secp256k1Dir & "/include" & + " -I" & secp256k1Dir & "/src" & + " -I" & secp256k1Dir & + " -DENABLE_MODULE_RECOVERY=1" & + " -DENABLE_MODULE_ECDH=1" & + " -DECMULT_WINDOW_SIZE=15" & + " -DECMULT_GEN_PREC_BITS=4" + + # Main secp256k1 source + let secp256k1Obj = vendorObjDir / "secp256k1.o" + if not fileExists(secp256k1Obj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/secp256k1.c -o " & secp256k1Obj + + # Precomputed tables (required for ecmult operations) + let secp256k1PreEcmultObj = vendorObjDir / "secp256k1_precomputed_ecmult.o" + if not fileExists(secp256k1PreEcmultObj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult.c -o " & secp256k1PreEcmultObj + + let secp256k1PreEcmultGenObj = vendorObjDir / "secp256k1_precomputed_ecmult_gen.o" + if not fileExists(secp256k1PreEcmultGenObj): + exec clangBase & secp256k1Flags & " -c " & secp256k1Dir & "/src/precomputed_ecmult_gen.c -o " & secp256k1PreEcmultGenObj + + # --- miniupnpc --- + echo "Compiling miniupnpc for iOS..." + let miniupnpcSrcDir = natTraversalPath / "vendor/miniupnp/miniupnpc/src" + let miniupnpcIncDir = natTraversalPath / "vendor/miniupnp/miniupnpc/include" + let miniupnpcBuildDir = natTraversalPath / "vendor/miniupnp/miniupnpc/build" + let miniupnpcFiles = @[ + "addr_is_reserved.c", "connecthostport.c", "igd_desc_parse.c", + "minisoap.c", "minissdpc.c", "miniupnpc.c", "miniwget.c", + "minixml.c", "portlistingparse.c", "receivedata.c", "upnpcommands.c", + "upnpdev.c", "upnperrors.c", "upnpreplyparse.c" + ] + for fileName in miniupnpcFiles: + let srcPath = miniupnpcSrcDir / fileName + let oFile = vendorObjDir / ("miniupnpc_" & fileName.changeFileExt("o")) + if fileExists(srcPath) and not fileExists(oFile): + exec clangBase & + " -I" & miniupnpcIncDir & + " -I" & miniupnpcSrcDir & + " -I" & miniupnpcBuildDir & + " -DMINIUPNPC_SET_SOCKET_TIMEOUT" & + " -D_BSD_SOURCE -D_DEFAULT_SOURCE" & + " -c " & srcPath & " -o " & oFile + + # --- libnatpmp --- + echo "Compiling libnatpmp for iOS..." + let natpmpSrcDir = natTraversalPath / "vendor/libnatpmp-upstream" + # Only compile natpmp.c - getgateway.c uses net/route.h which is not available on iOS + let natpmpObj = vendorObjDir / "natpmp_natpmp.o" + if not fileExists(natpmpObj): + exec clangBase & + " -I" & natpmpSrcDir & + " -DENABLE_STRNATPMPERR" & + " -c " & natpmpSrcDir & "/natpmp.c -o " & natpmpObj + + # Use iOS-specific stub for getgateway + let getgatewayStubSrc = "./library/ios_natpmp_stubs.c" + let getgatewayStubObj = vendorObjDir / "natpmp_getgateway_stub.o" + if fileExists(getgatewayStubSrc) and not fileExists(getgatewayStubObj): + exec clangBase & " -c " & getgatewayStubSrc & " -o " & getgatewayStubObj + + # --- BearSSL stubs (for tools functions not in main library) --- + echo "Compiling BearSSL stubs for iOS..." + let bearSslStubsSrc = "./library/ios_bearssl_stubs.c" + let bearSslStubsObj = vendorObjDir / "bearssl_stubs.o" + if fileExists(bearSslStubsSrc) and not fileExists(bearSslStubsObj): + exec clangBase & " -c " & bearSslStubsSrc & " -o " & bearSslStubsObj + + # Compile all Nim-generated C files to object files + echo "Compiling Nim-generated C files for iOS..." + var cFiles: seq[string] = @[] + for kind, path in walkDir(nimcacheDir): + if kind == pcFile and path.endsWith(".c"): + cFiles.add(path) + + for cFile in cFiles: + let baseName = extractFilename(cFile).changeFileExt("o") + let oFile = objDir / baseName + exec clangBase & + " -DENABLE_STRNATPMPERR" & + " -I" & nimLibPath & + " -I" & bearsslPath & "/bearssl/csources/inc/" & + " -I" & bearsslPath & "/bearssl/csources/tools/" & + " -I" & bearsslPath & "/bearssl/abi/" & + " -I" & secp256k1Path & "/vendor/secp256k1/include/" & + " -I" & natTraversalPath & "/vendor/miniupnp/miniupnpc/include/" & + " -I" & natTraversalPath & "/vendor/libnatpmp-upstream/" & + " -I" & nimcacheDir & + " -c " & cFile & + " -o " & oFile + + # Create static library from all object files + echo "Creating static library..." + var objFiles: seq[string] = @[] + for kind, path in walkDir(objDir): + if kind == pcFile and path.endsWith(".o"): + objFiles.add(path) + for kind, path in walkDir(vendorObjDir): + if kind == pcFile and path.endsWith(".o"): + objFiles.add(path) + + exec "libtool -static -o " & aFile & " " & objFiles.join(" ") + + echo "iOS library created: " & aFile + +task libWakuIOS, "Build the mobile bindings for iOS": + let srcDir = "./library" + let extraParams = "-d:chronicles_log_level=ERROR" + buildMobileIOS srcDir, extraParams + +proc test(name: string, params = "-d:chronicles_log_level=DEBUG") = # XXX: When running `> NIM_PARAMS="-d:chronicles_log_level=INFO" make test2` # I expect compiler flag to be overridden, however it stays with whatever is # specified here. @@ -109,12 +386,12 @@ proc test(name: string, params = "-d:chronicles_log_level=DEBUG", lang = "c") = ### Waku common tasks task testcommon, "Build & run common tests": - test "all_tests_common", "-d:chronicles_log_level=WARN -d:chronosStrictException" + test "all_tests_common", "-d:chronicles_log_level=DEBUG -d:chronosStrictException" ### Waku tasks task wakunode2, "Build Waku v2 cli node": let name = "wakunode2" - buildBinary name, "apps/wakunode2/", " -d:chronicles_log_level='TRACE' " + buildBinary name, "apps/wakunode2/", " -d:chronicles_log_level=TRACE " task benchmarks, "Some benchmarks": let name = "benchmarks" @@ -139,7 +416,7 @@ task testwakunode2, "Build & run wakunode2 app tests": test "all_tests_wakunode2" task example2, "Build Waku examples": - buildBinary "waku_example", "examples/" + buildBinary "api_example", "examples/api_example/" buildBinary "publisher", "examples/" buildBinary "subscriber", "examples/" buildBinary "filter_subscriber", "examples/" @@ -153,7 +430,8 @@ task chat2, "Build example Waku chat usage": let name = "chat2" buildBinary name, "apps/chat2/", - "-d:chronicles_sinks=textlines[file] -d:ssl -d:chronicles_log_level='TRACE' " + "-d:chronicles_sinks=textlines[file] -d:chronicles_log_level=TRACE " + # -d:ssl - cause unlisted exception error in libp2p/utility... task chat2mix, "Build example Waku chat mix usage": # NOTE For debugging, set debug level. For chat usage we want minimal log @@ -163,7 +441,8 @@ task chat2mix, "Build example Waku chat mix usage": let name = "chat2mix" buildBinary name, "apps/chat2mix/", - "-d:chronicles_sinks=textlines[file] -d:ssl -d:chronicles_log_level='TRACE' " + "-d:chronicles_sinks=textlines[file] -d:chronicles_log_level=TRACE " + # -d:ssl - cause unlisted exception error in libp2p/utility... task chat2bridge, "Build chat2bridge": let name = "chat2bridge" @@ -171,28 +450,33 @@ task chat2bridge, "Build chat2bridge": task liteprotocoltester, "Build liteprotocoltester": let name = "liteprotocoltester" - buildBinary name, "apps/liteprotocoltester/" + buildBinary name, "apps/liteprotocoltester/", "-d:chronicles_log_level=TRACE" task lightpushwithmix, "Build lightpushwithmix": let name = "lightpush_publisher_mix" buildBinary name, "examples/lightpush_mix/" -task buildone, "Build custom target": - let filepath = paramStr(paramCount()) - discard buildModule filepath - task buildTest, "Test custom target": - let filepath = paramStr(paramCount()) + let args = commandLineParams() + if args.len == 0: + quit "Missing test file" + + let filepath = args[^1] discard buildModule(filepath) import std/strutils task execTest, "Run test": - # Expects to be parameterized with test case name in quotes - # preceded with the nim source file name and path - # If no test case name is given still it requires empty quotes `""` - let filepath = paramStr(paramCount() - 1) - var testSuite = paramStr(paramCount()).strip(chars = {'\"'}) + let args = commandLineParams() + if args.len == 0: + quit "Missing arguments" + # expects: "" + let filepath = + if args.len >= 2: args[^2] + else: args[^1] + var testSuite = + if args.len >= 1: args[^1].strip(chars = {'\"'}) + else: "" if testSuite != "": testSuite = " \"" & testSuite & "\"" exec "build/" & filepath & ".bin " & testSuite @@ -205,16 +489,42 @@ let chroniclesParams = """-d:chronicles_disabled_topics="eth,dnsdisc.client" """ & "--warning:Deprecated:off " & "--warning:UnusedImport:on " & "-d:chronicles_log_level=TRACE" -task libwakuStatic, "Build the cbindings waku node library": - let name = "libwaku" - buildLibrary name, "library/", chroniclesParams, "static" +## Libwaku build tasks -task libwakuDynamic, "Build the cbindings waku node library": - let name = "libwaku" - buildLibrary name, "library/", chroniclesParams, "dynamic" +task libwakuDynamicWindows, "Generate bindings": + buildLibDynamicWindows("libwaku", "library") -### Mobile Android -task libWakuAndroid, "Build the mobile bindings for Android": - let srcDir = "./library" - let extraParams = "-d:chronicles_log_level=ERROR" - buildMobileAndroid srcDir, extraParams +task libwakuDynamicLinux, "Generate bindings": + buildLibDynamicLinux("libwaku", "library") + +task libwakuDynamicMac, "Generate bindings": + buildLibDynamicMac("libwaku", "library") + +task libwakuStaticWindows, "Generate bindings": + buildLibStaticWindows("libwaku", "library") + +task libwakuStaticLinux, "Generate bindings": + buildLibStaticLinux("libwaku", "library") + +task libwakuStaticMac, "Generate bindings": + buildLibStaticMac("libwaku", "library") + +## Liblogosdelivery build tasks + +task liblogosdeliveryDynamicWindows, "Generate bindings": + buildLibDynamicWindows("liblogosdelivery", "liblogosdelivery") + +task liblogosdeliveryDynamicLinux, "Generate bindings": + buildLibDynamicLinux("liblogosdelivery", "liblogosdelivery") + +task liblogosdeliveryDynamicMac, "Generate bindings": + buildLibDynamicMac("liblogosdelivery", "liblogosdelivery") + +task liblogosdeliveryStaticWindows, "Generate bindings": + buildLibStaticWindows("liblogosdelivery", "liblogosdelivery") + +task liblogosdeliveryStaticLinux, "Generate bindings": + buildLibStaticLinux("liblogosdelivery", "liblogosdelivery") + +task liblogosdeliveryStaticMac, "Generate bindings": + buildLibStaticMac("liblogosdelivery", "liblogosdelivery") diff --git a/waku/README.md b/waku/README.md index ed3887a09..d9f160cb5 100644 --- a/waku/README.md +++ b/waku/README.md @@ -45,15 +45,16 @@ Setting up a `wakunode2` on the smallest [digital ocean](https://docs.digitaloce make test ``` -To run a specific test. +To run a specific test file or test case: ```bash -# Get a shell with the right environment variables set -./env.sh bash -# Run a specific test -nim c -r ./tests/test_waku_filter_legacy.nim +# Run all tests in a specific file +make test tests/waku_filter_v2/test_waku_filter.nim + +# Run a specific test case within a file +make test tests/waku_filter_v2/test_waku_filter.nim "specific test name" ``` -You can also alter compile options. For example, if you want a less verbose output you can do the following. For more, refer to the [compiler flags](https://nim-lang.org/docs/nimc.html#compiler-usage) and [chronicles documentation](https://github.com/status-im/nim-chronicles#compile-time-configuration). +Alternatively, you can invoke the Nim compiler directly. For more on available flags, refer to the [compiler flags](https://nim-lang.org/docs/nimc.html#compiler-usage) and [chronicles documentation](https://github.com/status-im/nim-chronicles#compile-time-configuration). ```bash nim c -r -d:chronicles_log_level=WARN --verbosity=0 --hints=off ./tests/waku_filter_v2/test_waku_filter.nim @@ -231,7 +232,4 @@ However, they can be used for local testing purposes: mkdir -p ./ssl_dir/ openssl req -x509 -newkey rsa:4096 -keyout ./ssl_dir/key.pem -out ./ssl_dir/cert.pem -sha256 -nodes wakunode2 --websocket-secure-support=true --websocket-secure-key-path="./ssl_dir/key.pem" --websocket-secure-cert-path="./ssl_dir/cert.pem" -``` - - - +``` \ No newline at end of file diff --git a/waku/api.nim b/waku/api.nim new file mode 100644 index 000000000..a977a062a --- /dev/null +++ b/waku/api.nim @@ -0,0 +1,5 @@ +import ./api/[api, api_conf] +import ./events/message_events +import tools/confutils/entry_nodes + +export api, api_conf, entry_nodes, message_events diff --git a/waku/api/api.nim b/waku/api/api.nim index 5bab06188..1eee982fd 100644 --- a/waku/api/api.nim +++ b/waku/api/api.nim @@ -1,12 +1,20 @@ import chronicles, chronos, results import waku/factory/waku +import waku/[requests/health_requests, waku_core, waku_node] +import waku/node/delivery_service/send_service +import waku/node/delivery_service/subscription_manager +import libp2p/peerid +import ../../tools/confutils/cli_args +import ./[api_conf, types] -import ./api_conf +export cli_args -# TODO: Specs says it should return a `WakuNode`. As `send` and other APIs are defined, we can align. -proc createNode*(config: NodeConfig): Future[Result[Waku, string]] {.async.} = - let wakuConf = toWakuConf(config).valueOr: +logScope: + topics = "api" + +proc createNode*(conf: WakuNodeConf): Future[Result[Waku, string]] {.async.} = + let wakuConf = conf.toWakuConf().valueOr: return err("Failed to handle the configuration: " & error) ## We are not defining app callbacks at node creation @@ -15,3 +23,54 @@ proc createNode*(config: NodeConfig): Future[Result[Waku, string]] {.async.} = return err("Failed setting up Waku: " & $error) return ok(wakuRes) + +proc checkApiAvailability(w: Waku): Result[void, string] = + if w.isNil(): + return err("Waku node is not initialized") + + # TODO: Conciliate request-bouncing health checks here with unit testing. + # (For now, better to just allow all sends and rely on retries.) + + return ok() + +proc subscribe*( + w: Waku, contentTopic: ContentTopic +): Future[Result[void, string]] {.async.} = + ?checkApiAvailability(w) + + return w.deliveryService.subscriptionManager.subscribe(contentTopic) + +proc unsubscribe*(w: Waku, contentTopic: ContentTopic): Result[void, string] = + ?checkApiAvailability(w) + + return w.deliveryService.subscriptionManager.unsubscribe(contentTopic) + +proc send*( + w: Waku, envelope: MessageEnvelope +): Future[Result[RequestId, string]] {.async.} = + ?checkApiAvailability(w) + + let isSubbed = w.deliveryService.subscriptionManager + .isSubscribed(envelope.contentTopic) + .valueOr(false) + if not isSubbed: + info "Auto-subscribing to topic on send", contentTopic = envelope.contentTopic + w.deliveryService.subscriptionManager.subscribe(envelope.contentTopic).isOkOr: + warn "Failed to auto-subscribe", error = error + return err("Failed to auto-subscribe before sending: " & error) + + let requestId = RequestId.new(w.rng) + + let deliveryTask = DeliveryTask.new(requestId, envelope, w.brokerCtx).valueOr: + return err("API send: Failed to create delivery task: " & error) + + info "API send: scheduling delivery task", + requestId = $requestId, + pubsubTopic = deliveryTask.pubsubTopic, + contentTopic = deliveryTask.msg.contentTopic, + msgHash = deliveryTask.msgHash.to0xHex(), + myPeerId = w.node.peerId() + + asyncSpawn w.deliveryService.sendService.send(deliveryTask) + + return ok(requestId) diff --git a/waku/api/api_conf.nim b/waku/api/api_conf.nim index 155554dfd..3606be596 100644 --- a/waku/api/api_conf.nim +++ b/waku/api/api_conf.nim @@ -1,32 +1,36 @@ import std/[net, options] import results +import json_serialization, json_serialization/std/options as json_options import waku/common/utils/parse_size_units, + waku/common/logging, waku/factory/waku_conf, waku/factory/conf_builder/conf_builder, waku/factory/networks_config, - ./entry_nodes + tools/confutils/entry_nodes -type AutoShardingConfig* {.requiresInit.} = object +export json_serialization, json_options + +type AutoShardingConfig* = object numShardsInCluster*: uint16 -type RlnConfig* {.requiresInit.} = object +type RlnConfig* = object contractAddress*: string chainId*: uint epochSizeSec*: uint64 -type NetworkingConfig* {.requiresInit.} = object +type NetworkingConfig* = object listenIpv4*: string p2pTcpPort*: uint16 discv5UdpPort*: uint16 -type MessageValidation* {.requiresInit.} = object +type MessageValidation* = object maxMessageSize*: string # Accepts formats like "150 KiB", "1500 B" rlnConfig*: Option[RlnConfig] -type ProtocolsConfig* {.requiresInit.} = object +type ProtocolsConfig* = object entryNodes: seq[string] staticStoreNodes: seq[string] clusterId: uint16 @@ -58,10 +62,9 @@ proc init*( ) const TheWakuNetworkPreset* = ProtocolsConfig( - entryNodes: - @[ - "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im" - ], + entryNodes: @[ + "enrtree://AIRVQ5DDA4FFWLRBCHJWUWOO6X6S4ZTZ5B667LQ6AJU6PEYDLRD5O@sandbox.waku.nodes.status.im" + ], staticStoreNodes: @[], clusterId: 1, autoShardingConfig: AutoShardingConfig(numShardsInCluster: 8), @@ -81,11 +84,16 @@ type WakuMode* {.pure.} = enum Edge Core -type NodeConfig* {.requiresInit.} = object +type NodeConfig* {. + requiresInit, deprecated: "Use WakuNodeConf from tools/confutils/cli_args instead" +.} = object mode: WakuMode protocolsConfig: ProtocolsConfig networkingConfig: NetworkingConfig ethRpcEndpoints: seq[string] + p2pReliability: bool + logLevel: LogLevel + logFormat: LogFormat proc init*( T: typedesc[NodeConfig], @@ -93,17 +101,69 @@ proc init*( protocolsConfig: ProtocolsConfig = TheWakuNetworkPreset, networkingConfig: NetworkingConfig = DefaultNetworkingConfig, ethRpcEndpoints: seq[string] = @[], + p2pReliability: bool = false, + logLevel: LogLevel = LogLevel.INFO, + logFormat: LogFormat = LogFormat.TEXT, ): T = return T( mode: mode, protocolsConfig: protocolsConfig, networkingConfig: networkingConfig, ethRpcEndpoints: ethRpcEndpoints, + p2pReliability: p2pReliability, + logLevel: logLevel, + logFormat: logFormat, ) -proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = +# -- Getters for ProtocolsConfig (private fields) - used for testing -- + +proc entryNodes*(c: ProtocolsConfig): seq[string] = + c.entryNodes + +proc staticStoreNodes*(c: ProtocolsConfig): seq[string] = + c.staticStoreNodes + +proc clusterId*(c: ProtocolsConfig): uint16 = + c.clusterId + +proc autoShardingConfig*(c: ProtocolsConfig): AutoShardingConfig = + c.autoShardingConfig + +proc messageValidation*(c: ProtocolsConfig): MessageValidation = + c.messageValidation + +# -- Getters for NodeConfig (private fields) - used for testing -- + +proc mode*(c: NodeConfig): WakuMode = + c.mode + +proc protocolsConfig*(c: NodeConfig): ProtocolsConfig = + c.protocolsConfig + +proc networkingConfig*(c: NodeConfig): NetworkingConfig = + c.networkingConfig + +proc ethRpcEndpoints*(c: NodeConfig): seq[string] = + c.ethRpcEndpoints + +proc p2pReliability*(c: NodeConfig): bool = + c.p2pReliability + +proc logLevel*(c: NodeConfig): LogLevel = + c.logLevel + +proc logFormat*(c: NodeConfig): LogFormat = + c.logFormat + +proc toWakuConf*( + nodeConfig: NodeConfig +): Result[WakuConf, string] {.deprecated: "Use WakuNodeConf.toWakuConf instead".} = var b = WakuConfBuilder.init() + # Apply log configuration + b.withLogLevel(nodeConfig.logLevel) + b.withLogFormat(nodeConfig.logFormat) + # Apply networking configuration let networkingConfig = nodeConfig.networkingConfig let ip = parseIpAddress(networkingConfig.listenIpv4) @@ -131,7 +191,16 @@ proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = b.rateLimitConf.withRateLimits(@["filter:100/1s", "lightpush:5/1s", "px:5/1s"]) of Edge: - return err("Edge mode is not implemented") + # All client side protocols are mounted by default + # Peer exchange client is always enabled and start_node will start the px loop + # Metadata is always mounted + b.withPeerExchange(true) + # switch off all service side protocols and relay + b.withRelay(false) + b.filterServiceConf.withEnabled(false) + b.withLightPush(false) + b.storeServiceConf.withEnabled(false) + # Leave discv5 and rendezvous for user choice ## Network Conf let protocolsConfig = nodeConfig.protocolsConfig @@ -193,6 +262,7 @@ proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = ## Various configurations b.withNatStrategy("any") + b.withP2PReliability(nodeConfig.p2pReliability) let wakuConf = b.build().valueOr: return err("Failed to build configuration: " & error) @@ -201,3 +271,263 @@ proc toWakuConf*(nodeConfig: NodeConfig): Result[WakuConf, string] = return err("Failed to validate configuration: " & error) return ok(wakuConf) + +# ---- JSON serialization (writeValue / readValue) ---- +# ---------- AutoShardingConfig ---------- + +proc writeValue*(w: var JsonWriter, val: AutoShardingConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("numShardsInCluster", val.numShardsInCluster) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var AutoShardingConfig +) {.raises: [SerializationError, IOError].} = + var numShardsInCluster: Option[uint16] + + for fieldName in readObjectFields(r): + case fieldName + of "numShardsInCluster": + numShardsInCluster = some(r.readValue(uint16)) + else: + r.raiseUnexpectedField(fieldName, "AutoShardingConfig") + + if numShardsInCluster.isNone(): + r.raiseUnexpectedValue("Missing required field 'numShardsInCluster'") + + val = AutoShardingConfig(numShardsInCluster: numShardsInCluster.get()) + +# ---------- RlnConfig ---------- + +proc writeValue*(w: var JsonWriter, val: RlnConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("contractAddress", val.contractAddress) + w.writeField("chainId", val.chainId) + w.writeField("epochSizeSec", val.epochSizeSec) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var RlnConfig +) {.raises: [SerializationError, IOError].} = + var + contractAddress: Option[string] + chainId: Option[uint] + epochSizeSec: Option[uint64] + + for fieldName in readObjectFields(r): + case fieldName + of "contractAddress": + contractAddress = some(r.readValue(string)) + of "chainId": + chainId = some(r.readValue(uint)) + of "epochSizeSec": + epochSizeSec = some(r.readValue(uint64)) + else: + r.raiseUnexpectedField(fieldName, "RlnConfig") + + if contractAddress.isNone(): + r.raiseUnexpectedValue("Missing required field 'contractAddress'") + if chainId.isNone(): + r.raiseUnexpectedValue("Missing required field 'chainId'") + if epochSizeSec.isNone(): + r.raiseUnexpectedValue("Missing required field 'epochSizeSec'") + + val = RlnConfig( + contractAddress: contractAddress.get(), + chainId: chainId.get(), + epochSizeSec: epochSizeSec.get(), + ) + +# ---------- NetworkingConfig ---------- + +proc writeValue*(w: var JsonWriter, val: NetworkingConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("listenIpv4", val.listenIpv4) + w.writeField("p2pTcpPort", val.p2pTcpPort) + w.writeField("discv5UdpPort", val.discv5UdpPort) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var NetworkingConfig +) {.raises: [SerializationError, IOError].} = + var + listenIpv4: Option[string] + p2pTcpPort: Option[uint16] + discv5UdpPort: Option[uint16] + + for fieldName in readObjectFields(r): + case fieldName + of "listenIpv4": + listenIpv4 = some(r.readValue(string)) + of "p2pTcpPort": + p2pTcpPort = some(r.readValue(uint16)) + of "discv5UdpPort": + discv5UdpPort = some(r.readValue(uint16)) + else: + r.raiseUnexpectedField(fieldName, "NetworkingConfig") + + if listenIpv4.isNone(): + r.raiseUnexpectedValue("Missing required field 'listenIpv4'") + if p2pTcpPort.isNone(): + r.raiseUnexpectedValue("Missing required field 'p2pTcpPort'") + if discv5UdpPort.isNone(): + r.raiseUnexpectedValue("Missing required field 'discv5UdpPort'") + + val = NetworkingConfig( + listenIpv4: listenIpv4.get(), + p2pTcpPort: p2pTcpPort.get(), + discv5UdpPort: discv5UdpPort.get(), + ) + +# ---------- MessageValidation ---------- + +proc writeValue*(w: var JsonWriter, val: MessageValidation) {.raises: [IOError].} = + w.beginRecord() + w.writeField("maxMessageSize", val.maxMessageSize) + w.writeField("rlnConfig", val.rlnConfig) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var MessageValidation +) {.raises: [SerializationError, IOError].} = + var + maxMessageSize: Option[string] + rlnConfig: Option[Option[RlnConfig]] + + for fieldName in readObjectFields(r): + case fieldName + of "maxMessageSize": + maxMessageSize = some(r.readValue(string)) + of "rlnConfig": + rlnConfig = some(r.readValue(Option[RlnConfig])) + else: + r.raiseUnexpectedField(fieldName, "MessageValidation") + + if maxMessageSize.isNone(): + r.raiseUnexpectedValue("Missing required field 'maxMessageSize'") + + val = MessageValidation( + maxMessageSize: maxMessageSize.get(), rlnConfig: rlnConfig.get(none(RlnConfig)) + ) + +# ---------- ProtocolsConfig ---------- + +proc writeValue*(w: var JsonWriter, val: ProtocolsConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("entryNodes", val.entryNodes) + w.writeField("staticStoreNodes", val.staticStoreNodes) + w.writeField("clusterId", val.clusterId) + w.writeField("autoShardingConfig", val.autoShardingConfig) + w.writeField("messageValidation", val.messageValidation) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var ProtocolsConfig +) {.raises: [SerializationError, IOError].} = + var + entryNodes: Option[seq[string]] + staticStoreNodes: Option[seq[string]] + clusterId: Option[uint16] + autoShardingConfig: Option[AutoShardingConfig] + messageValidation: Option[MessageValidation] + + for fieldName in readObjectFields(r): + case fieldName + of "entryNodes": + entryNodes = some(r.readValue(seq[string])) + of "staticStoreNodes": + staticStoreNodes = some(r.readValue(seq[string])) + of "clusterId": + clusterId = some(r.readValue(uint16)) + of "autoShardingConfig": + autoShardingConfig = some(r.readValue(AutoShardingConfig)) + of "messageValidation": + messageValidation = some(r.readValue(MessageValidation)) + else: + r.raiseUnexpectedField(fieldName, "ProtocolsConfig") + + if entryNodes.isNone(): + r.raiseUnexpectedValue("Missing required field 'entryNodes'") + if clusterId.isNone(): + r.raiseUnexpectedValue("Missing required field 'clusterId'") + + val = ProtocolsConfig.init( + entryNodes = entryNodes.get(), + staticStoreNodes = staticStoreNodes.get(@[]), + clusterId = clusterId.get(), + autoShardingConfig = autoShardingConfig.get(DefaultAutoShardingConfig), + messageValidation = messageValidation.get(DefaultMessageValidation), + ) + +# ---------- NodeConfig ---------- + +proc writeValue*(w: var JsonWriter, val: NodeConfig) {.raises: [IOError].} = + w.beginRecord() + w.writeField("mode", val.mode) + w.writeField("protocolsConfig", val.protocolsConfig) + w.writeField("networkingConfig", val.networkingConfig) + w.writeField("ethRpcEndpoints", val.ethRpcEndpoints) + w.writeField("p2pReliability", val.p2pReliability) + w.writeField("logLevel", val.logLevel) + w.writeField("logFormat", val.logFormat) + w.endRecord() + +proc readValue*( + r: var JsonReader, val: var NodeConfig +) {.raises: [SerializationError, IOError].} = + var + mode: Option[WakuMode] + protocolsConfig: Option[ProtocolsConfig] + networkingConfig: Option[NetworkingConfig] + ethRpcEndpoints: Option[seq[string]] + p2pReliability: Option[bool] + logLevel: Option[LogLevel] + logFormat: Option[LogFormat] + + for fieldName in readObjectFields(r): + case fieldName + of "mode": + mode = some(r.readValue(WakuMode)) + of "protocolsConfig": + protocolsConfig = some(r.readValue(ProtocolsConfig)) + of "networkingConfig": + networkingConfig = some(r.readValue(NetworkingConfig)) + of "ethRpcEndpoints": + ethRpcEndpoints = some(r.readValue(seq[string])) + of "p2pReliability": + p2pReliability = some(r.readValue(bool)) + of "logLevel": + logLevel = some(r.readValue(LogLevel)) + of "logFormat": + logFormat = some(r.readValue(LogFormat)) + else: + r.raiseUnexpectedField(fieldName, "NodeConfig") + + val = NodeConfig.init( + mode = mode.get(WakuMode.Core), + protocolsConfig = protocolsConfig.get(TheWakuNetworkPreset), + networkingConfig = networkingConfig.get(DefaultNetworkingConfig), + ethRpcEndpoints = ethRpcEndpoints.get(@[]), + p2pReliability = p2pReliability.get(false), + logLevel = logLevel.get(LogLevel.INFO), + logFormat = logFormat.get(LogFormat.TEXT), + ) + +# ---------- Decode helper ---------- +# Json.decode returns T via `result`, which conflicts with {.requiresInit.} +# on Nim 2.x. This helper avoids the issue by using readValue into a var. + +proc decodeNodeConfigFromJson*( + jsonStr: string +): NodeConfig {. + raises: [SerializationError], + deprecated: "Use WakuNodeConf with fieldPairs-based JSON parsing instead" +.} = + var val = NodeConfig.init() # default-initialized + try: + var stream = unsafeMemoryInput(jsonStr) + var reader = (JsonReader[DefaultFlavor].init(stream)) + reader.readValue(val) + except IOError as err: + raise (ref SerializationError)(msg: err.msg) + return val diff --git a/waku/api/send_api.md b/waku/api/send_api.md new file mode 100644 index 000000000..2a5a2f8a4 --- /dev/null +++ b/waku/api/send_api.md @@ -0,0 +1,46 @@ +# SEND API + +**THIS IS TO BE REMOVED BEFORE PR MERGE** + +This document collects logic and todo's around the Send API. + +## Overview + +Send api hides the complex logic of using raw protocols for reliable message delivery. +The delivery method is chosen based on the node configuration and actual availabilities of peers. + +## Delivery task + +Each message send request is bundled into a task that not just holds the composed message but also the state of the delivery. + +## Delivery methods + +Depending on the configuration and the availability of store client protocol + actual configured and/or discovered store nodes: +- P2PReliability validation - checking network store node whether the message is reached at least a store node. +- Simple retry until message is propagated to the network + - Relay says >0 peers as publish result + - LightpushClient returns with success + +Depending on node config: +- Relay +- Lightpush + +These methods are used in combination to achieve the best reliability. +Fallback mechanism is used to switch between methods if the current one fails. + +Relay+StoreCheck -> Relay+simple retry -> Lightpush+StoreCheck -> Lightpush simple retry -> Error + +Combination is dynamically chosen on node configuration. Levels can be skipped depending on actual connectivity. +Actual connectivity is checked: +- Relay's topic health check - at least dLow peers in the mesh for the topic +- Store nodes availability - at least one store service node is available in peer manager +- Lightpush client availability - at least one lightpush service node is available in peer manager + +## Delivery processing + +At every send request, each task is tried to be delivered right away. +Any further retries and store check is done as a background task in a loop with predefined intervals. +Each task is set for a maximum number of retries and/or maximum time to live. + +In each round of store check and retry send tasks are selected based on their state. +The state is updated based on the result of the delivery method. diff --git a/waku/api/types.nim b/waku/api/types.nim new file mode 100644 index 000000000..9eae503c8 --- /dev/null +++ b/waku/api/types.nim @@ -0,0 +1,65 @@ +{.push raises: [].} + +import bearssl/rand, std/times, chronos +import stew/byteutils +import waku/utils/requests as request_utils +import waku/waku_core/[topics/content_topic, message/message, time] +import waku/requests/requests + +type + MessageEnvelope* = object + contentTopic*: ContentTopic + payload*: seq[byte] + ephemeral*: bool + + RequestId* = distinct string + + ConnectionStatus* {.pure.} = enum + Disconnected + PartiallyConnected + Connected + +proc new*(T: typedesc[RequestId], rng: ref HmacDrbgContext): T = + ## Generate a new RequestId using the provided RNG. + RequestId(request_utils.generateRequestId(rng)) + +proc `$`*(r: RequestId): string {.inline.} = + string(r) + +proc `==`*(a, b: RequestId): bool {.inline.} = + string(a) == string(b) + +proc init*( + T: type MessageEnvelope, + contentTopic: ContentTopic, + payload: seq[byte] | string, + ephemeral: bool = false, +): MessageEnvelope = + when payload is seq[byte]: + MessageEnvelope(contentTopic: contentTopic, payload: payload, ephemeral: ephemeral) + else: + MessageEnvelope( + contentTopic: contentTopic, payload: payload.toBytes(), ephemeral: ephemeral + ) + +proc toWakuMessage*(envelope: MessageEnvelope): WakuMessage = + ## Convert a MessageEnvelope to a WakuMessage. + var wm = WakuMessage( + contentTopic: envelope.contentTopic, + payload: envelope.payload, + ephemeral: envelope.ephemeral, + timestamp: getNowInNanosecondTime(), + ) + + ## TODO: First find out if proof is needed at all + ## Follow up: left it to the send logic to add RLN proof if needed and possible + # let requestedProof = ( + # waitFor RequestGenerateRlnProof.request(wm, getTime().toUnixFloat()) + # ).valueOr: + # warn "Failed to add RLN proof to WakuMessage: ", error = error + # return wm + + # wm.proof = requestedProof.proof + return wm + +{.pop.} diff --git a/waku/common/callbacks.nim b/waku/common/callbacks.nim index 9b8590152..83209ef24 100644 --- a/waku/common/callbacks.nim +++ b/waku/common/callbacks.nim @@ -1,5 +1,7 @@ -import ../waku_enr/capabilities +import waku/waku_enr/capabilities, waku/waku_rendezvous/waku_peer_record type GetShards* = proc(): seq[uint16] {.closure, gcsafe, raises: [].} type GetCapabilities* = proc(): seq[Capabilities] {.closure, gcsafe, raises: [].} + +type GetWakuPeerRecord* = proc(): WakuPeerRecord {.closure, gcsafe, raises: [].} diff --git a/waku/common/databases/db_postgres/dbconn.nim b/waku/common/databases/db_postgres/dbconn.nim index ee758730a..f24f3d4dd 100644 --- a/waku/common/databases/db_postgres/dbconn.nim +++ b/waku/common/databases/db_postgres/dbconn.nim @@ -48,8 +48,8 @@ proc check(db: DbConn): Result[void, string] = return err("exception in check: " & getCurrentExceptionMsg()) if message.len > 0: - let truncatedErr = message[0 .. 80] - ## libpq sometimes gives extremely long error messages + let truncatedErr = message[0 ..< min(80, message.len)] + error "postgres check issue. see truncated db error.", error = truncatedErr return err(truncatedErr) return ok() @@ -63,9 +63,8 @@ proc openDbConn(connString: string): Result[DbConn, string] = return err("exception opening new connection: " & getCurrentExceptionMsg()) if conn.status != CONNECTION_OK: - let checkRes = conn.check() - if checkRes.isErr(): - return err("failed to connect to database: " & checkRes.error) + conn.check().isOkOr: + return err("failed to connect to database: " & error) return err("unknown reason") @@ -212,11 +211,10 @@ proc waitQueryToFinish( pqclear(pqResult) proc containsRiskyPatterns(input: string): bool = - let riskyPatterns = - @[ - " OR ", " AND ", " UNION ", " SELECT ", "INSERT ", "DELETE ", "UPDATE ", "DROP ", - "EXEC ", "--", "/*", "*/", - ] + let riskyPatterns = @[ + " OR ", " AND ", " UNION ", " SELECT ", "INSERT ", "DELETE ", "UPDATE ", "DROP ", + "EXEC ", "--", "/*", "*/", + ] for pattern in riskyPatterns: if pattern.toLowerAscii() in input.toLowerAscii(): diff --git a/waku/common/databases/db_postgres/pgasyncpool.nim b/waku/common/databases/db_postgres/pgasyncpool.nim index 5f8bf40be..0b298084e 100644 --- a/waku/common/databases/db_postgres/pgasyncpool.nim +++ b/waku/common/databases/db_postgres/pgasyncpool.nim @@ -174,8 +174,8 @@ proc runStmt*( let len = paramValues.len discard dbConnWrapper.getDbConn().prepare(stmtName, sql(stmtDefinition), len) - if res.isErr(): - return err("failed prepare in runStmt: " & res.error.msg) + res.isOkOr: + return err("failed prepare in runStmt: " & error.msg) pool.conns[connIndex].inclPreparedStmt(stmtName) diff --git a/waku/common/databases/db_sqlite.nim b/waku/common/databases/db_sqlite.nim index a28668cde..e398ea5ac 100644 --- a/waku/common/databases/db_sqlite.nim +++ b/waku/common/databases/db_sqlite.nim @@ -265,8 +265,7 @@ proc getPageSize*(db: SqliteDatabase): DatabaseResult[int64] = proc handler(s: RawStmtPtr) = size = sqlite3_column_int64(s, 0) - let res = db.query("PRAGMA page_size;", handler) - if res.isErr(): + db.query("PRAGMA page_size;", handler).isOkOr: return err("failed to get page_size") return ok(size) @@ -277,8 +276,7 @@ proc getFreelistCount*(db: SqliteDatabase): DatabaseResult[int64] = proc handler(s: RawStmtPtr) = count = sqlite3_column_int64(s, 0) - let res = db.query("PRAGMA freelist_count;", handler) - if res.isErr(): + db.query("PRAGMA freelist_count;", handler).isOkOr: return err("failed to get freelist_count") return ok(count) @@ -289,8 +287,7 @@ proc getPageCount*(db: SqliteDatabase): DatabaseResult[int64] = proc handler(s: RawStmtPtr) = count = sqlite3_column_int64(s, 0) - let res = db.query("PRAGMA page_count;", handler) - if res.isErr(): + db.query("PRAGMA page_count;", handler).isOkOr: return err("failed to get page_count") return ok(count) @@ -319,8 +316,7 @@ proc gatherSqlitePageStats*(db: SqliteDatabase): DatabaseResult[(int64, int64, i proc vacuum*(db: SqliteDatabase): DatabaseResult[void] = ## The VACUUM command rebuilds the database file, repacking it into a minimal amount of disk space. - let res = db.query("VACUUM;", NoopRowHandler) - if res.isErr(): + db.query("VACUUM;", NoopRowHandler).isOkOr: return err("vacuum failed") return ok() @@ -339,8 +335,7 @@ proc getUserVersion*(database: SqliteDatabase): DatabaseResult[int64] = proc handler(s: ptr sqlite3_stmt) = version = sqlite3_column_int64(s, 0) - let res = database.query("PRAGMA user_version;", handler) - if res.isErr(): + database.query("PRAGMA user_version;", handler).isOkOr: return err("failed to get user_version") ok(version) @@ -354,8 +349,7 @@ proc setUserVersion*(database: SqliteDatabase, version: int64): DatabaseResult[v ## ## For more info check: https://www.sqlite.org/pragma.html#pragma_user_version let query = "PRAGMA user_version=" & $version & ";" - let res = database.query(query, NoopRowHandler) - if res.isErr(): + database.query(query, NoopRowHandler).isOkOr: return err("failed to set user_version") ok() @@ -400,11 +394,9 @@ proc filterMigrationScripts( if direction != "" and not script.toLower().endsWith("." & direction & ".sql"): return false - let scriptVersionRes = getMigrationScriptVersion(script) - if scriptVersionRes.isErr(): + let scriptVersion = getMigrationScriptVersion(script).valueOr: return false - let scriptVersion = scriptVersionRes.value return lowVersion < scriptVersion and scriptVersion <= highVersion paths.filter(filterPredicate) @@ -476,10 +468,9 @@ proc migrate*( for statement in script.breakIntoStatements(): info "executing migration statement", statement = statement - let execRes = db.query(statement, NoopRowHandler) - if execRes.isErr(): + db.query(statement, NoopRowHandler).isOkOr: error "failed to execute migration statement", - statement = statement, error = execRes.error + statement = statement, error = error return err("failed to execute migration statement") info "migration statement executed succesfully", statement = statement @@ -497,9 +488,8 @@ proc performSqliteVacuum*(db: SqliteDatabase): DatabaseResult[void] = info "starting sqlite database vacuuming" - let resVacuum = db.vacuum() - if resVacuum.isErr(): - return err("failed to execute vacuum: " & resVacuum.error) + db.vacuum().isOkOr: + return err("failed to execute vacuum: " & error) info "finished sqlite database vacuuming" ok() diff --git a/waku/common/enr/typed_record.nim b/waku/common/enr/typed_record.nim index d0b055ac4..1db357621 100644 --- a/waku/common/enr/typed_record.nim +++ b/waku/common/enr/typed_record.nim @@ -65,11 +65,10 @@ func id*(record: TypedRecord): Option[RecordId] = if fieldOpt.isNone(): return none(RecordId) - let fieldRes = toRecordId(fieldOpt.get()) - if fieldRes.isErr(): + let field = toRecordId(fieldOpt.get()).valueOr: return none(RecordId) - some(fieldRes.value) + return some(field) func secp256k1*(record: TypedRecord): Option[array[33, byte]] = record.tryGet("secp256k1", array[33, byte]) diff --git a/waku/common/rate_limit/per_peer_limiter.nim b/waku/common/rate_limit/per_peer_limiter.nim index 5cb96a2d1..16b6bf065 100644 --- a/waku/common/rate_limit/per_peer_limiter.nim +++ b/waku/common/rate_limit/per_peer_limiter.nim @@ -20,7 +20,7 @@ proc mgetOrPut( perPeerRateLimiter: var PerPeerRateLimiter, peerId: PeerId ): var Option[TokenBucket] = return perPeerRateLimiter.peerBucket.mgetOrPut( - peerId, newTokenBucket(perPeerRateLimiter.setting, ReplenishMode.Compensating) + peerId, newTokenBucket(perPeerRateLimiter.setting, ReplenishMode.Continuous) ) template checkUsageLimit*( diff --git a/waku/common/rate_limit/request_limiter.nim b/waku/common/rate_limit/request_limiter.nim index 0ede20be4..bc318e151 100644 --- a/waku/common/rate_limit/request_limiter.nim +++ b/waku/common/rate_limit/request_limiter.nim @@ -39,38 +39,82 @@ const SECONDS_RATIO = 3 const MINUTES_RATIO = 2 type RequestRateLimiter* = ref object of RootObj - tokenBucket: Option[TokenBucket] + tokenBucket: TokenBucket setting*: Option[RateLimitSetting] + mainBucketSetting: RateLimitSetting + ratio: int peerBucketSetting*: RateLimitSetting peerUsage: TimedMap[PeerId, TokenBucket] + checkUsageImpl: proc( + t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment + ): bool {.gcsafe, raises: [].} + +proc newMainTokenBucket( + setting: RateLimitSetting, ratio: int, startTime: Moment +): TokenBucket = + ## RequestRateLimiter's global bucket should keep the *rate* of the configured + ## setting while allowing a larger burst window. We achieve this by scaling + ## both capacity and fillDuration by the same ratio. + ## + ## This matches previous behavior where unused tokens could effectively + ## accumulate across multiple periods. + let burstCapacity = setting.volume * ratio + var bucket = TokenBucket.new( + capacity = burstCapacity, + fillDuration = setting.period * ratio, + startTime = startTime, + mode = Continuous, + ) + + # Start with the configured volume (not the burst capacity) so that the + # initial burst behavior matches the raw setting, while still allowing + # accumulation up to `burstCapacity` over time. + let excess = burstCapacity - setting.volume + if excess > 0: + discard bucket.tryConsume(excess, startTime) + + return bucket proc mgetOrPut( - requestRateLimiter: var RequestRateLimiter, peerId: PeerId + requestRateLimiter: var RequestRateLimiter, peerId: PeerId, now: Moment ): var TokenBucket = - let bucketForNew = newTokenBucket(some(requestRateLimiter.peerBucketSetting)).valueOr: + let bucketForNew = newTokenBucket( + some(requestRateLimiter.peerBucketSetting), Discrete, now + ).valueOr: raiseAssert "This branch is not allowed to be reached as it will not be called if the setting is None." return requestRateLimiter.peerUsage.mgetOrPut(peerId, bucketForNew) -proc checkUsage*( - t: var RequestRateLimiter, proto: string, conn: Connection, now = Moment.now() -): bool {.raises: [].} = - if t.tokenBucket.isNone(): - return true +proc checkUsageUnlimited( + t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment +): bool {.gcsafe, raises: [].} = + true - let peerBucket = t.mgetOrPut(conn.peerId) +proc checkUsageLimited( + t: var RequestRateLimiter, proto: string, conn: Connection, now: Moment +): bool {.gcsafe, raises: [].} = + # Lazy-init the main bucket using the first observed request time. This makes + # refill behavior deterministic under tests where `now` is controlled. + if isNil(t.tokenBucket): + t.tokenBucket = newMainTokenBucket(t.mainBucketSetting, t.ratio, now) + + let peerBucket = t.mgetOrPut(conn.peerId, now) ## check requesting peer's usage is not over the calculated ratio and let that peer go which not requested much/or this time... if not peerBucket.tryConsume(1, now): trace "peer usage limit reached", peer = conn.peerId return false # Ok if the peer can consume, check the overall budget we have left - let tokenBucket = t.tokenBucket.get() - if not tokenBucket.tryConsume(1, now): + if not t.tokenBucket.tryConsume(1, now): return false return true +proc checkUsage*( + t: var RequestRateLimiter, proto: string, conn: Connection, now = Moment.now() +): bool {.raises: [].} = + t.checkUsageImpl(t, proto, conn, now) + template checkUsageLimit*( t: var RequestRateLimiter, proto: string, @@ -135,9 +179,19 @@ func calcPeerTokenSetting( proc newRequestRateLimiter*(setting: Option[RateLimitSetting]): RequestRateLimiter = let ratio = calcPeriodRatio(setting) + let isLimited = setting.isSome() and not setting.get().isUnlimited() + let mainBucketSetting = + if isLimited: + setting.get() + else: + (0, 0.minutes) + return RequestRateLimiter( - tokenBucket: newTokenBucket(setting), + tokenBucket: nil, setting: setting, + mainBucketSetting: mainBucketSetting, + ratio: ratio, peerBucketSetting: calcPeerTokenSetting(setting, ratio), peerUsage: init(TimedMap[PeerId, TokenBucket], calcCacheTimeout(setting, ratio)), + checkUsageImpl: (if isLimited: checkUsageLimited else: checkUsageUnlimited), ) diff --git a/waku/common/rate_limit/setting.nim b/waku/common/rate_limit/setting.nim index 70f0ee721..66ff79d17 100644 --- a/waku/common/rate_limit/setting.nim +++ b/waku/common/rate_limit/setting.nim @@ -7,7 +7,6 @@ type RateLimitSetting* = tuple[volume: int, period: Duration] type RateLimitedProtocol* = enum GLOBAL - STOREV2 STOREV3 LIGHTPUSH PEEREXCHG @@ -47,8 +46,6 @@ proc translate(sProtocol: string): RateLimitedProtocol {.raises: [ValueError].} case sProtocol of "global": return GLOBAL - of "storev2": - return STOREV2 of "storev3": return STOREV3 of "lightpush": @@ -65,7 +62,6 @@ proc fillSettingTable( ) {.raises: [ValueError].} = if sProtocol == "store": # generic store will only applies to version which is not listed directly - discard t.hasKeyOrPut(STOREV2, setting) discard t.hasKeyOrPut(STOREV3, setting) else: let protocol = translate(sProtocol) @@ -87,7 +83,7 @@ proc parse*( ## group4: Unit of period - only h:hour, m:minute, s:second, ms:millisecond allowed ## whitespaces are allowed lazily const parseRegex = - """^\s*((store|storev2|storev3|lightpush|px|filter)\s*:)?\s*(\d+)\s*\/\s*(\d+)\s*(s|h|m|ms)\s*$""" + """^\s*((store|storev3|lightpush|px|filter)\s*:)?\s*(\d+)\s*\/\s*(\d+)\s*(s|h|m|ms)\s*$""" const regexParseSize = re2(parseRegex) for settingStr in settings: let aSetting = settingStr.toLower() diff --git a/waku/common/rate_limit/single_token_limiter.nim b/waku/common/rate_limit/single_token_limiter.nim index 50fb2d64c..fc4b0acd5 100644 --- a/waku/common/rate_limit/single_token_limiter.nim +++ b/waku/common/rate_limit/single_token_limiter.nim @@ -6,12 +6,15 @@ import std/[options], chronos/timer, libp2p/stream/connection, libp2p/utility import std/times except TimeInterval, Duration -import ./[token_bucket, setting, service_metrics] +import chronos/ratelimit as token_bucket + +import ./[setting, service_metrics] export token_bucket, setting, service_metrics proc newTokenBucket*( setting: Option[RateLimitSetting], - replenishMode: ReplenishMode = ReplenishMode.Compensating, + replenishMode: static[ReplenishMode] = ReplenishMode.Continuous, + startTime: Moment = Moment.now(), ): Option[TokenBucket] = if setting.isNone(): return none[TokenBucket]() @@ -19,7 +22,14 @@ proc newTokenBucket*( if setting.get().isUnlimited(): return none[TokenBucket]() - return some(TokenBucket.new(setting.get().volume, setting.get().period)) + return some( + TokenBucket.new( + capacity = setting.get().volume, + fillDuration = setting.get().period, + startTime = startTime, + mode = replenishMode, + ) + ) proc checkUsage( t: var TokenBucket, proto: string, now = Moment.now() diff --git a/waku/common/rate_limit/timed_map.nim b/waku/common/rate_limit/timed_map.nim index b05dfb0fb..b9a5c4cbf 100644 --- a/waku/common/rate_limit/timed_map.nim +++ b/waku/common/rate_limit/timed_map.nim @@ -106,16 +106,8 @@ proc mgetOrPut*[K, V](t: var TimedMap[K, V], k: K, v: V, now = Moment.now()): va let previous = t.del(k) # Refresh existing item - addedAt = - if previous.isSome(): - previous[].addedAt - else: - now - value = - if previous.isSome(): - previous[].value - else: - v + addedAt = if previous.isSome(): previous[].addedAt else: now + value = if previous.isSome(): previous[].value else: v let node = TimedEntry[K, V](key: k, value: value, addedAt: addedAt, expiresAt: now + t.timeout) diff --git a/waku/common/rate_limit/token_bucket.nim b/waku/common/rate_limit/token_bucket.nim deleted file mode 100644 index 799817ebd..000000000 --- a/waku/common/rate_limit/token_bucket.nim +++ /dev/null @@ -1,182 +0,0 @@ -{.push raises: [].} - -import chronos, std/math, std/options - -const BUDGET_COMPENSATION_LIMIT_PERCENT = 0.25 - -## This is an extract from chronos/rate_limit.nim due to the found bug in the original implementation. -## Unfortunately that bug cannot be solved without harm the original features of TokenBucket class. -## So, this current shortcut is used to enable move ahead with nwaku rate limiter implementation. -## ref: https://github.com/status-im/nim-chronos/issues/500 -## -## This version of TokenBucket is different from the original one in chronos/rate_limit.nim in many ways: -## - It has a new mode called `Compensating` which is the default mode. -## Compensation is calculated as the not used bucket capacity in the last measured period(s) in average. -## or up until maximum the allowed compansation treshold (Currently it is const 25%). -## Also compensation takes care of the proper time period calculation to avoid non-usage periods that can lead to -## overcompensation. -## - Strict mode is also available which will only replenish when time period is over but also will fill -## the bucket to the max capacity. - -type - ReplenishMode* = enum - Strict - Compensating - - TokenBucket* = ref object - budget: int ## Current number of tokens in the bucket - budgetCap: int ## Bucket capacity - lastTimeFull: Moment - ## This timer measures the proper periodizaiton of the bucket refilling - fillDuration: Duration ## Refill period - case replenishMode*: ReplenishMode - of Strict: - ## In strict mode, the bucket is refilled only till the budgetCap - discard - of Compensating: - ## This is the default mode. - maxCompensation: float - -func periodDistance(bucket: TokenBucket, currentTime: Moment): float = - ## notice fillDuration cannot be zero by design - ## period distance is a float number representing the calculated period time - ## since the last time bucket was refilled. - return - nanoseconds(currentTime - bucket.lastTimeFull).float / - nanoseconds(bucket.fillDuration).float - -func getUsageAverageSince(bucket: TokenBucket, distance: float): float = - if distance == 0.float: - ## in case there is zero time difference than the usage percentage is 100% - return 1.0 - - ## budgetCap can never be zero - ## usage average is calculated as a percentage of total capacity available over - ## the measured period - return bucket.budget.float / bucket.budgetCap.float / distance - -proc calcCompensation(bucket: TokenBucket, averageUsage: float): int = - # if we already fully used or even overused the tokens, there is no place for compensation - if averageUsage >= 1.0: - return 0 - - ## compensation is the not used bucket capacity in the last measured period(s) in average. - ## or maximum the allowed compansation treshold - let compensationPercent = - min((1.0 - averageUsage) * bucket.budgetCap.float, bucket.maxCompensation) - return trunc(compensationPercent).int - -func periodElapsed(bucket: TokenBucket, currentTime: Moment): bool = - return currentTime - bucket.lastTimeFull >= bucket.fillDuration - -## Update will take place if bucket is empty and trying to consume tokens. -## It checks if the bucket can be replenished as refill duration is passed or not. -## - strict mode: -proc updateStrict(bucket: TokenBucket, currentTime: Moment) = - if bucket.fillDuration == default(Duration): - bucket.budget = min(bucket.budgetCap, bucket.budget) - return - - if not periodElapsed(bucket, currentTime): - return - - bucket.budget = bucket.budgetCap - bucket.lastTimeFull = currentTime - -## - compensating - ballancing load: -## - between updates we calculate average load (current bucket capacity / number of periods till last update) -## - gives the percentage load used recently -## - with this we can replenish bucket up to 100% + calculated leftover from previous period (caped with max treshold) -proc updateWithCompensation(bucket: TokenBucket, currentTime: Moment) = - if bucket.fillDuration == default(Duration): - bucket.budget = min(bucket.budgetCap, bucket.budget) - return - - # do not replenish within the same period - if not periodElapsed(bucket, currentTime): - return - - let distance = bucket.periodDistance(currentTime) - let recentAvgUsage = bucket.getUsageAverageSince(distance) - let compensation = bucket.calcCompensation(recentAvgUsage) - - bucket.budget = bucket.budgetCap + compensation - bucket.lastTimeFull = currentTime - -proc update(bucket: TokenBucket, currentTime: Moment) = - if bucket.replenishMode == ReplenishMode.Compensating: - updateWithCompensation(bucket, currentTime) - else: - updateStrict(bucket, currentTime) - -proc tryConsume*(bucket: TokenBucket, tokens: int, now = Moment.now()): bool = - ## If `tokens` are available, consume them, - ## Otherwhise, return false. - - if bucket.budget >= bucket.budgetCap: - bucket.lastTimeFull = now - - if bucket.budget >= tokens: - bucket.budget -= tokens - return true - - bucket.update(now) - - if bucket.budget >= tokens: - bucket.budget -= tokens - return true - else: - return false - -proc replenish*(bucket: TokenBucket, tokens: int, now = Moment.now()) = - ## Add `tokens` to the budget (capped to the bucket capacity) - bucket.budget += tokens - bucket.update(now) - -proc new*( - T: type[TokenBucket], - budgetCap: int, - fillDuration: Duration = 1.seconds, - mode: ReplenishMode = ReplenishMode.Compensating, -): T = - assert not isZero(fillDuration) - assert budgetCap != 0 - - ## Create different mode TokenBucket - case mode - of ReplenishMode.Strict: - return T( - budget: budgetCap, - budgetCap: budgetCap, - fillDuration: fillDuration, - lastTimeFull: Moment.now(), - replenishMode: mode, - ) - of ReplenishMode.Compensating: - T( - budget: budgetCap, - budgetCap: budgetCap, - fillDuration: fillDuration, - lastTimeFull: Moment.now(), - replenishMode: mode, - maxCompensation: budgetCap.float * BUDGET_COMPENSATION_LIMIT_PERCENT, - ) - -proc newStrict*(T: type[TokenBucket], capacity: int, period: Duration): TokenBucket = - T.new(capacity, period, ReplenishMode.Strict) - -proc newCompensating*( - T: type[TokenBucket], capacity: int, period: Duration -): TokenBucket = - T.new(capacity, period, ReplenishMode.Compensating) - -func `$`*(b: TokenBucket): string {.inline.} = - if isNil(b): - return "nil" - return $b.budgetCap & "/" & $b.fillDuration - -func `$`*(ob: Option[TokenBucket]): string {.inline.} = - if ob.isNone(): - return "no-limit" - - return $ob.get() diff --git a/waku/common/waku_protocol.nim b/waku/common/waku_protocol.nim new file mode 100644 index 000000000..76a8aded0 --- /dev/null +++ b/waku/common/waku_protocol.nim @@ -0,0 +1,22 @@ +{.push raises: [].} + +type WakuProtocol* {.pure.} = enum + RelayProtocol = "Relay" + RlnRelayProtocol = "Rln Relay" + StoreProtocol = "Store" + FilterProtocol = "Filter" + LightpushProtocol = "Lightpush" + LegacyLightpushProtocol = "Legacy Lightpush" + PeerExchangeProtocol = "Peer Exchange" + RendezvousProtocol = "Rendezvous" + MixProtocol = "Mix" + StoreClientProtocol = "Store Client" + FilterClientProtocol = "Filter Client" + LightpushClientProtocol = "Lightpush Client" + LegacyLightpushClientProtocol = "Legacy Lightpush Client" + +const + RelayProtocols* = {RelayProtocol} + StoreClientProtocols* = {StoreClientProtocol} + LightpushClientProtocols* = {LightpushClientProtocol, LegacyLightpushClientProtocol} + FilterClientProtocols* = {FilterClientProtocol} diff --git a/waku/discovery/waku_discv5.nim b/waku/discovery/waku_discv5.nim index 94fc467fa..c1b253c8c 100644 --- a/waku/discovery/waku_discv5.nim +++ b/waku/discovery/waku_discv5.nim @@ -10,7 +10,7 @@ import eth/keys as eth_keys, eth/p2p/discoveryv5/node, eth/p2p/discoveryv5/protocol -import ../node/peer_manager/peer_manager, ../waku_core, ../waku_enr +import waku/[net/auto_port, node/peer_manager/peer_manager, waku_core, waku_enr] export protocol, waku_enr @@ -393,12 +393,11 @@ proc addBootstrapNode*(bootstrapAddr: string, bootstrapEnrs: var seq[enr.Record] if bootstrapAddr.len == 0 or bootstrapAddr[0] == '#': return - let enrRes = parseBootstrapAddress(bootstrapAddr) - if enrRes.isErr(): - info "ignoring invalid bootstrap address", reason = enrRes.error + let enr = parseBootstrapAddress(bootstrapAddr).valueOr: + info "ignoring invalid bootstrap address", reason = error return - bootstrapEnrs.add(enrRes.value) + bootstrapEnrs.add(enr) proc setupDiscoveryV5*( myENR: enr.Record, @@ -410,7 +409,15 @@ proc setupDiscoveryV5*( key: crypto.PrivateKey, p2pListenAddress: IpAddress, portsShift: uint16, -): WakuDiscoveryV5 = +): Result[WakuDiscoveryV5, string] = + ## Public only for testing. Callers should use `setupAndStartDiscv5`, which + ## additionally handles `udpPort == 0` via auto-port retry. + if conf.udpPort == Port(0): + return err( + "setupDiscoveryV5: udpPort must be non-zero; " & + "use setupAndStartDiscv5 for port=0 auto-port retry" + ) + let dynamicBootstrapEnrs = dynamicBootstrapNodes.filterIt(it.hasUdpPort()).mapIt(it.enr.get()) @@ -442,10 +449,47 @@ proc setupDiscoveryV5*( autoupdateRecord: conf.enrAutoUpdate, ) - WakuDiscoveryV5.new( - rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue + return ok( + WakuDiscoveryV5.new( + rng, discv5Conf, some(myENR), some(nodePeerManager), nodeTopicSubscriptionQueue + ) ) +proc setupAndStartDiscv5*( + myENR: enr.Record, + nodePeerManager: PeerManager, + nodeTopicSubscriptionQueue: AsyncEventQueue[SubscriptionEvent], + conf: Discv5Conf, + dynamicBootstrapNodes: seq[RemotePeerInfo], + rng: ref HmacDrbgContext, + key: crypto.PrivateKey, + p2pListenAddress: IpAddress, + portsShift: uint16, +): Future[Result[WakuDiscoveryV5, string]] {.async: (raises: []).} = + ## Construct and start a `WakuDiscoveryV5` instance, handling auto-port + ## retry when the caller asks for `udpPort == 0`. + proc attempt( + port: Port + ): Future[Result[WakuDiscoveryV5, string]] {.async: (raises: []).} = + var c = conf + c.udpPort = port + let wd = setupDiscoveryV5( + myENR, nodePeerManager, nodeTopicSubscriptionQueue, c, dynamicBootstrapNodes, rng, + key, p2pListenAddress, portsShift, + ).valueOr: + return err(error) + let startRes = await wd.start() + if startRes.isErr(): + return err("failed to start discovery, attempt: " & startRes.error) + return ok(wd) + + let wd = (await tryWithAutoPort[WakuDiscoveryV5](conf.udpPort, attempt)).valueOr: + return err("setupAndStartDiscv5: " & error) + return ok(wd) + +proc udpPort*(wd: WakuDiscoveryV5): Port = + wd.conf.port + proc updateBootstrapRecords*( self: var WakuDiscoveryV5, newRecordsString: string ): Result[void, string] = diff --git a/waku/discovery/waku_kademlia.nim b/waku/discovery/waku_kademlia.nim new file mode 100644 index 000000000..94b63a321 --- /dev/null +++ b/waku/discovery/waku_kademlia.nim @@ -0,0 +1,280 @@ +{.push raises: [].} + +import std/[options, sequtils] +import + chronos, + chronicles, + results, + stew/byteutils, + libp2p/[peerid, multiaddress, switch], + libp2p/extended_peer_record, + libp2p/crypto/curve25519, + libp2p/protocols/[kademlia, kad_disco], + libp2p/protocols/kademlia_discovery/types as kad_types, + libp2p/protocols/mix/mix_protocol + +import waku/waku_core, waku/node/peer_manager + +logScope: + topics = "waku extended kademlia discovery" + +const + DefaultExtendedKademliaDiscoveryInterval* = chronos.seconds(5) + ExtendedKademliaDiscoveryStartupDelay* = chronos.seconds(5) + +type + MixNodePoolSizeProvider* = proc(): int {.gcsafe, raises: [].} + NodeStartedProvider* = proc(): bool {.gcsafe, raises: [].} + + ExtendedKademliaDiscoveryParams* = object + bootstrapNodes*: seq[(PeerId, seq[MultiAddress])] + mixPubKey*: Option[Curve25519Key] + advertiseMix*: bool = false + + WakuKademlia* = ref object + protocol*: KademliaDiscovery + peerManager: PeerManager + discoveryLoop: Future[void] + running*: bool + getMixNodePoolSize: MixNodePoolSizeProvider + isNodeStarted: NodeStartedProvider + +proc new*( + T: type WakuKademlia, + switch: Switch, + params: ExtendedKademliaDiscoveryParams, + peerManager: PeerManager, + getMixNodePoolSize: MixNodePoolSizeProvider = nil, + isNodeStarted: NodeStartedProvider = nil, +): Result[T, string] = + if params.bootstrapNodes.len == 0: + info "creating kademlia discovery as seed node (no bootstrap nodes)" + + let kademlia = KademliaDiscovery.new( + switch, + bootstrapNodes = params.bootstrapNodes, + config = KadDHTConfig.new( + validator = kad_types.ExtEntryValidator(), selector = kad_types.ExtEntrySelector() + ), + codec = ExtendedKademliaDiscoveryCodec, + ) + + try: + switch.mount(kademlia) + except CatchableError: + return err("failed to mount kademlia discovery: " & getCurrentExceptionMsg()) + + # Register services BEFORE starting kademlia so they are included in the + # initial self-signed peer record published to the DHT + if params.advertiseMix: + if params.mixPubKey.isSome(): + let alreadyAdvertising = kademlia.startAdvertising( + ServiceInfo(id: MixProtocolID, data: @(params.mixPubKey.get())) + ) + if alreadyAdvertising: + warn "mix service was already being advertised" + debug "extended kademlia advertising mix service", + keyHex = byteutils.toHex(params.mixPubKey.get()), + bootstrapNodes = params.bootstrapNodes.len + else: + warn "mix advertising enabled but no key provided" + + info "kademlia discovery created", + bootstrapNodes = params.bootstrapNodes.len, advertiseMix = params.advertiseMix + + return ok( + WakuKademlia( + protocol: kademlia, + peerManager: peerManager, + running: false, + getMixNodePoolSize: getMixNodePoolSize, + isNodeStarted: isNodeStarted, + ) + ) + +proc extractMixPubKey(service: ServiceInfo): Option[Curve25519Key] = + if service.id != MixProtocolID: + trace "service is not mix protocol", + serviceId = service.id, mixProtocolId = MixProtocolID + return none(Curve25519Key) + + if service.data.len != Curve25519KeySize: + warn "invalid mix pub key length from kademlia record", + expected = Curve25519KeySize, + actual = service.data.len, + dataHex = byteutils.toHex(service.data) + return none(Curve25519Key) + + debug "found mix protocol service", + dataLen = service.data.len, expectedLen = Curve25519KeySize + + let key = intoCurve25519Key(service.data) + debug "successfully extracted mix pub key", keyHex = byteutils.toHex(key) + return some(key) + +proc remotePeerInfoFrom(record: ExtendedPeerRecord): Option[RemotePeerInfo] = + debug "processing kademlia record", + peerId = record.peerId, + numAddresses = record.addresses.len, + numServices = record.services.len, + serviceIds = record.services.mapIt(it.id) + + if record.addresses.len == 0: + trace "kademlia record missing addresses", peerId = record.peerId + return none(RemotePeerInfo) + + let addrs = record.addresses.mapIt(it.address) + if addrs.len == 0: + trace "kademlia record produced no dialable addresses", peerId = record.peerId + return none(RemotePeerInfo) + + let protocols = record.services.mapIt(it.id) + + var mixPubKey = none(Curve25519Key) + for service in record.services: + debug "checking service", + peerId = record.peerId, serviceId = service.id, dataLen = service.data.len + mixPubKey = extractMixPubKey(service) + if mixPubKey.isSome(): + debug "extracted mix public key from service", peerId = record.peerId + break + + if record.services.len > 0 and mixPubKey.isNone(): + debug "record has services but no valid mix key", + peerId = record.peerId, services = record.services.mapIt(it.id) + return none(RemotePeerInfo) + return some( + RemotePeerInfo.init( + record.peerId, + addrs = addrs, + protocols = protocols, + origin = PeerOrigin.Kademlia, + mixPubKey = mixPubKey, + ) + ) + +proc lookupMixPeers*( + wk: WakuKademlia +): Future[Result[int, string]] {.async: (raises: []).} = + ## Lookup mix peers via kademlia and add them to the peer store. + ## Returns the number of mix peers found and added. + if wk.protocol.isNil(): + return err("cannot lookup mix peers: kademlia not mounted") + + let mixService = ServiceInfo(id: MixProtocolID, data: @[]) + var records: seq[ExtendedPeerRecord] + try: + records = await wk.protocol.lookup(mixService) + except CatchableError: + return err("mix peer lookup failed: " & getCurrentExceptionMsg()) + + debug "mix peer lookup returned records", numRecords = records.len + + var added = 0 + for record in records: + let peerOpt = remotePeerInfoFrom(record) + if peerOpt.isNone(): + continue + + let peerInfo = peerOpt.get() + if peerInfo.mixPubKey.isNone(): + continue + + wk.peerManager.addPeer(peerInfo, PeerOrigin.Kademlia) + info "mix peer added via kademlia lookup", + peerId = $peerInfo.peerId, mixPubKey = byteutils.toHex(peerInfo.mixPubKey.get()) + added.inc() + + info "mix peer lookup complete", found = added + return ok(added) + +proc runDiscoveryLoop( + wk: WakuKademlia, interval: Duration, minMixPeers: int +) {.async: (raises: []).} = + info "extended kademlia discovery loop started", interval = interval + + try: + while true: + # Wait for node to be started + if not wk.isNodeStarted.isNil() and not wk.isNodeStarted(): + await sleepAsync(ExtendedKademliaDiscoveryStartupDelay) + continue + + var records: seq[ExtendedPeerRecord] + try: + records = await wk.protocol.randomRecords() + except CatchableError as e: + warn "extended kademlia discovery failed", error = e.msg + await sleepAsync(interval) + continue + + debug "received random records from kademlia", numRecords = records.len + + var added = 0 + for record in records: + let peerOpt = remotePeerInfoFrom(record) + if peerOpt.isNone(): + continue + + let peerInfo = peerOpt.get() + wk.peerManager.addPeer(peerInfo, PeerOrigin.Kademlia) + debug "peer added via extended kademlia discovery", + peerId = $peerInfo.peerId, + addresses = peerInfo.addrs.mapIt($it), + protocols = peerInfo.protocols, + hasMixPubKey = peerInfo.mixPubKey.isSome() + added.inc() + + if added > 0: + info "added peers from extended kademlia discovery", count = added + + # Targeted mix peer lookup when pool is low + if minMixPeers > 0 and not wk.getMixNodePoolSize.isNil() and + wk.getMixNodePoolSize() < minMixPeers: + debug "mix node pool below threshold, performing targeted lookup", + currentPoolSize = wk.getMixNodePoolSize(), threshold = minMixPeers + let found = (await wk.lookupMixPeers()).valueOr: + warn "targeted mix peer lookup failed", error = error + 0 + if found > 0: + info "found mix peers via targeted kademlia lookup", count = found + + await sleepAsync(interval) + except CancelledError as e: + debug "extended kademlia discovery loop cancelled", error = e.msg + except CatchableError as e: + error "extended kademlia discovery loop failed", error = e.msg + +proc start*( + wk: WakuKademlia, + interval: Duration = DefaultExtendedKademliaDiscoveryInterval, + minMixPeers: int = 0, +): Future[Result[void, string]] {.async: (raises: []).} = + if wk.running: + return err("already running") + + try: + await wk.protocol.start() + except CatchableError as e: + return err("failed to start kademlia discovery: " & e.msg) + + wk.discoveryLoop = wk.runDiscoveryLoop(interval, minMixPeers) + + info "kademlia discovery started" + return ok() + +proc stop*(wk: WakuKademlia) {.async: (raises: []).} = + if not wk.running: + return + + info "Stopping kademlia discovery" + + wk.running = false + + if not wk.discoveryLoop.isNil(): + await wk.discoveryLoop.cancelAndWait() + wk.discoveryLoop = nil + + if not wk.protocol.isNil(): + await wk.protocol.stop() + info "Successfully stopped kademlia discovery" diff --git a/waku/events/delivery_events.nim b/waku/events/delivery_events.nim new file mode 100644 index 000000000..5730335e0 --- /dev/null +++ b/waku/events/delivery_events.nim @@ -0,0 +1,12 @@ +import brokers/event_broker +import waku/waku_core/[message/message, message/digest] + +EventBroker: + type OnFilterSubscribeEvent* = object + pubsubTopic*: string + contentTopics*: seq[string] + +EventBroker: + type OnFilterUnSubscribeEvent* = object + pubsubTopic*: string + contentTopics*: seq[string] diff --git a/waku/events/events.nim b/waku/events/events.nim new file mode 100644 index 000000000..5a3c0c748 --- /dev/null +++ b/waku/events/events.nim @@ -0,0 +1,3 @@ +import ./[message_events, delivery_events, health_events, peer_events, lifecycle_events] + +export message_events, delivery_events, health_events, peer_events, lifecycle_events diff --git a/waku/events/health_events.nim b/waku/events/health_events.nim new file mode 100644 index 000000000..95912941e --- /dev/null +++ b/waku/events/health_events.nim @@ -0,0 +1,27 @@ +import brokers/event_broker + +import waku/api/types +import waku/node/health_monitor/[protocol_health, topic_health] +import waku/waku_core/topics + +export protocol_health, topic_health + +# Notify health changes to node connectivity +EventBroker: + type EventConnectionStatusChange* = object + connectionStatus*: ConnectionStatus + +# Notify health changes to a subscribed topic +# TODO: emit content topic health change events when subscribe/unsubscribe +# from/to content topic is provided in the new API (so we know which +# content topics are of interest to the application) +EventBroker: + type EventContentTopicHealthChange* = object + contentTopic*: ContentTopic + health*: TopicHealth + +# Notify health changes to a shard (pubsub topic) +EventBroker: + type EventShardTopicHealthChange* = object + topic*: PubsubTopic + health*: TopicHealth diff --git a/waku/events/message_events.nim b/waku/events/message_events.nim new file mode 100644 index 000000000..b45f91249 --- /dev/null +++ b/waku/events/message_events.nim @@ -0,0 +1,34 @@ +import brokers/event_broker +import waku/[api/types, waku_core/message, waku_core/topics] +export types + +EventBroker: + # Event emitted when a message is sent to the network + type MessageSentEvent* = object + requestId*: RequestId + messageHash*: string + +EventBroker: + # Event emitted when a message send operation fails + type MessageErrorEvent* = object + requestId*: RequestId + messageHash*: string + error*: string + +EventBroker: + # Confirmation that a message has been correctly delivered to some neighbouring nodes. + type MessagePropagatedEvent* = object + requestId*: RequestId + messageHash*: string + +EventBroker: + # Event emitted when a message is received via Waku + type MessageReceivedEvent* = object + messageHash*: string + message*: WakuMessage + +EventBroker: + # Internal event emitted when a message arrives from the network via any protocol + type MessageSeenEvent* = object + topic*: PubsubTopic + message*: WakuMessage diff --git a/waku/events/peer_events.nim b/waku/events/peer_events.nim new file mode 100644 index 000000000..7eed309b3 --- /dev/null +++ b/waku/events/peer_events.nim @@ -0,0 +1,13 @@ +import brokers/event_broker +import libp2p/switch + +type WakuPeerEventKind* {.pure.} = enum + EventConnected + EventDisconnected + EventIdentified + EventMetadataUpdated + +EventBroker: + type WakuPeerEvent* = object + peerId*: PeerId + kind*: WakuPeerEventKind diff --git a/waku/factory/app_callbacks.nim b/waku/factory/app_callbacks.nim index d28b9f2d1..f1d3369be 100644 --- a/waku/factory/app_callbacks.nim +++ b/waku/factory/app_callbacks.nim @@ -1,6 +1,7 @@ -import ../waku_relay, ../node/peer_manager +import ../waku_relay, ../node/peer_manager, ../node/health_monitor/connection_status type AppCallbacks* = ref object relayHandler*: WakuRelayHandler topicHealthChangeHandler*: TopicHealthChangeHandler connectionChangeHandler*: ConnectionChangeHandler + connectionStatusChangeHandler*: ConnectionStatusChangeHandler diff --git a/waku/factory/builder.nim b/waku/factory/builder.nim index 772cfbffd..4212cb92d 100644 --- a/waku/factory/builder.nim +++ b/waku/factory/builder.nim @@ -8,7 +8,9 @@ import libp2p/builders, libp2p/nameresolving/nameresolver, libp2p/transports/wstransport, - libp2p/protocols/connectivity/relay/relay + libp2p/protocols/connectivity/relay/relay, + brokers/broker_context + import ../waku_enr, ../discovery/waku_discv5, @@ -83,20 +85,19 @@ proc withNetworkConfigurationDetails*( ): WakuNodeBuilderResult {. deprecated: "use 'builder.withNetworkConfiguration()' instead" .} = - let netConfig = - ?NetConfig.init( - bindIp = bindIp, - bindPort = bindPort, - extIp = extIp, - extPort = extPort, - extMultiAddrs = extMultiAddrs, - wsBindPort = some(wsBindPort), - wsEnabled = wsEnabled, - wssEnabled = wssEnabled, - wakuFlags = wakuFlags, - dns4DomainName = dns4DomainName, - dnsNameServers = dnsNameServers, - ) + let netConfig = ?NetConfig.init( + bindIp = bindIp, + bindPort = bindPort, + extIp = extIp, + extPort = extPort, + extMultiAddrs = extMultiAddrs, + wsBindPort = some(wsBindPort), + wsEnabled = wsEnabled, + wssEnabled = wssEnabled, + wakuFlags = wakuFlags, + dns4DomainName = dns4DomainName, + dnsNameServers = dnsNameServers, + ) builder.withNetworkConfiguration(netConfig) ok() @@ -209,6 +210,7 @@ proc build*(builder: WakuNodeBuilder): Result[WakuNode, string] = maxServicePeers = some(builder.maxServicePeers), colocationLimit = builder.colocationLimit, shardedPeerManagement = builder.shardAware, + maxConnections = builder.switchMaxConnections.get(builders.MaxConnections), ) var node: WakuNode diff --git a/waku/factory/conf_builder/conf_builder.nim b/waku/factory/conf_builder/conf_builder.nim index 37cea76fe..b8d0316c3 100644 --- a/waku/factory/conf_builder/conf_builder.nim +++ b/waku/factory/conf_builder/conf_builder.nim @@ -10,10 +10,12 @@ import ./metrics_server_conf_builder, ./rate_limit_conf_builder, ./rln_relay_conf_builder, - ./mix_conf_builder + ./mix_conf_builder, + ./kademlia_discovery_conf_builder export waku_conf_builder, filter_service_conf_builder, store_sync_conf_builder, store_service_conf_builder, rest_server_conf_builder, dns_discovery_conf_builder, discv5_conf_builder, web_socket_conf_builder, metrics_server_conf_builder, - rate_limit_conf_builder, rln_relay_conf_builder, mix_conf_builder + rate_limit_conf_builder, rln_relay_conf_builder, mix_conf_builder, + kademlia_discovery_conf_builder diff --git a/waku/factory/conf_builder/discv5_conf_builder.nim b/waku/factory/conf_builder/discv5_conf_builder.nim index e2729021e..5dd269d23 100644 --- a/waku/factory/conf_builder/discv5_conf_builder.nim +++ b/waku/factory/conf_builder/discv5_conf_builder.nim @@ -4,6 +4,8 @@ import ../waku_conf logScope: topics = "waku conf builder discv5" +const DefaultDiscv5UdpPort*: Port = Port(9000) + ########################### ## Discv5 Config Builder ## ########################### @@ -38,8 +40,8 @@ proc withTableIpLimit*(b: var Discv5ConfBuilder, tableIpLimit: uint) = proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: Port) = b.udpPort = some(udpPort) -proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint) = - b.udpPort = some(Port(udpPort.uint16)) +proc withUdpPort*(b: var Discv5ConfBuilder, udpPort: uint16) = + b.udpPort = some(Port(udpPort)) proc withBootstrapNodes*(b: var Discv5ConfBuilder, bootstrapNodes: seq[string]) = # TODO: validate ENRs? @@ -57,7 +59,7 @@ proc build*(b: Discv5ConfBuilder): Result[Option[Discv5Conf], string] = bucketIpLimit: b.bucketIpLimit.get(2), enrAutoUpdate: b.enrAutoUpdate.get(true), tableIpLimit: b.tableIpLimit.get(10), - udpPort: b.udpPort.get(9000.Port), + udpPort: b.udpPort.get(DefaultDiscv5UdpPort), ) ) ) diff --git a/waku/factory/conf_builder/filter_service_conf_builder.nim b/waku/factory/conf_builder/filter_service_conf_builder.nim index a3f056b01..0a6617430 100644 --- a/waku/factory/conf_builder/filter_service_conf_builder.nim +++ b/waku/factory/conf_builder/filter_service_conf_builder.nim @@ -22,6 +22,12 @@ proc withEnabled*(b: var FilterServiceConfBuilder, enabled: bool) = proc withMaxPeersToServe*(b: var FilterServiceConfBuilder, maxPeersToServe: uint32) = b.maxPeersToServe = some(maxPeersToServe) +proc withMaxPeersToServeIfNotAssigned*( + b: var FilterServiceConfBuilder, maxPeersToServe: uint32 +) = + if b.maxPeersToServe.isNone(): + b.maxPeersToServe = some(maxPeersToServe) + proc withSubscriptionTimeout*( b: var FilterServiceConfBuilder, subscriptionTimeout: uint16 ) = diff --git a/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim b/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim new file mode 100644 index 000000000..916d71be1 --- /dev/null +++ b/waku/factory/conf_builder/kademlia_discovery_conf_builder.nim @@ -0,0 +1,40 @@ +import chronicles, std/options, results +import libp2p/[peerid, multiaddress, peerinfo] +import waku/factory/waku_conf + +logScope: + topics = "waku conf builder kademlia discovery" + +####################################### +## Kademlia Discovery Config Builder ## +####################################### +type KademliaDiscoveryConfBuilder* = object + enabled*: bool + bootstrapNodes*: seq[string] + +proc init*(T: type KademliaDiscoveryConfBuilder): KademliaDiscoveryConfBuilder = + KademliaDiscoveryConfBuilder() + +proc withEnabled*(b: var KademliaDiscoveryConfBuilder, enabled: bool) = + b.enabled = enabled + +proc withBootstrapNodes*( + b: var KademliaDiscoveryConfBuilder, bootstrapNodes: seq[string] +) = + b.bootstrapNodes = bootstrapNodes + +proc build*( + b: KademliaDiscoveryConfBuilder +): Result[Option[KademliaDiscoveryConf], string] = + # Kademlia is enabled if explicitly enabled OR if bootstrap nodes are provided + let enabled = b.enabled or b.bootstrapNodes.len > 0 + if not enabled: + return ok(none(KademliaDiscoveryConf)) + + var parsedNodes: seq[(PeerId, seq[MultiAddress])] + for nodeStr in b.bootstrapNodes: + let (peerId, ma) = parseFullAddress(nodeStr).valueOr: + return err("Failed to parse kademlia bootstrap node: " & error) + parsedNodes.add((peerId, @[ma])) + + return ok(some(KademliaDiscoveryConf(bootstrapNodes: parsedNodes))) diff --git a/waku/factory/conf_builder/metrics_server_conf_builder.nim b/waku/factory/conf_builder/metrics_server_conf_builder.nim index 0f0d18564..8b2ea4eb8 100644 --- a/waku/factory/conf_builder/metrics_server_conf_builder.nim +++ b/waku/factory/conf_builder/metrics_server_conf_builder.nim @@ -4,6 +4,8 @@ import ../waku_conf logScope: topics = "waku conf builder metrics server" +const DefaultMetricsHttpPort*: Port = Port(8008) + ################################### ## Metrics Server Config Builder ## ################################### @@ -40,7 +42,7 @@ proc build*(b: MetricsServerConfBuilder): Result[Option[MetricsServerConf], stri some( MetricsServerConf( httpAddress: b.httpAddress.get(static parseIpAddress("127.0.0.1")), - httpPort: b.httpPort.get(8008.Port), + httpPort: b.httpPort.get(DefaultMetricsHttpPort), logging: b.logging.get(false), ) ) diff --git a/waku/factory/conf_builder/rate_limit_conf_builder.nim b/waku/factory/conf_builder/rate_limit_conf_builder.nim index 0d466a132..b2edbef03 100644 --- a/waku/factory/conf_builder/rate_limit_conf_builder.nim +++ b/waku/factory/conf_builder/rate_limit_conf_builder.nim @@ -14,6 +14,12 @@ proc init*(T: type RateLimitConfBuilder): RateLimitConfBuilder = proc withRateLimits*(b: var RateLimitConfBuilder, rateLimits: seq[string]) = b.strValue = some(rateLimits) +proc withRateLimitsIfNotAssigned*( + b: var RateLimitConfBuilder, rateLimits: seq[string] +) = + if b.strValue.isNone() or b.strValue.get().len == 0: + b.strValue = some(rateLimits) + proc build*(b: RateLimitConfBuilder): Result[ProtocolRateLimitSettings, string] = if b.strValue.isSome() and b.objValue.isSome(): return err("Rate limits conf must only be set once on the builder") diff --git a/waku/factory/conf_builder/rest_server_conf_builder.nim b/waku/factory/conf_builder/rest_server_conf_builder.nim index 2efd91f02..dcafbb56a 100644 --- a/waku/factory/conf_builder/rest_server_conf_builder.nim +++ b/waku/factory/conf_builder/rest_server_conf_builder.nim @@ -4,6 +4,8 @@ import ../waku_conf logScope: topics = "waku conf builder rest server" +const DefaultRestPort*: Port = Port(8645) + ################################ ## REST Server Config Builder ## ################################ @@ -46,8 +48,6 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] = if b.listenAddress.isNone(): return err("restServer.listenAddress is not specified") - if b.port.isNone(): - return err("restServer.port is not specified") if b.relayCacheCapacity.isNone(): return err("restServer.relayCacheCapacity is not specified") @@ -56,7 +56,7 @@ proc build*(b: RestServerConfBuilder): Result[Option[RestServerConf], string] = RestServerConf( allowOrigin: b.allowOrigin, listenAddress: b.listenAddress.get(), - port: b.port.get(), + port: b.port.get(DefaultRestPort), admin: b.admin.get(false), relayCacheCapacity: b.relayCacheCapacity.get(), ) diff --git a/waku/factory/conf_builder/store_service_conf_builder.nim b/waku/factory/conf_builder/store_service_conf_builder.nim index d5d48c34d..f1b0b1402 100644 --- a/waku/factory/conf_builder/store_service_conf_builder.nim +++ b/waku/factory/conf_builder/store_service_conf_builder.nim @@ -1,4 +1,5 @@ -import chronicles, std/options, results, chronos +import std/[options, strutils, sequtils] +import chronicles, results, chronos import ../waku_conf, ./store_sync_conf_builder logScope: @@ -13,9 +14,8 @@ type StoreServiceConfBuilder* = object dbMigration*: Option[bool] dbURl*: Option[string] dbVacuum*: Option[bool] - supportV2*: Option[bool] maxNumDbConnections*: Option[int] - retentionPolicy*: Option[string] + retentionPolicies*: seq[string] resume*: Option[bool] storeSyncConf*: StoreSyncConfBuilder @@ -34,20 +34,48 @@ proc withDbUrl*(b: var StoreServiceConfBuilder, dbUrl: string) = proc withDbVacuum*(b: var StoreServiceConfBuilder, dbVacuum: bool) = b.dbVacuum = some(dbVacuum) -proc withSupportV2*(b: var StoreServiceConfBuilder, supportV2: bool) = - b.supportV2 = some(supportV2) - proc withMaxNumDbConnections*( b: var StoreServiceConfBuilder, maxNumDbConnections: int ) = b.maxNumDbConnections = some(maxNumDbConnections) -proc withRetentionPolicy*(b: var StoreServiceConfBuilder, retentionPolicy: string) = - b.retentionPolicy = some(retentionPolicy) +proc withRetentionPolicies*(b: var StoreServiceConfBuilder, retentionPolicies: string) = + b.retentionPolicies = retentionPolicies + .multiReplace((" ", ""), ("\t", "")) + .split(";") + .mapIt(it.strip()) + .filterIt(it.len > 0) proc withResume*(b: var StoreServiceConfBuilder, resume: bool) = b.resume = some(resume) +const ValidRetentionPolicyTypes = ["time", "capacity", "size"] + +proc validateRetentionPolicies(policies: seq[string]): Result[void, string] = + var seen: seq[string] + + for p in policies: + let policy = p.multiReplace((" ", ""), ("\t", "")) + let parts = policy.split(":", 1) + if parts.len != 2 or parts[1] == "": + return err( + "invalid retention policy format: '" & policy & "', expected ':'" + ) + + let policyType = parts[0].toLowerAscii() + if policyType notin ValidRetentionPolicyTypes: + return err( + "unknown retention policy type: '" & policyType & + "', valid types are: time, capacity, size" + ) + + if policyType in seen: + return err("duplicated retention policy type: '" & policyType & "'") + + seen.add(policyType) + + return ok() + proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string] = if not b.enabled.get(false): return ok(none(StoreServiceConf)) @@ -58,15 +86,22 @@ proc build*(b: StoreServiceConfBuilder): Result[Option[StoreServiceConf], string let storeSyncConf = b.storeSyncConf.build().valueOr: return err("Store Sync Conf failed to build") + let retentionPolicies = + if b.retentionPolicies.len == 0: + @["time:" & $2.days.seconds] + else: + validateRetentionPolicies(b.retentionPolicies).isOkOr: + return err("invalid retention policies: " & error) + b.retentionPolicies + return ok( some( StoreServiceConf( dbMigration: b.dbMigration.get(true), dbURl: b.dbUrl.get(), dbVacuum: b.dbVacuum.get(false), - supportV2: b.supportV2.get(false), maxNumDbConnections: b.maxNumDbConnections.get(50), - retentionPolicy: b.retentionPolicy.get("time:" & $2.days.seconds), + retentionPolicies: retentionPolicies, resume: b.resume.get(false), storeSyncConf: storeSyncConf, ) diff --git a/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index 645869247..96e34eeed 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -8,11 +8,15 @@ import results import - ../waku_conf, - ../networks_config, - ../../common/logging, - ../../common/utils/parse_size_units, - ../../waku_enr/capabilities + waku/[ + factory/waku_conf, + factory/networks_config, + common/logging, + common/utils/parse_size_units, + waku_enr/capabilities, + persistency/persistency, + ], + tools/confutils/entry_nodes import ./filter_service_conf_builder, @@ -25,11 +29,16 @@ import ./metrics_server_conf_builder, ./rate_limit_conf_builder, ./rln_relay_conf_builder, - ./mix_conf_builder + ./mix_conf_builder, + ./kademlia_discovery_conf_builder logScope: topics = "waku conf builder" +const + DefaultMaxConnections* = 150 + DefaultP2pTcpPort*: Port = Port(60000) + type MaxMessageSizeKind* = enum mmskNone mmskStr @@ -78,6 +87,7 @@ type WakuConfBuilder* = object mixConf*: MixConfBuilder webSocketConf*: WebSocketConfBuilder rateLimitConf*: RateLimitConfBuilder + kademliaDiscoveryConf*: KademliaDiscoveryConfBuilder # End conf builders relay: Option[bool] lightPush: Option[bool] @@ -127,6 +137,8 @@ type WakuConfBuilder* = object circuitRelayClient: Option[bool] p2pReliability: Option[bool] + localStoragePath: Option[string] + proc init*(T: type WakuConfBuilder): WakuConfBuilder = WakuConfBuilder( dnsDiscoveryConf: DnsDiscoveryConfBuilder.init(), @@ -138,6 +150,7 @@ proc init*(T: type WakuConfBuilder): WakuConfBuilder = storeServiceConf: StoreServiceConfBuilder.init(), webSocketConf: WebSocketConfBuilder.init(), rateLimitConf: RateLimitConfBuilder.init(), + kademliaDiscoveryConf: KademliaDiscoveryConfBuilder.init(), ) proc withNetworkConf*(b: var WakuConfBuilder, networkConf: NetworkConf) = @@ -248,9 +261,6 @@ proc withAgentString*(b: var WakuConfBuilder, agentString: string) = proc withColocationLimit*(b: var WakuConfBuilder, colocationLimit: int) = b.colocationLimit = some(colocationLimit) -proc withMaxRelayPeers*(b: var WakuConfBuilder, maxRelayPeers: int) = - b.maxRelayPeers = some(maxRelayPeers) - proc withRelayServiceRatio*(b: var WakuConfBuilder, relayServiceRatio: string) = b.relayServiceRatio = some(relayServiceRatio) @@ -265,6 +275,9 @@ proc withRelayShardedPeerManagement*( proc withP2pReliability*(b: var WakuConfBuilder, p2pReliability: bool) = b.p2pReliability = some(p2pReliability) +proc withLocalStoragePath*(b: var WakuConfBuilder, localStoragePath: string) = + b.localStoragePath = some(localStoragePath) + proc withExtMultiAddrs*(builder: var WakuConfBuilder, extMultiAddrs: seq[string]) = builder.extMultiAddrs = concat(builder.extMultiAddrs, extMultiAddrs) @@ -296,7 +309,6 @@ proc buildShardingConf( bNumShardsInCluster: Option[uint16], bSubscribeShards: Option[seq[uint16]], ): (ShardingConf, seq[uint16]) = - echo "bSubscribeShards: ", bSubscribeShards case bShardingConfKind.get(AutoSharding) of StaticSharding: (ShardingConf(kind: StaticSharding), bSubscribeShards.get(@[])) @@ -350,10 +362,13 @@ proc applyNetworkConf(builder: var WakuConfBuilder) = builder.rlnRelayConf.withEpochSizeSec(networkConf.rlnEpochSizeSec) if builder.rlnRelayConf.userMessageLimit.isSome(): - warn "RLN Relay Dynamic was provided alongside a network conf", + warn "RLN Relay User Message Limit was provided alongside a network conf", used = networkConf.rlnRelayUserMessageLimit, discarded = builder.rlnRelayConf.userMessageLimit - builder.rlnRelayConf.withUserMessageLimit(networkConf.rlnRelayUserMessageLimit) + if builder.rlnRelayConf.userMessageLimit.get(0) == 0: + ## only override with the "preset" value if there was not explicit set value + builder.rlnRelayConf.withUserMessageLimit(networkConf.rlnRelayUserMessageLimit) + # End Apply relay parameters case builder.maxMessageSize.kind @@ -368,17 +383,17 @@ proc applyNetworkConf(builder: var WakuConfBuilder) = warn "Sharding Conf was provided alongside a network conf", used = networkConf.shardingConf.kind, discarded = builder.shardingConf - if builder.numShardsInCluster.isSome(): - warn "Num Shards In Cluster was provided alongside a network conf", - used = networkConf.shardingConf.numShardsInCluster, - discarded = builder.numShardsInCluster - case networkConf.shardingConf.kind of StaticSharding: builder.shardingConf = some(StaticSharding) of AutoSharding: builder.shardingConf = some(AutoSharding) - builder.numShardsInCluster = some(networkConf.shardingConf.numShardsInCluster) + if builder.numShardsInCluster.isSome(): + warn "Num Shards In Cluster overrides network conf preset", + used = builder.numShardsInCluster.get(), + ignored = networkConf.shardingConf.numShardsInCluster + else: + builder.numShardsInCluster = some(networkConf.shardingConf.numShardsInCluster) if networkConf.discv5Discovery: if builder.discv5Conf.enabled.isNone: @@ -391,6 +406,42 @@ proc applyNetworkConf(builder: var WakuConfBuilder) = discarded = builder.discv5Conf.bootstrapNodes builder.discv5Conf.withBootstrapNodes(networkConf.discv5BootstrapNodes) + if networkConf.enableKadDiscovery: + if not builder.kademliaDiscoveryConf.enabled: + builder.kademliaDiscoveryConf.withEnabled(networkConf.enableKadDiscovery) + + if builder.kademliaDiscoveryConf.bootstrapNodes.len == 0 and + networkConf.kadBootstrapNodes.len > 0: + builder.kademliaDiscoveryConf.withBootstrapNodes(networkConf.kadBootstrapNodes) + + if networkConf.mix: + if builder.mix.isNone: + builder.mix = some(networkConf.mix) + + if builder.p2pReliability.isNone: + builder.withP2pReliability(networkConf.p2pReliability) + + # Process entry nodes from network config - classify and distribute + if networkConf.entryNodes.len > 0: + let processed = processEntryNodes(networkConf.entryNodes) + if processed.isOk(): + let (enrTreeUrls, bootstrapEnrs, staticNodesFromEntry) = processed.get() + + # Set ENRTree URLs for DNS discovery + if enrTreeUrls.len > 0: + for url in enrTreeUrls: + builder.dnsDiscoveryConf.withEnrTreeUrl(url) + + # Set ENR records as bootstrap nodes for discv5 + if bootstrapEnrs.len > 0: + builder.discv5Conf.withBootstrapNodes(bootstrapEnrs) + + # Add static nodes (multiaddrs and those extracted from ENR entries) + if staticNodesFromEntry.len > 0: + builder.withStaticNodes(staticNodesFromEntry) + else: + warn "Failed to process entry nodes from network conf", error = processed.error() + proc build*( builder: var WakuConfBuilder, rng: ref HmacDrbgContext = crypto.newRng() ): Result[WakuConf, string] = @@ -507,6 +558,9 @@ proc build*( let rateLimit = builder.rateLimitConf.build().valueOr: return err("Rate limits Conf building failed: " & $error) + let kademliaDiscoveryConf = builder.kademliaDiscoveryConf.build().valueOr: + return err("Kademlia Discovery Conf building failed: " & $error) + # End - Build sub-configs let logLevel = @@ -530,12 +584,7 @@ proc build*( warn "Nat Strategy is not specified, defaulting to none" "none" - let p2pTcpPort = - if builder.p2pTcpPort.isSome(): - builder.p2pTcpPort.get() - else: - warn "P2P Listening TCP Port is not specified, listening on 60000" - 60000.Port + let p2pTcpPort = builder.p2pTcpPort.get(DefaultP2pTcpPort) let p2pListenAddress = if builder.p2pListenAddress.isSome(): @@ -592,11 +641,16 @@ proc build*( if builder.maxConnections.isSome(): builder.maxConnections.get() else: - warn "Max Connections was not specified, defaulting to 300" - 300 + warn "Max connections not specified, defaulting to DefaultMaxConnections", + default = DefaultMaxConnections + DefaultMaxConnections + + if maxConnections < DefaultMaxConnections: + warn "max-connections less than DefaultMaxConnections; we suggest using DefaultMaxConnections or more for better connectivity", + provided = maxConnections, recommended = DefaultMaxConnections # TODO: Do the git version thing here - let agentString = builder.agentString.get("nwaku") + let agentString = builder.agentString.get("logos-delivery") # TODO: use `DefaultColocationLimit`. the user of this value should # probably be defining a config object @@ -606,7 +660,7 @@ proc build*( let relayShardedPeerManagement = builder.relayShardedPeerManagement.get(false) let wakuFlags = CapabilitiesBitfield.init( - lightpush = lightPush, + lightpush = lightPush and relay, filter = filterServiceConf.isSome, store = storeServiceConf.isSome, relay = relay, @@ -624,6 +678,7 @@ proc build*( restServerConf: restServerConf, dnsDiscoveryConf: dnsDiscoveryConf, mixConf: mixConf, + kademliaDiscoveryConf: kademliaDiscoveryConf, # end confs nodeKey: nodeKey, clusterId: clusterId, @@ -663,13 +718,14 @@ proc build*( agentString: agentString, colocationLimit: colocationLimit, maxRelayPeers: builder.maxRelayPeers, - relayServiceRatio: builder.relayServiceRatio.get("60:40"), + relayServiceRatio: builder.relayServiceRatio.get("50:50"), rateLimit: rateLimit, circuitRelayClient: builder.circuitRelayClient.get(false), staticNodes: builder.staticNodes, relayShardedPeerManagement: relayShardedPeerManagement, p2pReliability: builder.p2pReliability.get(false), wakuFlags: wakuFlags, + localStoragePath: builder.localStoragePath.get(DefaultStoragePath), ) ?wakuConf.validate() diff --git a/waku/factory/conf_builder/web_socket_conf_builder.nim b/waku/factory/conf_builder/web_socket_conf_builder.nim index 88edc0941..61334d958 100644 --- a/waku/factory/conf_builder/web_socket_conf_builder.nim +++ b/waku/factory/conf_builder/web_socket_conf_builder.nim @@ -4,6 +4,8 @@ import waku/factory/waku_conf logScope: topics = "waku conf builder websocket" +const DefaultWebSocketPort*: Port = Port(8000) + ############################## ## WebSocket Config Builder ## ############################## @@ -41,14 +43,12 @@ proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] = if not b.enabled.get(false): return ok(none(WebSocketConf)) - if b.webSocketPort.isNone(): - return err("websocket.port is not specified") - if not b.secureEnabled.get(false): return ok( some( WebSocketConf( - port: b.websocketPort.get(), secureConf: none(WebSocketSecureConf) + port: b.webSocketPort.get(DefaultWebSocketPort), + secureConf: none(WebSocketSecureConf), ) ) ) @@ -61,7 +61,7 @@ proc build*(b: WebSocketConfBuilder): Result[Option[WebSocketConf], string] = return ok( some( WebSocketConf( - port: b.webSocketPort.get(), + port: b.webSocketPort.get(DefaultWebSocketPort), secureConf: some( WebSocketSecureConf(keyPath: b.keyPath.get(), certPath: b.certPath.get()) ), diff --git a/waku/factory/internal_config.nim b/waku/factory/internal_config.nim index 22b101021..fa36aff57 100644 --- a/waku/factory/internal_config.nim +++ b/waku/factory/internal_config.nim @@ -8,10 +8,10 @@ import std/[options, sequtils, net], results -import ../common/utils/nat, ../node/net_config, ../waku_enr, ../waku_core, ./waku_conf +import waku/[common/utils/nat, net/net_config, waku_enr, waku_core], ./waku_conf -proc enrConfiguration*( - conf: WakuConf, netConfig: NetConfig +proc tryBuildEnrRecord( + conf: WakuConf, netConfig: NetConfig, multiaddrs: seq[MultiAddress] ): Result[enr.Record, string] = var enrBuilder = EnrBuilder.init(conf.nodeKey) @@ -22,23 +22,44 @@ proc enrConfiguration*( if netConfig.wakuFlags.isSome(): enrBuilder.withWakuCapabilities(netConfig.wakuFlags.get()) - enrBuilder.withMultiaddrs(netConfig.enrMultiaddrs) + if multiaddrs.len > 0: + enrBuilder.withMultiaddrs(multiaddrs) enrBuilder.withWakuRelaySharding( RelayShards(clusterId: conf.clusterId, shardIds: conf.subscribeShards) ).isOkOr: return err("could not initialize ENR with shards") - let recordRes = enrBuilder.build() - let record = - if recordRes.isErr(): - error "failed to create record", error = recordRes.error - return err($recordRes.error) - else: - recordRes.get() + let record = enrBuilder.build().valueOr: + return err($error) return ok(record) +proc enrConfiguration*( + conf: WakuConf, netConfig: NetConfig +): Result[enr.Record, string] = + for retained in countdown(netConfig.enrMultiaddrs.len, 0): + let multiaddrs = netConfig.enrMultiaddrs[0 ..< retained] + let record = tryBuildEnrRecord(conf, netConfig, multiaddrs).valueOr: + if retained > 0: + warn "failed to create enr record, retrying with fewer multiaddrs", + error = error, + totalMultiaddrs = netConfig.enrMultiaddrs.len, + retainedMultiaddrs = retained - 1, + removedMultiaddr = multiaddrs[^1] + continue + + error "failed to create enr record", error = error + return err($error) + + if retained < netConfig.enrMultiaddrs.len: + warn "created enr record after trimming multiaddrs", + totalMultiaddrs = netConfig.enrMultiaddrs.len, retainedMultiaddrs = retained + + return ok(record) + + return err("failed to create enr record") + proc dnsResolve*( domain: string, dnsAddrsNameServers: seq[IpAddress] ): Future[Result[string, string]] {.async.} = @@ -70,16 +91,13 @@ proc networkConfiguration*( ): Future[NetConfigResult] {.async.} = ## `udpPort` is only supplied to satisfy underlying APIs but is not ## actually a supported transport for libp2p traffic. - let natRes = setupNat( + var (extIp, extTcpPort, _) = setupNat( conf.natStrategy.string, clientId, Port(uint16(conf.p2pTcpPort) + portsShift), Port(uint16(conf.p2pTcpPort) + portsShift), - ) - if natRes.isErr(): - return err("failed to setup NAT: " & $natRes.error) - - var (extIp, extTcpPort, _) = natRes.get() + ).valueOr: + return err("failed to setup NAT: " & $error) let discv5UdpPort = @@ -101,12 +119,10 @@ proc networkConfiguration*( # Resolve and use DNS domain IP if conf.dns4DomainName.isSome() and extIp.isNone(): try: - let dnsRes = await dnsResolve(conf.dns4DomainName.get(), dnsAddrsNameServers) + let dns = (await dnsResolve(conf.dns4DomainName.get(), dnsAddrsNameServers)).valueOr: + return err($error) # Pass error down the stack - if dnsRes.isErr(): - return err($dnsRes.error) # Pass error down the stack - - extIp = some(parseIpAddress(dnsRes.get())) + extIp = some(parseIpAddress(dns)) except CatchableError: return err("Could not update extIp to resolved DNS IP: " & getCurrentExceptionMsg()) diff --git a/waku/factory/networks_config.nim b/waku/factory/networks_config.nim index c7193aa9c..488f58464 100644 --- a/waku/factory/networks_config.nim +++ b/waku/factory/networks_config.nim @@ -29,6 +29,11 @@ type NetworkConf* = object shardingConf*: ShardingConf discv5Discovery*: bool discv5BootstrapNodes*: seq[string] + enableKadDiscovery*: bool + kadBootstrapNodes*: seq[string] + entryNodes*: seq[string] + mix*: bool + p2pReliability*: bool # cluster-id=1 (aka The Waku Network) # Cluster configuration corresponding to The Waku Network. Note that it @@ -45,13 +50,75 @@ proc TheWakuNetworkConf*(T: type NetworkConf): NetworkConf = rlnEpochSizeSec: 600, rlnRelayUserMessageLimit: 100, shardingConf: ShardingConf(kind: AutoSharding, numShardsInCluster: 8), + enableKadDiscovery: false, + kadBootstrapNodes: @[], + entryNodes: @[], + mix: false, + p2pReliability: false, discv5Discovery: true, - discv5BootstrapNodes: - @[ - "enr:-QESuED0qW1BCmF-oH_ARGPr97Nv767bl_43uoy70vrbah3EaCAdK3Q0iRQ6wkSTTpdrg_dU_NC2ydO8leSlRpBX4pxiAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOTd-h5owwj-cx7xrmbvQKU8CV3Fomfdvcv1MBc-67T5oN0Y3CCdl-DdWRwgiMohXdha3UyDw", - "enr:-QEkuED9X80QF_jcN9gA2ZRhhmwVEeJnsg_Hyg7IFCTYnZD0BDI7a8HArE61NhJZFwygpHCWkgwSt2vqiABXkBxzIqZBAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPFAS8zz2cg1QQhxMaK8CzkGQ5wdHvPJcrgLzJGOiHpwYN0Y3CCdl-DdWRwgiMohXdha3UyDw", - "enr:-QEkuEBfEzJm_kigJ2HoSS_RBFJYhKHocGdkhhBr6jSUAWjLdFPp6Pj1l4yiTQp7TGHyu1kC6FyaU573VN8klLsEm-XuAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOwsS69tgD7u1K50r5-qG5hweuTwa0W26aYPnvivpNlrYN0Y3CCdl-DdWRwgiMohXdha3UyDw", - ], + discv5BootstrapNodes: @[ + "enr:-QESuED0qW1BCmF-oH_ARGPr97Nv767bl_43uoy70vrbah3EaCAdK3Q0iRQ6wkSTTpdrg_dU_NC2ydO8leSlRpBX4pxiAYJpZIJ2NIJpcIRA4VDAim11bHRpYWRkcnO4XAArNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwAtNiZub2RlLTAxLmRvLWFtczMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOTd-h5owwj-cx7xrmbvQKU8CV3Fomfdvcv1MBc-67T5oN0Y3CCdl-DdWRwgiMohXdha3UyDw", + "enr:-QEkuED9X80QF_jcN9gA2ZRhhmwVEeJnsg_Hyg7IFCTYnZD0BDI7a8HArE61NhJZFwygpHCWkgwSt2vqiABXkBxzIqZBAYJpZIJ2NIJpcIQiQlleim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmdjLXVzLWNlbnRyYWwxLWEud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQPFAS8zz2cg1QQhxMaK8CzkGQ5wdHvPJcrgLzJGOiHpwYN0Y3CCdl-DdWRwgiMohXdha3UyDw", + "enr:-QEkuEBfEzJm_kigJ2HoSS_RBFJYhKHocGdkhhBr6jSUAWjLdFPp6Pj1l4yiTQp7TGHyu1kC6FyaU573VN8klLsEm-XuAYJpZIJ2NIJpcIQI2SVcim11bHRpYWRkcnO4bgA0Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQZ2XwA2Ni9ub2RlLTAxLmFjLWNuLWhvbmdrb25nLWMud2FrdS5zYW5kYm94LnN0YXR1cy5pbQYfQN4DgnJzkwABCAAAAAEAAgADAAQABQAGAAeJc2VjcDI1NmsxoQOwsS69tgD7u1K50r5-qG5hweuTwa0W26aYPnvivpNlrYN0Y3CCdl-DdWRwgiMohXdha3UyDw", + ], + ) + +# cluster-id=2 (Logos Dev Network) +# Cluster configuration for the Logos Dev Network. +proc LogosDevConf*(T: type NetworkConf): NetworkConf = + const ZeroChainId = 0'u256 + return NetworkConf( + maxMessageSize: "150KiB", + clusterId: 2, + rlnRelay: false, + rlnRelayEthContractAddress: "", + rlnRelayDynamic: false, + rlnRelayChainId: ZeroChainId, + rlnEpochSizeSec: 0, + rlnRelayUserMessageLimit: 0, + shardingConf: ShardingConf(kind: AutoSharding, numShardsInCluster: 8), + enableKadDiscovery: true, + mix: true, + p2pReliability: true, + discv5Discovery: true, + discv5BootstrapNodes: @[], + entryNodes: @[ + "/dns4/delivery-01.do-ams3.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAmTUbnxLGT9JvV6mu9oPyDjqHK4Phs1VDJNUgESgNSkuby", + "/dns4/delivery-02.do-ams3.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAmMK7PYygBtKUQ8EHp7EfaD3bCEsJrkFooK8RQ2PVpJprH", + "/dns4/delivery-01.gc-us-central1-a.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm4S1JYkuzDKLKQvwgAhZKs9otxXqt8SCGtB4hoJP1S397", + "/dns4/delivery-02.gc-us-central1-a.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm8Y9kgBNtjxvCnf1X6gnZJW5EGE4UwwCL3CCm55TwqBiH", + "/dns4/delivery-01.ac-cn-hongkong-c.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAm8YokiNun9BkeA1ZRmhLbtNUvcwRr64F69tYj9fkGyuEP", + "/dns4/delivery-02.ac-cn-hongkong-c.logos.dev.status.im/tcp/30303/p2p/16Uiu2HAkvwhGHKNry6LACrB8TmEFoCJKEX29XR5dDUzk3UT3UNSE", + ], + ) + +# cluster-id=2 (Logos Test Network) +# Cluster configuration for the Logos Test Network. +proc LogosTestConf*(T: type NetworkConf): NetworkConf = + const ZeroChainId = 0'u256 + return NetworkConf( + maxMessageSize: "150KiB", + clusterId: 2, + rlnRelay: false, + rlnRelayEthContractAddress: "", + rlnRelayDynamic: false, + rlnRelayChainId: ZeroChainId, + rlnEpochSizeSec: 0, + rlnRelayUserMessageLimit: 0, + shardingConf: ShardingConf(kind: AutoSharding, numShardsInCluster: 8), + enableKadDiscovery: true, + mix: true, + p2pReliability: true, + discv5Discovery: true, + discv5BootstrapNodes: @[], + entryNodes: @[ + "/dns4/node-01.do-ams3.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmQ9X2xDfPG3uL77V9piYDhjq14JhKCtcmNYsTMKNqrKCj", + "/dns4/node-02.do-ams3.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmB8NYprrfQrgWVzsJtYWkfjsXbmJEGNMG6othXsQ53BwG", + "/dns4/node-01.gc-us-central1-a.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmF8WtwGPmeGHgYAX2277jHgy5cW9F7zsB8EqUjBZQAZQ3", + "/dns4/node-02.gc-us-central1-a.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmUuXhUW9bdJpzN1kfDziFiUZo4bszTk66cvr7uuyCHXR7", + "/dns4/node-01.ac-cn-hongkong-c.logos.test.status.im/tcp/30303/p2p/16Uiu2HAmL3oU95jh1BZHozn3uNhx8HEneirgr8M1jEAapzXGDqRF", + "/dns4/node-02.ac-cn-hongkong-c.logos.test.status.im/tcp/30303/p2p/16Uiu2HAm28CoBZjpyxsanC8tQpbvZ7bZJnVYuB1EgFzb571qpWsV", + ], ) proc validateShards*( diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index a5eb4f2ca..52b719b8f 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -6,7 +6,8 @@ import libp2p/protocols/pubsub/gossipsub, libp2p/protocols/connectivity/relay/relay, libp2p/nameresolving/dnsresolver, - libp2p/crypto/crypto + libp2p/crypto/crypto, + libp2p/crypto/curve25519 import ./internal_config, @@ -24,20 +25,16 @@ import ../waku_archive/retention_policy/builder as policy_builder, ../waku_archive/driver as driver, ../waku_archive/driver/builder as driver_builder, - ../waku_archive_legacy/driver as legacy_driver, - ../waku_archive_legacy/driver/builder as legacy_driver_builder, ../waku_store, ../waku_store/common as store_common, - ../waku_store_legacy, - ../waku_store_legacy/common as legacy_common, ../waku_filter_v2, ../waku_peer_exchange, + ../discovery/waku_kademlia, ../node/peer_manager, ../node/peer_manager/peer_store/waku_peer_storage, ../node/peer_manager/peer_store/migrations as peer_store_sqlite_migrations, ../waku_lightpush_legacy/common, - ../common/rate_limit/setting, - ../common/databases/dburl + ../common/rate_limit/setting ## Peer persistence @@ -47,11 +44,10 @@ proc setupPeerStorage(): Result[Option[WakuPeerStorage], string] = ?peer_store_sqlite_migrations.migrate(db) - let res = WakuPeerStorage.new(db) - if res.isErr(): - return err("failed to init peer store" & res.error) + let res = WakuPeerStorage.new(db).valueOr: + return err("failed to init peer store" & error) - ok(some(res.value)) + return ok(some(res)) ## Init waku node instance @@ -125,11 +121,10 @@ proc initNode( builder.withRateLimit(conf.rateLimit) builder.withCircuitRelay(relay) - let node = - ?builder.build().mapErr( - proc(err: string): string = - "failed to create waku node instance: " & err - ) + let node = ?builder.build().mapErr( + proc(err: string): string = + "failed to create waku node instance: " & err + ) ok(node) @@ -164,65 +159,54 @@ proc setupProtocols( error "Unrecoverable error occurred", error = msg quit(QuitFailure) + #mount mix + if conf.mixConf.isSome(): + let mixConf = conf.mixConf.get() + (await node.mountMix(conf.clusterId, mixConf.mixKey, mixConf.mixnodes)).isOkOr: + return err("failed to mount waku mix protocol: " & $error) + + # Setup extended kademlia discovery + if conf.kademliaDiscoveryConf.isSome(): + let mixPubKey = + if conf.mixConf.isSome(): + some(conf.mixConf.get().mixPubKey) + else: + none(Curve25519Key) + + node.wakuKademlia = WakuKademlia.new( + node.switch, + ExtendedKademliaDiscoveryParams( + bootstrapNodes: conf.kademliaDiscoveryConf.get().bootstrapNodes, + mixPubKey: mixPubKey, + advertiseMix: conf.mixConf.isSome(), + ), + node.peerManager, + getMixNodePoolSize = proc(): int {.gcsafe, raises: [].} = + if node.wakuMix.isNil(): + 0 + else: + node.getMixNodePoolSize(), + isNodeStarted = proc(): bool {.gcsafe, raises: [].} = + node.started, + ).valueOr: + return err("failed to setup kademlia discovery: " & error) + if conf.storeServiceConf.isSome(): let storeServiceConf = conf.storeServiceConf.get() - if storeServiceConf.supportV2: - let archiveDriverRes = await legacy_driver.ArchiveDriver.new( + + let archiveDriver = ( + await driver.ArchiveDriver.new( storeServiceConf.dbUrl, storeServiceConf.dbVacuum, storeServiceConf.dbMigration, storeServiceConf.maxNumDbConnections, onFatalErrorAction, ) - if archiveDriverRes.isErr(): - return err("failed to setup legacy archive driver: " & archiveDriverRes.error) + ).valueOr: + return err("failed to setup archive driver: " & error) - let mountArcRes = node.mountLegacyArchive(archiveDriverRes.get()) - if mountArcRes.isErr(): - return err("failed to mount waku legacy archive protocol: " & mountArcRes.error) + let retPolicies = policy.RetentionPolicy.new(storeServiceConf.retentionPolicies).valueOr: + return err("failed to create retention policy: " & error) - ## For now we always mount the future archive driver but if the legacy one is mounted, - ## then the legacy will be in charge of performing the archiving. - ## Regarding storage, the only diff between the current/future archive driver and the legacy - ## one, is that the legacy stores an extra field: the id (message digest.) - - ## TODO: remove this "migrate" variable once legacy store is removed - ## It is now necessary because sqlite's legacy store has an extra field: storedAt - ## This breaks compatibility between store's and legacy store's schemas in sqlite - ## So for now, we need to make sure that when legacy store is enabled and we use sqlite - ## that we migrate our db according to legacy store's schema to have the extra field - - let engineRes = dburl.getDbEngine(storeServiceConf.dbUrl) - if engineRes.isErr(): - return err("error getting db engine in setupProtocols: " & engineRes.error) - - let engine = engineRes.get() - - let migrate = - if engine == "sqlite" and storeServiceConf.supportV2: - false - else: - storeServiceConf.dbMigration - - let archiveDriverRes = await driver.ArchiveDriver.new( - storeServiceConf.dbUrl, storeServiceConf.dbVacuum, migrate, - storeServiceConf.maxNumDbConnections, onFatalErrorAction, - ) - if archiveDriverRes.isErr(): - return err("failed to setup archive driver: " & archiveDriverRes.error) - - let retPolicyRes = policy.RetentionPolicy.new(storeServiceConf.retentionPolicy) - if retPolicyRes.isErr(): - return err("failed to create retention policy: " & retPolicyRes.error) - - let mountArcRes = node.mountArchive(archiveDriverRes.get(), retPolicyRes.get()) - if mountArcRes.isErr(): - return err("failed to mount waku archive protocol: " & mountArcRes.error) - - if storeServiceConf.supportV2: - # Store legacy setup - try: - await mountLegacyStore(node, node.rateLimitSettings.getSetting(STOREV2)) - except CatchableError: - return - err("failed to mount waku legacy store protocol: " & getCurrentExceptionMsg()) + node.mountArchive(archiveDriver, retPolicies).isOkOr: + return err("failed to mount waku archive protocol: " & error) # Store setup try: @@ -255,12 +239,6 @@ proc setupProtocols( return err("failed to set node waku store peer: " & error) node.peerManager.addServicePeer(storeNode, WakuStoreCodec) - mountLegacyStoreClient(node) - if conf.remoteStoreNode.isSome(): - let storeNode = parsePeerInfo(conf.remoteStoreNode.get()).valueOr: - return err("failed to set node waku legacy store peer: " & error) - node.peerManager.addServicePeer(storeNode, WakuLegacyStoreCodec) - if conf.storeServiceConf.isSome and conf.storeServiceConf.get().resume: node.setupStoreResume() @@ -331,9 +309,9 @@ proc setupProtocols( protectedShard = shardKey.shard, publicKey = shardKey.key node.wakuRelay.addSignedShardsValidator(subscribedProtectedShards, conf.clusterId) - # Only relay nodes should be rendezvous points. - if conf.rendezvous: - await node.mountRendezvous(conf.clusterId) + if conf.rendezvous: + await node.mountRendezvous(conf.clusterId, shards) + await node.mountRendezvousClient(conf.clusterId) # Keepalive mounted on all nodes try: @@ -363,8 +341,11 @@ proc setupProtocols( # NOTE Must be mounted after relay if conf.lightPush: try: - await mountLightPush(node, node.rateLimitSettings.getSetting(LIGHTPUSH)) - await mountLegacyLightPush(node, node.rateLimitSettings.getSetting(LIGHTPUSH)) + (await mountLightPush(node, node.rateLimitSettings.getSetting(LIGHTPUSH))).isOkOr: + return err("failed to mount waku lightpush protocol: " & $error) + + (await mountLegacyLightPush(node, node.rateLimitSettings.getSetting(LIGHTPUSH))).isOkOr: + return err("failed to mount waku legacy lightpush protocol: " & $error) except CatchableError: return err("failed to mount waku lightpush protocol: " & getCurrentExceptionMsg()) @@ -418,14 +399,6 @@ proc setupProtocols( if conf.peerExchangeDiscovery: await node.mountPeerExchangeClient() - #mount mix - if conf.mixConf.isSome(): - ( - await node.mountMix( - conf.clusterId, conf.mixConf.get().mixKey, conf.mixConf.get().mixnodes - ) - ).isOkOr: - return err("failed to mount waku mix protocol: " & $error) return ok() ## Start node @@ -477,6 +450,11 @@ proc startNode*( if conf.relay: node.peerManager.start() + if not node.wakuKademlia.isNil(): + let minMixPeers = if conf.mixConf.isSome(): 4 else: 0 + (await node.wakuKademlia.start(minMixPeers = minMixPeers)).isOkOr: + return err("failed to start kademlia discovery: " & error) + return ok() proc setupNode*( diff --git a/waku/factory/waku.nim b/waku/factory/waku.nim index 15d73d64d..6a5567f8c 100644 --- a/waku/factory/waku.nim +++ b/waku/factory/waku.nim @@ -13,38 +13,43 @@ import libp2p/services/autorelayservice, libp2p/services/hpservice, libp2p/peerid, - libp2p/discovery/discoverymngr, - libp2p/discovery/rendezvousinterface, eth/keys, eth/p2p/discoveryv5/enr, presto, metrics, - metrics/chronos_httpserver -import - ../common/logging, - ../waku_core, - ../waku_node, - ../node/peer_manager, - ../node/health_monitor, - ../node/waku_metrics, - ../node/delivery_monitor/delivery_monitor, - ../waku_api/message_cache, - ../waku_api/rest/server, - ../waku_api/rest/builder as rest_server_builder, - ../waku_archive, - ../waku_relay/protocol, - ../discovery/waku_dnsdisc, - ../discovery/waku_discv5, - ../discovery/autonat_service, - ../waku_enr/sharding, - ../waku_rln_relay, - ../waku_store, - ../waku_filter_v2, - ../factory/node_factory, - ../factory/internal_config, - ../factory/app_callbacks, - ../waku_enr/multiaddr, - ./waku_conf + metrics/chronos_httpserver, + brokers/broker_context, + waku/[ + waku_core, + waku_node, + waku_archive, + waku_rln_relay, + waku_store, + waku_filter_v2, + waku_relay/protocol, + waku_enr/sharding, + waku_enr/multiaddr, + api/types, + common/logging, + node/peer_manager, + node/health_monitor, + node/waku_metrics, + node/delivery_service/delivery_service, + node/delivery_service/subscription_manager, + rest_api/message_cache, + rest_api/endpoint/server, + rest_api/endpoint/builder as rest_server_builder, + discovery/waku_dnsdisc, + discovery/waku_discv5, + discovery/autonat_service, + requests/health_requests, + factory/node_factory, + factory/internal_config, + factory/app_callbacks, + persistency/persistency, + ], + ./waku_conf, + ./waku_state_info logScope: topics = "wakunode waku" @@ -53,7 +58,7 @@ logScope: const git_version* {.strdefine.} = "n/a" type Waku* = ref object - version: string + stateInfo*: WakuStateInfo conf*: WakuConf rng*: ref HmacDrbgContext @@ -63,20 +68,18 @@ type Waku* = ref object dynamicBootstrapNodes*: seq[RemotePeerInfo] dnsRetryLoopHandle: Future[void] networkConnLoopHandle: Future[void] - discoveryMngr: DiscoveryManager node*: WakuNode healthMonitor*: NodeHealthMonitor - deliveryMonitor: DeliveryMonitor + deliveryService*: DeliveryService restServer*: WakuRestServerRef metricsServer*: MetricsHttpServerRef appCallbacks*: AppCallbacks -func version*(waku: Waku): string = - waku.version + brokerCtx*: BrokerContext proc setupSwitchServices( waku: Waku, conf: WakuConf, circuitRelay: Relay, rng: ref HmacDrbgContext @@ -116,7 +119,10 @@ proc newCircuitRelay(isRelayClient: bool): Relay = return Relay.new() proc setupAppCallbacks( - node: WakuNode, conf: WakuConf, appCallbacks: AppCallbacks + node: WakuNode, + conf: WakuConf, + appCallbacks: AppCallbacks, + healthMonitor: NodeHealthMonitor, ): Result[void, string] = if appCallbacks.isNil(): info "No external callbacks to be set" @@ -157,19 +163,33 @@ proc setupAppCallbacks( err("Cannot configure connectionChangeHandler callback with empty peer manager") node.peerManager.onConnectionChange = appCallbacks.connectionChangeHandler + if not appCallbacks.connectionStatusChangeHandler.isNil(): + if healthMonitor.isNil(): + return + err("Cannot configure connectionStatusChangeHandler with empty health monitor") + + healthMonitor.onConnectionStatusChange = appCallbacks.connectionStatusChangeHandler + return ok() proc new*( T: type Waku, wakuConf: WakuConf, appCallbacks: AppCallbacks = nil ): Future[Result[Waku, string]] {.async.} = let rng = crypto.newRng() + let brokerCtx = globalBrokerContext() logging.setupLog(wakuConf.logLevel, wakuConf.logFormat) ?wakuConf.validate() wakuConf.logConf() - let healthMonitor = NodeHealthMonitor.new(wakuConf.dnsAddrsNameServers) + let relay = newCircuitRelay(wakuConf.circuitRelayClient) + + let node = (await setupNode(wakuConf, rng, relay)).valueOr: + error "Failed setting up node", error = $error + return err("Failed setting up node: " & $error) + + let healthMonitor = NodeHealthMonitor.new(node, wakuConf.dnsAddrsNameServers) let restServer: WakuRestServerRef = if wakuConf.restServerConf.isSome(): @@ -183,46 +203,33 @@ proc new*( else: nil - var relay = newCircuitRelay(wakuConf.circuitRelayClient) + if not restServer.isNil(): + let boundRestPort = restServer.httpServer.address.port + node.ports.rest = boundRestPort.uint16 + wakuConf.restServerConf.get().port = boundRestPort - let node = (await setupNode(wakuConf, rng, relay)).valueOr: - error "Failed setting up node", error = $error - return err("Failed setting up node: " & $error) + # Set the extMultiAddrsOnly flag so the node knows not to replace explicit addresses + node.extMultiAddrsOnly = wakuConf.endpointConf.extMultiAddrsOnly - healthMonitor.setNodeToHealthMonitor(node) - healthMonitor.onlineMonitor.setPeerStoreToOnlineMonitor(node.switch.peerStore) - healthMonitor.onlineMonitor.addOnlineStateObserver( - node.peerManager.getOnlineStateObserver() - ) - - node.setupAppCallbacks(wakuConf, appCallbacks).isOkOr: + node.setupAppCallbacks(wakuConf, appCallbacks, healthMonitor).isOkOr: error "Failed setting up app callbacks", error = error return err("Failed setting up app callbacks: " & $error) ## Delivery Monitor - var deliveryMonitor: DeliveryMonitor - if wakuConf.p2pReliability: - if wakuConf.remoteStoreNode.isNone(): - return err("A storenode should be set when reliability mode is on") - - let deliveryMonitorRes = DeliveryMonitor.new( - node.wakuStoreClient, node.wakuRelay, node.wakuLightpushClient, - node.wakuFilterClient, - ) - if deliveryMonitorRes.isErr(): - return err("could not create delivery monitor: " & $deliveryMonitorRes.error) - deliveryMonitor = deliveryMonitorRes.get() + let deliveryService = DeliveryService.new(wakuConf.p2pReliability, node).valueOr: + return err("could not create delivery service: " & $error) var waku = Waku( - version: git_version, + stateInfo: WakuStateInfo.init(node), conf: wakuConf, rng: rng, key: wakuConf.nodeKey, node: node, healthMonitor: healthMonitor, - deliveryMonitor: deliveryMonitor, + deliveryService: deliveryService, appCallbacks: appCallbacks, restServer: restServer, + brokerCtx: brokerCtx, ) waku.setupSwitchServices(wakuConf, relay, rng) @@ -248,7 +255,7 @@ proc getPorts( return ok((tcpPort: tcpPort, websocketPort: websocketPort)) proc getRunningNetConfig(waku: ptr Waku): Future[Result[NetConfig, string]] {.async.} = - var conf = waku[].conf + let conf = waku[].conf let (tcpPort, websocketPort) = getPorts(waku[].node.switch.peerInfo.listenAddrs).valueOr: return err("Could not retrieve ports: " & error) @@ -280,6 +287,10 @@ proc updateEnr(waku: ptr Waku): Future[Result[void, string]] {.async.} = waku[].node.enr = record + # If TCP/WS was configured with port 0, node.announcedAddresses was built + # pre-bind with a port value of 0. In any case, the resync is harmless. + waku[].node.announcedAddresses = netConf.announcedAddresses + return ok() proc updateAddressInENR(waku: ptr Waku): Result[void, string] = @@ -311,11 +322,8 @@ proc updateAddressInENR(waku: ptr Waku): Result[void, string] = return ok() proc updateWaku(waku: ptr Waku): Future[Result[void, string]] {.async.} = - let conf = waku[].conf - if conf.endpointConf.p2pTcpPort == Port(0) or - (conf.websocketConf.isSome() and conf.websocketConf.get.port == Port(0)): - (await updateEnr(waku)).isOkOr: - return err("error calling updateEnr: " & $error) + (await updateEnr(waku)).isOkOr: + return err("error calling updateEnr: " & $error) ?updateAnnouncedAddrWithPrimaryIpAddr(waku[].node) @@ -328,16 +336,14 @@ proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} = await sleepAsync(30.seconds) if waku.conf.dnsDiscoveryConf.isSome(): let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get() - let dynamicBootstrapNodesRes = await waku_dnsdisc.retrieveDynamicBootstrapNodes( - dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers - ) - if dynamicBootstrapNodesRes.isErr(): - error "Retrieving dynamic bootstrap nodes failed", - error = dynamicBootstrapNodesRes.error + waku[].dynamicBootstrapNodes = ( + await waku_dnsdisc.retrieveDynamicBootstrapNodes( + dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers + ) + ).valueOr: + error "Retrieving dynamic bootstrap nodes failed", error = error continue - waku[].dynamicBootstrapNodes = dynamicBootstrapNodesRes.get() - if not waku[].wakuDiscv5.isNil(): let dynamicBootstrapEnrs = waku[].dynamicBootstrapNodes .filterIt(it.hasUdpPort()) @@ -360,7 +366,7 @@ proc startDnsDiscoveryRetryLoop(waku: ptr Waku): Future[void] {.async.} = error "failed to connect to dynamic bootstrap nodes: " & getCurrentExceptionMsg() return -proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = +proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async: (raises: []).} = if waku[].node.started: warn "startWaku: waku node already started" return ok() @@ -370,9 +376,15 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = if conf.dnsDiscoveryConf.isSome(): let dnsDiscoveryConf = waku.conf.dnsDiscoveryConf.get() - let dynamicBootstrapNodesRes = await waku_dnsdisc.retrieveDynamicBootstrapNodes( - dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers - ) + let dynamicBootstrapNodesRes = + try: + await waku_dnsdisc.retrieveDynamicBootstrapNodes( + dnsDiscoveryConf.enrTreeUrl, dnsDiscoveryConf.nameServers + ) + except CatchableError as exc: + Result[seq[RemotePeerInfo], string].err( + "Retrieving dynamic bootstrap nodes failed: " & exc.msg + ) if dynamicBootstrapNodesRes.isErr(): error "Retrieving dynamic bootstrap nodes failed", @@ -382,38 +394,99 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = else: waku[].dynamicBootstrapNodes = dynamicBootstrapNodesRes.get() + ## Initialize persistency singleton instance - we don't need the instance itself here, + ## but this ensures it's initialized before any store job starts. + discard Persistency.instance(conf.localStoragePath).valueOr: + error "Failed to initialize persistency instance", error = $error + return err("Failed to initialize persistency instance: " & $error) + (await startNode(waku.node, waku.conf, waku.dynamicBootstrapNodes)).isOkOr: return err("error while calling startNode: " & $error) - ## Update waku data that is set dynamically on node start - (await updateWaku(waku)).isOkOr: - return err("Error in updateApp: " & $error) + let bound = getPorts(waku.node.switch.peerInfo.listenAddrs).valueOr: + return err("failed to read bound ports from switch: " & $error) + waku[].node.ports.tcp = bound.tcpPort.get(Port(0)).uint16 + waku[].node.ports.webSocket = bound.websocketPort.get(Port(0)).uint16 ## Discv5 if conf.discv5Conf.isSome(): - waku[].wakuDiscV5 = waku_discv5.setupDiscoveryV5( - waku.node.enr, - waku.node.peerManager, - waku.node.topicSubscriptionQueue, - conf.discv5Conf.get(), - waku.dynamicBootstrapNodes, - waku.rng, - conf.nodeKey, - conf.endpointConf.p2pListenAddress, - conf.portsShift, - ) + waku[].wakuDiscV5 = ( + await waku_discv5.setupAndStartDiscv5( + waku.node.enr, + waku.node.peerManager, + waku.node.topicSubscriptionQueue, + conf.discv5Conf.get(), + waku.dynamicBootstrapNodes, + waku.rng, + conf.nodeKey, + conf.endpointConf.p2pListenAddress, + conf.portsShift, + ) + ).valueOr: + return err("failed to start waku discovery v5: " & error) - (await waku.wakuDiscV5.start()).isOkOr: - return err("failed to start waku discovery v5: " & $error) + waku[].node.ports.discv5Udp = waku[].wakuDiscV5.udpPort.uint16 + waku[].conf.discv5Conf.get().udpPort = waku[].wakuDiscV5.udpPort + + ## Update waku data that is set dynamically on node start + try: + (await updateWaku(waku)).isOkOr: + return err("Error in startWaku: " & $error) + except CatchableError: + return err("Caught exception in startWaku: " & getCurrentExceptionMsg()) ## Reliability - if not waku[].deliveryMonitor.isNil(): - waku[].deliveryMonitor.startDeliveryMonitor() + if not waku[].deliveryService.isNil(): + waku[].deliveryService.startDeliveryService().isOkOr: + return err("failed to start delivery service: " & $error) ## Health Monitor waku[].healthMonitor.startHealthMonitor().isOkOr: return err("failed to start health monitor: " & $error) + ## Setup RequestConnectionStatus provider + + RequestConnectionStatus.setProvider( + globalBrokerContext(), + proc(): Result[RequestConnectionStatus, string] = + try: + let healthReport = waku[].healthMonitor.getSyncNodeHealthReport() + return + ok(RequestConnectionStatus(connectionStatus: healthReport.connectionStatus)) + except CatchableError: + err("Failed to read health report: " & getCurrentExceptionMsg()), + ).isOkOr: + error "Failed to set RequestConnectionStatus provider", error = error + + ## Setup RequestProtocolHealth provider + + RequestProtocolHealth.setProvider( + globalBrokerContext(), + proc( + protocol: WakuProtocol + ): Future[Result[RequestProtocolHealth, string]] {.async.} = + try: + let protocolHealthStatus = + await waku[].healthMonitor.getProtocolHealthInfo(protocol) + return ok(RequestProtocolHealth(healthStatus: protocolHealthStatus)) + except CatchableError: + return err("Failed to get protocol health: " & getCurrentExceptionMsg()), + ).isOkOr: + error "Failed to set RequestProtocolHealth provider", error = error + + ## Setup RequestHealthReport provider + + RequestHealthReport.setProvider( + globalBrokerContext(), + proc(): Future[Result[RequestHealthReport, string]] {.async.} = + try: + let report = await waku[].healthMonitor.getNodeHealthReport() + return ok(RequestHealthReport(healthReport: report)) + except CatchableError: + return err("Failed to get health report: " & getCurrentExceptionMsg()), + ).isOkOr: + error "Failed to set RequestHealthReport provider", error = error + if conf.restServerConf.isSome(): rest_server_builder.startRestServerProtocolSupport( waku[].restServer, @@ -429,41 +502,72 @@ proc startWaku*(waku: ptr Waku): Future[Result[void, string]] {.async.} = return err ("Starting protocols support REST server failed: " & $error) if conf.metricsServerConf.isSome(): - waku[].metricsServer = ( - await ( - waku_metrics.startMetricsServerAndLogging( + try: + let (server, port) = ( + await waku_metrics.startMetricsServerAndLogging( conf.metricsServerConf.get(), conf.portsShift ) + ).valueOr: + return err("Starting monitoring and external interfaces failed: " & error) + waku[].metricsServer = server + waku[].node.ports.metrics = port.uint16 + waku[].conf.metricsServerConf.get().httpPort = port + except CatchableError: + return err( + "Caught exception starting monitoring and external interfaces failed: " & + getCurrentExceptionMsg() ) - ).valueOr: - return err("Starting monitoring and external interfaces failed: " & error) - waku[].healthMonitor.setOverallHealth(HealthStatus.READY) return ok() -proc stop*(waku: Waku): Future[void] {.async: (raises: [Exception]).} = +proc stop*(waku: Waku): Future[Result[void, string]] {.async: (raises: []).} = ## Waku shutdown if not waku.node.started: warn "stop: attempting to stop node that isn't running" - waku.healthMonitor.setOverallHealth(HealthStatus.SHUTTING_DOWN) + try: + waku.healthMonitor.setOverallHealth(HealthStatus.SHUTTING_DOWN) - if not waku.metricsServer.isNil(): - await waku.metricsServer.stop() + Persistency.reset() - if not waku.wakuDiscv5.isNil(): - await waku.wakuDiscv5.stop() + if not waku.metricsServer.isNil(): + await waku.metricsServer.stop() - if not waku.node.isNil(): - await waku.node.stop() + if not waku.wakuDiscv5.isNil(): + await waku.wakuDiscv5.stop() - if not waku.dnsRetryLoopHandle.isNil(): - await waku.dnsRetryLoopHandle.cancelAndWait() + if not waku.deliveryService.isNil(): + await waku.deliveryService.stopDeliveryService() + waku.deliveryService = nil - if not waku.healthMonitor.isNil(): - await waku.healthMonitor.stopHealthMonitor() + if not waku.node.isNil(): + await waku.node.stop() - if not waku.restServer.isNil(): - await waku.restServer.stop() + if not waku.dnsRetryLoopHandle.isNil(): + await waku.dnsRetryLoopHandle.cancelAndWait() + + if not waku.healthMonitor.isNil(): + await waku.healthMonitor.stopHealthMonitor() + + ## Clear RequestConnectionStatus provider + RequestConnectionStatus.clearProvider(waku.brokerCtx) + + if not waku.restServer.isNil(): + await waku.restServer.stop() + except Exception: + error "waku stop failed: " & getCurrentExceptionMsg() + return err("waku stop failed: " & getCurrentExceptionMsg()) + + return ok() + +proc isModeCoreAvailable*(waku: Waku): bool = + return not waku.node.wakuRelay.isNil() + +proc isModeEdgeAvailable*(waku: Waku): bool = + return + waku.node.wakuRelay.isNil() and not waku.node.wakuStoreClient.isNil() and + not waku.node.wakuFilterClient.isNil() and not waku.node.wakuLightPushClient.isNil() + +{.pop.} diff --git a/waku/factory/waku_conf.nim b/waku/factory/waku_conf.nim index fe6ead490..9edc12a44 100644 --- a/waku/factory/waku_conf.nim +++ b/waku/factory/waku_conf.nim @@ -4,12 +4,13 @@ import libp2p/crypto/crypto, libp2p/multiaddress, libp2p/crypto/curve25519, + libp2p/peerid, secp256k1, results import ../waku_rln_relay/rln_relay, - ../waku_api/rest/builder, + ../rest_api/endpoint/builder, ../discovery/waku_discv5, ../node/waku_metrics, ../common/logging, @@ -38,7 +39,7 @@ type ProtectedShard* {.requiresInit.} = object type DnsDiscoveryConf* {.requiresInit.} = object enrTreeUrl*: string - # TODO: should probably only have one set of name servers (see dnsaddrs) + # TODO: should probably only have one set of name servers (see dnsaddrs) nameServers*: seq[IpAddress] type StoreSyncConf* {.requiresInit.} = object @@ -51,13 +52,16 @@ type MixConf* = ref object mixPubKey*: Curve25519Key mixnodes*: seq[MixNodePubInfo] +type KademliaDiscoveryConf* = object + bootstrapNodes*: seq[(PeerId, seq[MultiAddress])] + ## Bootstrap nodes for extended kademlia discovery. + type StoreServiceConf* {.requiresInit.} = object dbMigration*: bool dbURl*: string dbVacuum*: bool - supportV2*: bool maxNumDbConnections*: int - retentionPolicy*: string + retentionPolicies*: seq[string] resume*: bool storeSyncConf*: Option[StoreSyncConf] @@ -109,6 +113,7 @@ type WakuConf* {.requiresInit.} = ref object metricsServerConf*: Option[MetricsServerConf] webSocketConf*: Option[WebSocketConf] mixConf*: Option[MixConf] + kademliaDiscoveryConf*: Option[KademliaDiscoveryConf] portsShift*: uint16 dnsAddrsNameServers*: seq[IpAddress] @@ -147,6 +152,8 @@ type WakuConf* {.requiresInit.} = ref object p2pReliability*: bool + localStoragePath*: string + proc logConf*(conf: WakuConf) = info "Configuration: Enabled protocols", relay = conf.relay, @@ -154,7 +161,8 @@ proc logConf*(conf: WakuConf) = store = conf.storeServiceConf.isSome(), filter = conf.filterServiceConf.isSome(), lightPush = conf.lightPush, - peerExchange = conf.peerExchangeService + peerExchange = conf.peerExchangeService, + rendezvous = conf.rendezvous info "Configuration. Network", cluster = conf.clusterId diff --git a/waku/factory/waku_state_info.nim b/waku/factory/waku_state_info.nim new file mode 100644 index 000000000..5796e04f5 --- /dev/null +++ b/waku/factory/waku_state_info.nim @@ -0,0 +1,59 @@ +## This module is aimed to collect and provide information about the state of the node, +## such as its version, metrics values, etc. +## It has been originally designed to be used by the debug API, which acts as a consumer of +## this information, but any other module can populate the information it needs to be +## accessible through the debug API. + +import std/[tables, sequtils, strutils] +import metrics, eth/p2p/discoveryv5/enr, libp2p/peerid, stew/byteutils +import waku/[waku_node, net/bound_ports] + +type + NodeInfoId* {.pure.} = enum + Version + Metrics + MyMultiaddresses + MyENR + MyPeerId + MyBoundPorts + MyMixPubKey + + WakuStateInfo* {.requiresInit.} = object + node: WakuNode + +proc getAllPossibleInfoItemIds*(self: WakuStateInfo): seq[NodeInfoId] = + ## Returns all possible options that can be queried to learn about the node's information. + var ret = newSeq[NodeInfoId](0) + for item in NodeInfoId: + ret.add(item) + return ret + +proc getMetrics(): string = + {.gcsafe.}: + return defaultRegistry.toText() ## defaultRegistry is {.global.} in metrics module + +proc getNodeInfoItem*(self: WakuStateInfo, infoItemId: NodeInfoId): string = + ## Returns the content of the info item with the given id if it exists. + case infoItemId + of NodeInfoId.Version: + return git_version + of NodeInfoId.Metrics: + return getMetrics() + of NodeInfoId.MyMultiaddresses: + return self.node.info().listenAddresses.join(",") + of NodeInfoId.MyENR: + return self.node.enr.toURI() + of NodeInfoId.MyPeerId: + return $PeerId(self.node.peerId()) + of NodeInfoId.MyBoundPorts: + return $self.node.ports + of NodeInfoId.MyMixPubKey: + ## Empty when the mix protocol is not mounted on this node. + if self.node.wakuMix.isNil(): + return "" + return self.node.wakuMix.pubKey.to0xHex() + else: + return "unknown info item id" + +proc init*(T: typedesc[WakuStateInfo], node: WakuNode): T = + return WakuStateInfo(node: node) diff --git a/waku/incentivization/eligibility_manager.nim b/waku/incentivization/eligibility_manager.nim index b10b293e1..cbbf4774c 100644 --- a/waku/incentivization/eligibility_manager.nim +++ b/waku/incentivization/eligibility_manager.nim @@ -38,14 +38,10 @@ proc getMinedTransactionReceipt( proc getTxAndTxReceipt( eligibilityManager: EligibilityManager, txHash: TxHash ): Future[Result[(TransactionObject, ReceiptObject), string]] {.async.} = - let txFuture = eligibilityManager.getTransactionByHash(txHash) - let receiptFuture = eligibilityManager.getMinedTransactionReceipt(txHash) - await allFutures(txFuture, receiptFuture) - let tx = txFuture.read() - let txReceipt = receiptFuture.read() - if txReceipt.isErr(): - return err("Cannot get tx receipt: " & txReceipt.error) - return ok((tx, txReceipt.get())) + let tx = await eligibilityManager.getTransactionByHash(txHash) + let txReceipt = (await eligibilityManager.getMinedTransactionReceipt(txHash)).valueOr: + return err("Cannot get tx receipt: " & error) + return ok((tx, txReceipt)) proc isEligibleTxId*( eligibilityManager: EligibilityManager, diff --git a/waku/net/auto_port.nim b/waku/net/auto_port.nim new file mode 100644 index 000000000..38176d27d --- /dev/null +++ b/waku/net/auto_port.nim @@ -0,0 +1,48 @@ +{.push raises: [].} + +import std/[net, random] +import chronos, results + +const + AutoPortRetryCount* = 20 + AutoPortMin = 50000'u16 + AutoPortMax = 59000'u16 + AutoPortAttemptTimeout = chronos.seconds(30) + +proc getAutoPort*(): uint16 = + var rng = initRand() + uint16(rng.rand(AutoPortMin.int .. AutoPortMax.int)) + +proc tryWithAutoPort*[T]( + startingPort: Port, + attempt: proc(p: Port): Future[Result[T, string]] {.async: (raises: []).}, +): Future[Result[T, string]] {.async: (raises: []).} = + ## If `startingPort == Port(0)`, call `attempt` up to `AutoPortRetryCount` + ## times with random ports. Otherwise call it once with `startingPort`. + ## Returns the first ok or the last err. + let autoMode = startingPort == Port(0) + let attempts = if autoMode: AutoPortRetryCount else: 1 + var lastErr = "" + for i in 1 .. attempts: + let port = + if autoMode: + Port(getAutoPort()) + else: + startingPort + let fut = attempt(port) + let res = + try: + if await fut.withTimeout(AutoPortAttemptTimeout): + await fut + else: + fut.cancelSoon() + Result[T, string].err("bind attempt timed out") + except CancelledError: + fut.cancelSoon() + Result[T, string].err("bind attempt cancelled") + if res.isOk(): + return ok(res.get()) + lastErr = res.error + if autoMode: + return err("auto-port exhausted; last error: " & lastErr) + return err("port bind failed: " & lastErr) diff --git a/waku/net/bound_ports.nim b/waku/net/bound_ports.nim new file mode 100644 index 000000000..f8f561940 --- /dev/null +++ b/waku/net/bound_ports.nim @@ -0,0 +1,20 @@ +{.push raises: [].} + +import std/json + +type BoundPorts* {.requiresInit.} = object + ## Set by the factory once each service has bound to a port. + ## A value of 0 means the service was not enabled or did not bind. + tcp*: uint16 + webSocket*: uint16 + rest*: uint16 + discv5Udp*: uint16 + metrics*: uint16 + +proc init*(T: type BoundPorts): BoundPorts = + return BoundPorts( + tcp: 0'u16, webSocket: 0'u16, rest: 0'u16, discv5Udp: 0'u16, metrics: 0'u16 + ) + +proc `$`*(p: BoundPorts): string = + return $(%*p) diff --git a/waku/node/net_config.nim b/waku/net/net_config.nim similarity index 95% rename from waku/node/net_config.nim rename to waku/net/net_config.nim index 4802694c4..fc4b42fe6 100644 --- a/waku/node/net_config.nim +++ b/waku/net/net_config.nim @@ -156,12 +156,16 @@ proc init*( if extMultiAddrs.len > 0: announcedAddresses.add(extMultiAddrs) + announcedAddresses = announcedAddresses.deduplicate() + let # enrMultiaddrs are just addresses which cannot be represented in ENR, as described in # https://rfc.vac.dev/spec/31/#many-connection-types - enrMultiaddrs = announcedAddresses.filterIt( - it.hasProtocol("dns4") or it.hasProtocol("dns6") or it.hasProtocol("ws") or - it.hasProtocol("wss") + enrMultiaddrs = deduplicate( + announcedAddresses.filterIt( + it.hasProtocol("dns4") or it.hasProtocol("dns6") or it.hasProtocol("ws") or + it.hasProtocol("wss") + ) ) ok( diff --git a/waku/node/api.nim b/waku/node/api.nim deleted file mode 100644 index 6f8f1cdd9..000000000 --- a/waku/node/api.nim +++ /dev/null @@ -1,9 +0,0 @@ -import - ./api/filter as filter_api, - ./api/lightpush as lightpush_api, - ./api/store as store_api, - ./api/relay as relay_api, - ./api/peer_exchange as peer_exchange_api, - ./api/ping as ping_api - -export filter_api, lightpush_api, store_api, relay_api, peer_exchange_api, ping_api diff --git a/waku/node/api/store.nim b/waku/node/api/store.nim deleted file mode 100644 index ddac5fbfd..000000000 --- a/waku/node/api/store.nim +++ /dev/null @@ -1,309 +0,0 @@ -{.push raises: [].} - -import - std/[options], - chronos, - chronicles, - metrics, - results, - eth/keys, - eth/p2p/discoveryv5/enr, - libp2p/crypto/crypto, - libp2p/protocols/ping, - libp2p/protocols/pubsub/gossipsub, - libp2p/protocols/pubsub/rpc/messages, - libp2p/builders, - libp2p/transports/tcptransport, - libp2p/transports/wstransport, - libp2p/utility - -import - ../waku_node, - ../../waku_core, - ../../waku_store_legacy/protocol as legacy_store, - ../../waku_store_legacy/client as legacy_store_client, - ../../waku_store_legacy/common as legacy_store_common, - ../../waku_store/protocol as store, - ../../waku_store/client as store_client, - ../../waku_store/common as store_common, - ../../waku_store/resume, - ../peer_manager, - ../../common/rate_limit/setting, - ../../waku_archive, - ../../waku_archive_legacy - -logScope: - topics = "waku node store api" - -## Waku archive -proc mountArchive*( - node: WakuNode, - driver: waku_archive.ArchiveDriver, - retentionPolicy = none(waku_archive.RetentionPolicy), -): Result[void, string] = - node.wakuArchive = waku_archive.WakuArchive.new( - driver = driver, retentionPolicy = retentionPolicy - ).valueOr: - return err("error in mountArchive: " & error) - - node.wakuArchive.start() - - return ok() - -proc mountLegacyArchive*( - node: WakuNode, driver: waku_archive_legacy.ArchiveDriver -): Result[void, string] = - node.wakuLegacyArchive = waku_archive_legacy.WakuArchive.new(driver = driver).valueOr: - return err("error in mountLegacyArchive: " & error) - - return ok() - -## Legacy Waku Store - -# TODO: Review this mapping logic. Maybe, move it to the appplication code -proc toArchiveQuery( - request: legacy_store_common.HistoryQuery -): waku_archive_legacy.ArchiveQuery = - waku_archive_legacy.ArchiveQuery( - pubsubTopic: request.pubsubTopic, - contentTopics: request.contentTopics, - cursor: request.cursor.map( - proc(cursor: HistoryCursor): waku_archive_legacy.ArchiveCursor = - waku_archive_legacy.ArchiveCursor( - pubsubTopic: cursor.pubsubTopic, - senderTime: cursor.senderTime, - storeTime: cursor.storeTime, - digest: cursor.digest, - ) - ), - startTime: request.startTime, - endTime: request.endTime, - pageSize: request.pageSize.uint, - direction: request.direction, - requestId: request.requestId, - ) - -# TODO: Review this mapping logic. Maybe, move it to the appplication code -proc toHistoryResult*( - res: waku_archive_legacy.ArchiveResult -): legacy_store_common.HistoryResult = - if res.isErr(): - let error = res.error - case res.error.kind - of waku_archive_legacy.ArchiveErrorKind.DRIVER_ERROR, - waku_archive_legacy.ArchiveErrorKind.INVALID_QUERY: - err(HistoryError(kind: HistoryErrorKind.BAD_REQUEST, cause: res.error.cause)) - else: - err(HistoryError(kind: HistoryErrorKind.UNKNOWN)) - else: - let response = res.get() - ok( - HistoryResponse( - messages: response.messages, - cursor: response.cursor.map( - proc(cursor: waku_archive_legacy.ArchiveCursor): HistoryCursor = - HistoryCursor( - pubsubTopic: cursor.pubsubTopic, - senderTime: cursor.senderTime, - storeTime: cursor.storeTime, - digest: cursor.digest, - ) - ), - ) - ) - -proc mountLegacyStore*( - node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit -) {.async.} = - info "mounting waku legacy store protocol" - - if node.wakuLegacyArchive.isNil(): - error "failed to mount waku legacy store protocol", error = "waku archive not set" - return - - # TODO: Review this handler logic. Maybe, move it to the appplication code - let queryHandler: HistoryQueryHandler = proc( - request: HistoryQuery - ): Future[legacy_store_common.HistoryResult] {.async.} = - if request.cursor.isSome(): - request.cursor.get().checkHistCursor().isOkOr: - return err(error) - - let request = request.toArchiveQuery() - let response = await node.wakuLegacyArchive.findMessagesV2(request) - return response.toHistoryResult() - - node.wakuLegacyStore = legacy_store.WakuStore.new( - node.peerManager, node.rng, queryHandler, some(rateLimit) - ) - - if node.started: - # Node has started already. Let's start store too. - await node.wakuLegacyStore.start() - - node.switch.mount( - node.wakuLegacyStore, protocolMatcher(legacy_store_common.WakuLegacyStoreCodec) - ) - -proc mountLegacyStoreClient*(node: WakuNode) = - info "mounting legacy store client" - - node.wakuLegacyStoreClient = - legacy_store_client.WakuStoreClient.new(node.peerManager, node.rng) - -proc query*( - node: WakuNode, query: legacy_store_common.HistoryQuery, peer: RemotePeerInfo -): Future[legacy_store_common.WakuStoreResult[legacy_store_common.HistoryResponse]] {. - async, gcsafe -.} = - ## Queries known nodes for historical messages - if node.wakuLegacyStoreClient.isNil(): - return err("waku legacy store client is nil") - - let queryRes = await node.wakuLegacyStoreClient.query(query, peer) - if queryRes.isErr(): - return err("legacy store client query error: " & $queryRes.error) - - let response = queryRes.get() - - return ok(response) - -# TODO: Move to application module (e.g., wakunode2.nim) -proc query*( - node: WakuNode, query: legacy_store_common.HistoryQuery -): Future[legacy_store_common.WakuStoreResult[legacy_store_common.HistoryResponse]] {. - async, gcsafe, deprecated: "Use 'node.query()' with peer destination instead" -.} = - ## Queries known nodes for historical messages - if node.wakuLegacyStoreClient.isNil(): - return err("waku legacy store client is nil") - - let peerOpt = node.peerManager.selectPeer(legacy_store_common.WakuLegacyStoreCodec) - if peerOpt.isNone(): - error "no suitable remote peers" - return err("peer_not_found_failure") - - return await node.query(query, peerOpt.get()) - -when defined(waku_exp_store_resume): - # TODO: Move to application module (e.g., wakunode2.nim) - proc resume*( - node: WakuNode, peerList: Option[seq[RemotePeerInfo]] = none(seq[RemotePeerInfo]) - ) {.async, gcsafe.} = - ## resume proc retrieves the history of waku messages published on the default waku pubsub topic since the last time the waku node has been online - ## for resume to work properly the waku node must have the store protocol mounted in the full mode (i.e., persisting messages) - ## messages are stored in the wakuStore's messages field and in the message db - ## the offline time window is measured as the difference between the current time and the timestamp of the most recent persisted waku message - ## an offset of 20 second is added to the time window to count for nodes asynchrony - ## peerList indicates the list of peers to query from. The history is fetched from the first available peer in this list. Such candidates should be found through a discovery method (to be developed). - ## if no peerList is passed, one of the peers in the underlying peer manager unit of the store protocol is picked randomly to fetch the history from. - ## The history gets fetched successfully if the dialed peer has been online during the queried time window. - if node.wakuLegacyStoreClient.isNil(): - return - - let retrievedMessages = await node.wakuLegacyStoreClient.resume(peerList) - if retrievedMessages.isErr(): - error "failed to resume store", error = retrievedMessages.error - return - - info "the number of retrieved messages since the last online time: ", - number = retrievedMessages.value - -## Waku Store - -proc toArchiveQuery(request: StoreQueryRequest): waku_archive.ArchiveQuery = - var query = waku_archive.ArchiveQuery() - - query.includeData = request.includeData - query.pubsubTopic = request.pubsubTopic - query.contentTopics = request.contentTopics - query.startTime = request.startTime - query.endTime = request.endTime - query.hashes = request.messageHashes - query.cursor = request.paginationCursor - query.direction = request.paginationForward - query.requestId = request.requestId - - if request.paginationLimit.isSome(): - query.pageSize = uint(request.paginationLimit.get()) - - return query - -proc toStoreResult(res: waku_archive.ArchiveResult): StoreQueryResult = - let response = res.valueOr: - return err(StoreError.new(300, "archive error: " & $error)) - - var res = StoreQueryResponse() - - res.statusCode = 200 - res.statusDesc = "OK" - - for i in 0 ..< response.hashes.len: - let hash = response.hashes[i] - - let kv = store_common.WakuMessageKeyValue(messageHash: hash) - - res.messages.add(kv) - - for i in 0 ..< response.messages.len: - res.messages[i].message = some(response.messages[i]) - res.messages[i].pubsubTopic = some(response.topics[i]) - - res.paginationCursor = response.cursor - - return ok(res) - -proc mountStore*( - node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit -) {.async.} = - if node.wakuArchive.isNil(): - error "failed to mount waku store protocol", error = "waku archive not set" - return - - info "mounting waku store protocol" - - let requestHandler: StoreQueryRequestHandler = proc( - request: StoreQueryRequest - ): Future[StoreQueryResult] {.async.} = - let request = request.toArchiveQuery() - let response = await node.wakuArchive.findMessages(request) - - return response.toStoreResult() - - node.wakuStore = - store.WakuStore.new(node.peerManager, node.rng, requestHandler, some(rateLimit)) - - if node.started: - await node.wakuStore.start() - - node.switch.mount(node.wakuStore, protocolMatcher(store_common.WakuStoreCodec)) - -proc mountStoreClient*(node: WakuNode) = - info "mounting store client" - - node.wakuStoreClient = store_client.WakuStoreClient.new(node.peerManager, node.rng) - -proc query*( - node: WakuNode, request: store_common.StoreQueryRequest, peer: RemotePeerInfo -): Future[store_common.WakuStoreResult[store_common.StoreQueryResponse]] {. - async, gcsafe -.} = - ## Queries known nodes for historical messages - if node.wakuStoreClient.isNil(): - return err("waku store v3 client is nil") - - let response = (await node.wakuStoreClient.query(request, peer)).valueOr: - var res = StoreQueryResponse() - res.statusCode = uint32(error.kind) - res.statusDesc = $error - - return ok(res) - - return ok(response) - -proc setupStoreResume*(node: WakuNode) = - node.wakuStoreResume = StoreResume.new( - node.peerManager, node.wakuArchive, node.wakuStoreClient - ).valueOr: - error "Failed to setup Store Resume", error = $error - return diff --git a/waku/node/delivery_monitor/delivery_callback.nim b/waku/node/delivery_monitor/delivery_callback.nim deleted file mode 100644 index c996bc7b0..000000000 --- a/waku/node/delivery_monitor/delivery_callback.nim +++ /dev/null @@ -1,17 +0,0 @@ -import ../../waku_core - -type DeliveryDirection* {.pure.} = enum - PUBLISHING - RECEIVING - -type DeliverySuccess* {.pure.} = enum - SUCCESSFUL - UNSUCCESSFUL - -type DeliveryFeedbackCallback* = proc( - success: DeliverySuccess, - dir: DeliveryDirection, - comment: string, - msgHash: WakuMessageHash, - msg: WakuMessage, -) {.gcsafe, raises: [].} diff --git a/waku/node/delivery_monitor/delivery_monitor.nim b/waku/node/delivery_monitor/delivery_monitor.nim deleted file mode 100644 index 4dda542cc..000000000 --- a/waku/node/delivery_monitor/delivery_monitor.nim +++ /dev/null @@ -1,43 +0,0 @@ -## This module helps to ensure the correct transmission and reception of messages - -import results -import chronos -import - ./recv_monitor, - ./send_monitor, - ./delivery_callback, - ../../waku_core, - ../../waku_store/client, - ../../waku_relay/protocol, - ../../waku_lightpush/client, - ../../waku_filter_v2/client - -type DeliveryMonitor* = ref object - sendMonitor: SendMonitor - recvMonitor: RecvMonitor - -proc new*( - T: type DeliveryMonitor, - storeClient: WakuStoreClient, - wakuRelay: protocol.WakuRelay, - wakuLightpushClient: WakuLightpushClient, - wakuFilterClient: WakuFilterClient, -): Result[T, string] = - ## storeClient is needed to give store visitility to DeliveryMonitor - ## wakuRelay and wakuLightpushClient are needed to give a mechanism to SendMonitor to re-publish - let sendMonitor = ?SendMonitor.new(storeClient, wakuRelay, wakuLightpushClient) - let recvMonitor = RecvMonitor.new(storeClient, wakuFilterClient) - return ok(DeliveryMonitor(sendMonitor: sendMonitor, recvMonitor: recvMonitor)) - -proc startDeliveryMonitor*(self: DeliveryMonitor) = - self.sendMonitor.startSendMonitor() - self.recvMonitor.startRecvMonitor() - -proc stopDeliveryMonitor*(self: DeliveryMonitor) {.async.} = - self.sendMonitor.stopSendMonitor() - await self.recvMonitor.stopRecvMonitor() - -proc setDeliveryCallback*(self: DeliveryMonitor, deliveryCb: DeliveryFeedbackCallback) = - ## The deliveryCb is a proc defined by the api client so that it can get delivery feedback - self.sendMonitor.setDeliveryCallback(deliveryCb) - self.recvMonitor.setDeliveryCallback(deliveryCb) diff --git a/waku/node/delivery_monitor/publish_observer.nim b/waku/node/delivery_monitor/publish_observer.nim deleted file mode 100644 index 1f517f8bd..000000000 --- a/waku/node/delivery_monitor/publish_observer.nim +++ /dev/null @@ -1,9 +0,0 @@ -import chronicles -import ../../waku_core/message/message - -type PublishObserver* = ref object of RootObj - -method onMessagePublished*( - self: PublishObserver, pubsubTopic: string, message: WakuMessage -) {.base, gcsafe, raises: [].} = - error "onMessagePublished not implemented" diff --git a/waku/node/delivery_monitor/recv_monitor.nim b/waku/node/delivery_monitor/recv_monitor.nim deleted file mode 100644 index 6ea35d301..000000000 --- a/waku/node/delivery_monitor/recv_monitor.nim +++ /dev/null @@ -1,196 +0,0 @@ -## This module is in charge of taking care of the messages that this node is expecting to -## receive and is backed by store-v3 requests to get an additional degree of certainty -## - -import std/[tables, sequtils, options] -import chronos, chronicles, libp2p/utility -import - ../../waku_core, - ./delivery_callback, - ./subscriptions_observer, - ../../waku_store/[client, common], - ../../waku_filter_v2/client, - ../../waku_core/topics - -const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries - -const MaxMessageLife = chronos.minutes(7) ## Max time we will keep track of rx messages - -const PruneOldMsgsPeriod = chronos.minutes(1) - -const DelayExtra* = chronos.seconds(5) - ## Additional security time to overlap the missing messages queries - -type TupleHashAndMsg = tuple[hash: WakuMessageHash, msg: WakuMessage] - -type RecvMessage = object - msgHash: WakuMessageHash - rxTime: Timestamp - ## timestamp of the rx message. We will not keep the rx messages forever - -type RecvMonitor* = ref object of SubscriptionObserver - topicsInterest: Table[PubsubTopic, seq[ContentTopic]] - ## Tracks message verification requests and when was the last time a - ## pubsub topic was verified for missing messages - ## The key contains pubsub-topics - - storeClient: WakuStoreClient - deliveryCb: DeliveryFeedbackCallback - - recentReceivedMsgs: seq[RecvMessage] - - msgCheckerHandler: Future[void] ## allows to stop the msgChecker async task - msgPrunerHandler: Future[void] ## removes too old messages - - startTimeToCheck: Timestamp - endTimeToCheck: Timestamp - -proc getMissingMsgsFromStore( - self: RecvMonitor, msgHashes: seq[WakuMessageHash] -): Future[Result[seq[TupleHashAndMsg], string]] {.async.} = - let storeResp: StoreQueryResponse = ( - await self.storeClient.queryToAny( - StoreQueryRequest(includeData: true, messageHashes: msgHashes) - ) - ).valueOr: - return err("getMissingMsgsFromStore: " & $error) - - let otherwiseMsg = WakuMessage() - ## message to be returned if the Option message is none - return ok( - storeResp.messages.mapIt((hash: it.messageHash, msg: it.message.get(otherwiseMsg))) - ) - -proc performDeliveryFeedback( - self: RecvMonitor, - success: DeliverySuccess, - dir: DeliveryDirection, - comment: string, - msgHash: WakuMessageHash, - msg: WakuMessage, -) {.gcsafe, raises: [].} = - ## This procs allows to bring delivery feedback to the API client - ## It requires a 'deliveryCb' to be registered beforehand. - if self.deliveryCb.isNil(): - error "deliveryCb is nil in performDeliveryFeedback", - success, dir, comment, msg_hash - return - - info "recv monitor performDeliveryFeedback", - success, dir, comment, msg_hash = shortLog(msgHash) - self.deliveryCb(success, dir, comment, msgHash, msg) - -proc msgChecker(self: RecvMonitor) {.async.} = - ## Continuously checks if a message has been received - while true: - await sleepAsync(StoreCheckPeriod) - - self.endTimeToCheck = getNowInNanosecondTime() - - var msgHashesInStore = newSeq[WakuMessageHash](0) - for pubsubTopic, cTopics in self.topicsInterest.pairs: - let storeResp: StoreQueryResponse = ( - await self.storeClient.queryToAny( - StoreQueryRequest( - includeData: false, - pubsubTopic: some(PubsubTopic(pubsubTopic)), - contentTopics: cTopics, - startTime: some(self.startTimeToCheck - DelayExtra.nanos), - endTime: some(self.endTimeToCheck + DelayExtra.nanos), - ) - ) - ).valueOr: - error "msgChecker failed to get remote msgHashes", - pubsubTopic, cTopics, error = $error - continue - - msgHashesInStore.add(storeResp.messages.mapIt(it.messageHash)) - - ## compare the msgHashes seen from the store vs the ones received directly - let rxMsgHashes = self.recentReceivedMsgs.mapIt(it.msgHash) - let missedHashes: seq[WakuMessageHash] = - msgHashesInStore.filterIt(not rxMsgHashes.contains(it)) - - ## Now retrieve the missed WakuMessages - let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) - if missingMsgsRet.isOk(): - ## Give feedback so that the api client can perfom any action with the missed messages - for msgTuple in missingMsgsRet.get(): - self.performDeliveryFeedback( - DeliverySuccess.UNSUCCESSFUL, RECEIVING, "Missed message", msgTuple.hash, - msgTuple.msg, - ) - else: - error "failed to retrieve missing messages: ", error = $missingMsgsRet.error - - ## update next check times - self.startTimeToCheck = self.endTimeToCheck - -method onSubscribe( - self: RecvMonitor, pubsubTopic: string, contentTopics: seq[string] -) {.gcsafe, raises: [].} = - info "onSubscribe", pubsubTopic, contentTopics - self.topicsInterest.withValue(pubsubTopic, contentTopicsOfInterest): - contentTopicsOfInterest[].add(contentTopics) - do: - self.topicsInterest[pubsubTopic] = contentTopics - -method onUnsubscribe( - self: RecvMonitor, pubsubTopic: string, contentTopics: seq[string] -) {.gcsafe, raises: [].} = - info "onUnsubscribe", pubsubTopic, contentTopics - - self.topicsInterest.withValue(pubsubTopic, contentTopicsOfInterest): - let remainingCTopics = - contentTopicsOfInterest[].filterIt(not contentTopics.contains(it)) - contentTopicsOfInterest[] = remainingCTopics - - if remainingCTopics.len == 0: - self.topicsInterest.del(pubsubTopic) - do: - error "onUnsubscribe unsubscribing from wrong topic", pubsubTopic, contentTopics - -proc new*( - T: type RecvMonitor, - storeClient: WakuStoreClient, - wakuFilterClient: WakuFilterClient, -): T = - ## The storeClient will help to acquire any possible missed messages - - let now = getNowInNanosecondTime() - var recvMonitor = RecvMonitor(storeClient: storeClient, startTimeToCheck: now) - - if not wakuFilterClient.isNil(): - wakuFilterClient.addSubscrObserver(recvMonitor) - - let filterPushHandler = proc( - pubsubTopic: PubsubTopic, message: WakuMessage - ) {.async, closure.} = - ## Captures all the messages recived through filter - - let msgHash = computeMessageHash(pubSubTopic, message) - let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) - recvMonitor.recentReceivedMsgs.add(rxMsg) - - wakuFilterClient.registerPushHandler(filterPushHandler) - - return recvMonitor - -proc loopPruneOldMessages(self: RecvMonitor) {.async.} = - while true: - let oldestAllowedTime = getNowInNanosecondTime() - MaxMessageLife.nanos - self.recentReceivedMsgs.keepItIf(it.rxTime > oldestAllowedTime) - await sleepAsync(PruneOldMsgsPeriod) - -proc startRecvMonitor*(self: RecvMonitor) = - self.msgCheckerHandler = self.msgChecker() - self.msgPrunerHandler = self.loopPruneOldMessages() - -proc stopRecvMonitor*(self: RecvMonitor) {.async.} = - if not self.msgCheckerHandler.isNil(): - await self.msgCheckerHandler.cancelAndWait() - if not self.msgPrunerHandler.isNil(): - await self.msgPrunerHandler.cancelAndWait() - -proc setDeliveryCallback*(self: RecvMonitor, deliveryCb: DeliveryFeedbackCallback) = - self.deliveryCb = deliveryCb diff --git a/waku/node/delivery_monitor/send_monitor.nim b/waku/node/delivery_monitor/send_monitor.nim deleted file mode 100644 index 15b16065f..000000000 --- a/waku/node/delivery_monitor/send_monitor.nim +++ /dev/null @@ -1,212 +0,0 @@ -## This module reinforces the publish operation with regular store-v3 requests. -## - -import std/[sequtils, tables] -import chronos, chronicles, libp2p/utility -import - ./delivery_callback, - ./publish_observer, - ../../waku_core, - ./not_delivered_storage/not_delivered_storage, - ../../waku_store/[client, common], - ../../waku_archive/archive, - ../../waku_relay/protocol, - ../../waku_lightpush/client - -const MaxTimeInCache* = chronos.minutes(1) - ## Messages older than this time will get completely forgotten on publication and a - ## feedback will be given when that happens - -const SendCheckInterval* = chronos.seconds(3) - ## Interval at which we check that messages have been properly received by a store node - -const MaxMessagesToCheckAtOnce = 100 - ## Max number of messages to check if they were properly archived by a store node - -const ArchiveTime = chronos.seconds(3) - ## Estimation of the time we wait until we start confirming that a message has been properly - ## received and archived by a store node - -type DeliveryInfo = object - pubsubTopic: string - msg: WakuMessage - -type SendMonitor* = ref object of PublishObserver - publishedMessages: Table[WakuMessageHash, DeliveryInfo] - ## Cache that contains the delivery info per message hash. - ## This is needed to make sure the published messages are properly published - - msgStoredCheckerHandle: Future[void] ## handle that allows to stop the async task - - notDeliveredStorage: NotDeliveredStorage - ## NOTE: this is not fully used because that might be tackled by higher abstraction layers - - storeClient: WakuStoreClient - deliveryCb: DeliveryFeedbackCallback - - wakuRelay: protocol.WakuRelay - wakuLightpushClient: WakuLightPushClient - -proc new*( - T: type SendMonitor, - storeClient: WakuStoreClient, - wakuRelay: protocol.WakuRelay, - wakuLightpushClient: WakuLightPushClient, -): Result[T, string] = - if wakuRelay.isNil() and wakuLightpushClient.isNil(): - return err( - "Could not create SendMonitor. wakuRelay or wakuLightpushClient should be set" - ) - - let notDeliveredStorage = ?NotDeliveredStorage.new() - - let sendMonitor = SendMonitor( - notDeliveredStorage: notDeliveredStorage, - storeClient: storeClient, - wakuRelay: wakuRelay, - wakuLightpushClient: wakuLightPushClient, - ) - - if not wakuRelay.isNil(): - wakuRelay.addPublishObserver(sendMonitor) - - if not wakuLightpushClient.isNil(): - wakuLightpushClient.addPublishObserver(sendMonitor) - - return ok(sendMonitor) - -proc performFeedbackAndCleanup( - self: SendMonitor, - msgsToDiscard: Table[WakuMessageHash, DeliveryInfo], - success: DeliverySuccess, - dir: DeliveryDirection, - comment: string, -) = - ## This procs allows to bring delivery feedback to the API client - ## It requires a 'deliveryCb' to be registered beforehand. - if self.deliveryCb.isNil(): - error "deliveryCb is nil in performFeedbackAndCleanup", - success, dir, comment, hashes = toSeq(msgsToDiscard.keys).mapIt(shortLog(it)) - return - - for hash, deliveryInfo in msgsToDiscard: - info "send monitor performFeedbackAndCleanup", - success, dir, comment, msg_hash = shortLog(hash) - - self.deliveryCb(success, dir, comment, hash, deliveryInfo.msg) - self.publishedMessages.del(hash) - -proc checkMsgsInStore( - self: SendMonitor, msgsToValidate: Table[WakuMessageHash, DeliveryInfo] -): Future[ - Result[ - tuple[ - publishedCorrectly: Table[WakuMessageHash, DeliveryInfo], - notYetPublished: Table[WakuMessageHash, DeliveryInfo], - ], - void, - ] -] {.async.} = - let hashesToValidate = toSeq(msgsToValidate.keys) - - let storeResp: StoreQueryResponse = ( - await self.storeClient.queryToAny( - StoreQueryRequest(includeData: false, messageHashes: hashesToValidate) - ) - ).valueOr: - error "checkMsgsInStore failed to get remote msgHashes", - hashes = hashesToValidate.mapIt(shortLog(it)), error = $error - return err() - - let publishedHashes = storeResp.messages.mapIt(it.messageHash) - - var notYetPublished: Table[WakuMessageHash, DeliveryInfo] - var publishedCorrectly: Table[WakuMessageHash, DeliveryInfo] - - for msgHash, deliveryInfo in msgsToValidate.pairs: - if publishedHashes.contains(msgHash): - publishedCorrectly[msgHash] = deliveryInfo - self.publishedMessages.del(msgHash) ## we will no longer track that message - else: - notYetPublished[msgHash] = deliveryInfo - - return ok((publishedCorrectly: publishedCorrectly, notYetPublished: notYetPublished)) - -proc processMessages(self: SendMonitor) {.async.} = - var msgsToValidate: Table[WakuMessageHash, DeliveryInfo] - var msgsToDiscard: Table[WakuMessageHash, DeliveryInfo] - - let now = getNowInNanosecondTime() - let timeToCheckThreshold = now - ArchiveTime.nanos - let maxLifeTime = now - MaxTimeInCache.nanos - - for hash, deliveryInfo in self.publishedMessages.pairs: - if deliveryInfo.msg.timestamp < maxLifeTime: - ## message is too old - msgsToDiscard[hash] = deliveryInfo - - if deliveryInfo.msg.timestamp < timeToCheckThreshold: - msgsToValidate[hash] = deliveryInfo - - ## Discard the messages that are too old - self.performFeedbackAndCleanup( - msgsToDiscard, DeliverySuccess.UNSUCCESSFUL, DeliveryDirection.PUBLISHING, - "Could not publish messages. Please try again.", - ) - - let (publishedCorrectly, notYetPublished) = ( - await self.checkMsgsInStore(msgsToValidate) - ).valueOr: - return ## the error log is printed in checkMsgsInStore - - ## Give positive feedback for the correctly published messages - self.performFeedbackAndCleanup( - publishedCorrectly, DeliverySuccess.SUCCESSFUL, DeliveryDirection.PUBLISHING, - "messages published correctly", - ) - - ## Try to publish again - for msgHash, deliveryInfo in notYetPublished.pairs: - let pubsubTopic = deliveryInfo.pubsubTopic - let msg = deliveryInfo.msg - if not self.wakuRelay.isNil(): - info "trying to publish again with wakuRelay", msgHash, pubsubTopic - (await self.wakuRelay.publish(pubsubTopic, msg)).isOkOr: - error "could not publish with wakuRelay.publish", - msgHash, pubsubTopic, error = $error - continue - - if not self.wakuLightpushClient.isNil(): - info "trying to publish again with wakuLightpushClient", msgHash, pubsubTopic - (await self.wakuLightpushClient.publishToAny(pubsubTopic, msg)).isOkOr: - error "could not publish with publishToAny", error = $error - continue - -proc checkIfMessagesStored(self: SendMonitor) {.async.} = - ## Continuously monitors that the sent messages have been received by a store node - while true: - await self.processMessages() - await sleepAsync(SendCheckInterval) - -method onMessagePublished( - self: SendMonitor, pubsubTopic: string, msg: WakuMessage -) {.gcsafe, raises: [].} = - ## Implementation of the PublishObserver interface. - ## - ## When publishing a message either through relay or lightpush, we want to add some extra effort - ## to make sure it is received to one store node. Hence, keep track of those published messages. - - info "onMessagePublished" - let msgHash = computeMessageHash(pubSubTopic, msg) - - if not self.publishedMessages.hasKey(msgHash): - self.publishedMessages[msgHash] = DeliveryInfo(pubsubTopic: pubsubTopic, msg: msg) - -proc startSendMonitor*(self: SendMonitor) = - self.msgStoredCheckerHandle = self.checkIfMessagesStored() - -proc stopSendMonitor*(self: SendMonitor) = - discard self.msgStoredCheckerHandle.cancelAndWait() - -proc setDeliveryCallback*(self: SendMonitor, deliveryCb: DeliveryFeedbackCallback) = - self.deliveryCb = deliveryCb diff --git a/waku/node/delivery_monitor/subscriptions_observer.nim b/waku/node/delivery_monitor/subscriptions_observer.nim deleted file mode 100644 index 800117ae9..000000000 --- a/waku/node/delivery_monitor/subscriptions_observer.nim +++ /dev/null @@ -1,13 +0,0 @@ -import chronicles - -type SubscriptionObserver* = ref object of RootObj - -method onSubscribe*( - self: SubscriptionObserver, pubsubTopic: string, contentTopics: seq[string] -) {.base, gcsafe, raises: [].} = - error "onSubscribe not implemented" - -method onUnsubscribe*( - self: SubscriptionObserver, pubsubTopic: string, contentTopics: seq[string] -) {.base, gcsafe, raises: [].} = - error "onUnsubscribe not implemented" diff --git a/waku/node/delivery_service/delivery_service.nim b/waku/node/delivery_service/delivery_service.nim new file mode 100644 index 000000000..f3d78d98e --- /dev/null +++ b/waku/node/delivery_service/delivery_service.nim @@ -0,0 +1,44 @@ +## This module helps to ensure the correct transmission and reception of messages + +import results +import chronos, chronicles +import + ./recv_service, + ./send_service, + ./subscription_manager, + waku/[ + waku_core, waku_node, waku_store/client, waku_relay/protocol, waku_lightpush/client + ] + +type DeliveryService* = ref object + sendService*: SendService + recvService*: RecvService + subscriptionManager*: SubscriptionManager + +proc new*( + T: type DeliveryService, useP2PReliability: bool, w: WakuNode +): Result[T, string] = + ## storeClient is needed to give store visitility to DeliveryService + ## wakuRelay and wakuLightpushClient are needed to give a mechanism to SendService to re-publish + let subscriptionManager = SubscriptionManager.new(w) + let sendService = ?SendService.new(useP2PReliability, w, subscriptionManager) + let recvService = RecvService.new(w, subscriptionManager) + + return ok( + DeliveryService( + sendService: sendService, + recvService: recvService, + subscriptionManager: subscriptionManager, + ) + ) + +proc startDeliveryService*(self: DeliveryService): Result[void, string] = + ?self.subscriptionManager.startSubscriptionManager() + self.recvService.startRecvService() + self.sendService.startSendService() + return ok() + +proc stopDeliveryService*(self: DeliveryService) {.async.} = + await self.sendService.stopSendService() + await self.recvService.stopRecvService() + await self.subscriptionManager.stopSubscriptionManager() diff --git a/waku/node/delivery_monitor/not_delivered_storage/migrations.nim b/waku/node/delivery_service/not_delivered_storage/migrations.nim similarity index 72% rename from waku/node/delivery_monitor/not_delivered_storage/migrations.nim rename to waku/node/delivery_service/not_delivered_storage/migrations.nim index 6f0b3265d..807074d64 100644 --- a/waku/node/delivery_monitor/not_delivered_storage/migrations.nim +++ b/waku/node/delivery_service/not_delivered_storage/migrations.nim @@ -4,7 +4,7 @@ import std/[tables, strutils, os], results, chronicles import ../../../common/databases/db_sqlite, ../../../common/databases/common logScope: - topics = "waku node delivery_monitor" + topics = "waku node delivery_service" const TargetSchemaVersion* = 1 # increase this when there is an update in the database schema @@ -17,10 +17,8 @@ const PeerStoreMigrationPath: string = projectRoot / "migrations" / "sent_msgs" proc migrate*(db: SqliteDatabase): DatabaseResult[void] = info "starting peer store's sqlite database migration for sent messages" - let migrationRes = - migrate(db, TargetSchemaVersion, migrationsScriptsDir = PeerStoreMigrationPath) - if migrationRes.isErr(): - return err("failed to execute migration scripts: " & migrationRes.error) + migrate(db, TargetSchemaVersion, migrationsScriptsDir = PeerStoreMigrationPath).isOkOr: + return err("failed to execute migration scripts: " & error) info "finished peer store's sqlite database migration for sent messages" ok() diff --git a/waku/node/delivery_monitor/not_delivered_storage/not_delivered_storage.nim b/waku/node/delivery_service/not_delivered_storage/not_delivered_storage.nim similarity index 93% rename from waku/node/delivery_monitor/not_delivered_storage/not_delivered_storage.nim rename to waku/node/delivery_service/not_delivered_storage/not_delivered_storage.nim index 85611310b..b0f5f5828 100644 --- a/waku/node/delivery_monitor/not_delivered_storage/not_delivered_storage.nim +++ b/waku/node/delivery_service/not_delivered_storage/not_delivered_storage.nim @@ -1,17 +1,17 @@ ## This module is aimed to keep track of the sent/published messages that are considered ## not being properly delivered. -## +## ## The archiving of such messages will happen in a local sqlite database. -## +## ## In the very first approach, we consider that a message is sent properly is it has been ## received by any store node. -## +## import results import ../../../common/databases/db_sqlite, ../../../waku_core/message/message, - ../../../node/delivery_monitor/not_delivered_storage/migrations + ../../../node/delivery_service/not_delivered_storage/migrations const NotDeliveredMessagesDbUrl = "not-delivered-messages.db" diff --git a/waku/node/delivery_service/recv_service.nim b/waku/node/delivery_service/recv_service.nim new file mode 100644 index 000000000..c4dcf4fef --- /dev/null +++ b/waku/node/delivery_service/recv_service.nim @@ -0,0 +1,3 @@ +import ./recv_service/recv_service + +export recv_service diff --git a/waku/node/delivery_service/recv_service/recv_service.nim b/waku/node/delivery_service/recv_service/recv_service.nim new file mode 100644 index 000000000..899f80f71 --- /dev/null +++ b/waku/node/delivery_service/recv_service/recv_service.nim @@ -0,0 +1,188 @@ +## This module is in charge of taking care of the messages that this node is expecting to +## receive and is backed by store-v3 requests to get an additional degree of certainty +## + +import std/[tables, sequtils, options, sets] +import chronos, chronicles, libp2p/utility +import ../[subscription_manager] +import brokers/broker_context +import + waku/[ + waku_core, + waku_store/client, + waku_store/common, + waku_filter_v2/client, + waku_core/topics, + events/message_events, + waku_node, + ] + +const StoreCheckPeriod = chronos.minutes(5) ## How often to perform store queries + +const MaxMessageLife = chronos.minutes(7) ## Max time we will keep track of rx messages + +const PruneOldMsgsPeriod = chronos.minutes(1) + +const DelayExtra* = chronos.seconds(5) + ## Additional security time to overlap the missing messages queries + +type TupleHashAndMsg = + tuple[hash: WakuMessageHash, msg: WakuMessage, pubsubTopic: PubsubTopic] + +type RecvMessage = object + msgHash: WakuMessageHash + rxTime: Timestamp + ## timestamp of the rx message. We will not keep the rx messages forever + +type RecvService* = ref object of RootObj + brokerCtx: BrokerContext + node: WakuNode + seenMsgListener: MessageSeenEventListener + subscriptionManager: SubscriptionManager + + recentReceivedMsgs: seq[RecvMessage] + + msgCheckerHandler: Future[void] ## allows to stop the msgChecker async task + msgPrunerHandler: Future[void] ## removes too old messages + + startTimeToCheck: Timestamp + endTimeToCheck: Timestamp + +proc getMissingMsgsFromStore( + self: RecvService, msgHashes: seq[WakuMessageHash] +): Future[Result[seq[TupleHashAndMsg], string]] {.async.} = + let storeResp: StoreQueryResponse = ( + await self.node.wakuStoreClient.queryToAny( + StoreQueryRequest(includeData: true, messageHashes: msgHashes) + ) + ).valueOr: + return err("getMissingMsgsFromStore: " & $error) + + let otherwiseMsg = WakuMessage() + let otherwiseTopic = PubsubTopic("") + return ok( + storeResp.messages.mapIt( + ( + hash: it.messageHash, + msg: it.message.get(otherwiseMsg), + pubsubTopic: it.pubsubTopic.get(otherwiseTopic), + ) + ) + ) + +proc processIncomingMessage( + self: RecvService, pubsubTopic: string, message: WakuMessage +): bool = + ## Return false if the incoming message is from a non-subscribed topic, + ## or if the message is a duplicate (recently-seen). Otherwise, save it as + ## recently-seen, emit a MessageReceivedEvent, and return true. + + if not self.subscriptionManager.isSubscribed(pubsubTopic, message.contentTopic): + trace "skipping message as I am not subscribed", + shard = pubsubTopic, contentTopic = message.contentTopic + return false + + let msgHash = computeMessageHash(pubsubTopic, message) + if self.recentReceivedMsgs.anyIt(it.msgHash == msgHash): + trace "skipping duplicate message", + shard = pubsubTopic, + contentTopic = message.contentTopic, + msg_hash = msgHash.to0xHex() + return false + + let rxMsg = RecvMessage(msgHash: msgHash, rxTime: message.timestamp) + self.recentReceivedMsgs.add(rxMsg) + MessageReceivedEvent.emit(self.brokerCtx, msgHash.to0xHex(), message) + return true + +proc checkStore*(self: RecvService) {.async.} = + ## Checks the store for messages that were not received directly and + ## delivers them via MessageReceivedEvent. + self.endTimeToCheck = getNowInNanosecondTime() + + ## query store and deliver new recovered messages per subscribed topic + for pubsubTopic, contentTopics in self.subscriptionManager.subscribedTopics: + let storeResp: StoreQueryResponse = ( + await self.node.wakuStoreClient.queryToAny( + StoreQueryRequest( + includeData: false, + pubsubTopic: some(pubsubTopic), + contentTopics: toSeq(contentTopics), + startTime: some(self.startTimeToCheck - DelayExtra.nanos), + endTime: some(self.endTimeToCheck + DelayExtra.nanos), + ) + ) + ).valueOr: + error "msgChecker failed to get remote msgHashes", + pubsubTopic = pubsubTopic, cTopics = toSeq(contentTopics), error = $error + continue + + ## compare the msgHashes seen from the store vs the ones received directly + let msgHashesInStore = storeResp.messages.mapIt(it.messageHash) + let rxMsgHashes = self.recentReceivedMsgs.mapIt(it.msgHash) + let missedHashes: seq[WakuMessageHash] = + msgHashesInStore.filterIt(not rxMsgHashes.contains(it)) + + if missedHashes.len > 0: + info "missed messages detected, checking store for missed messages", + pubsubTopic = pubsubTopic, missedCount = missedHashes.len + + ## Now retrieve the missing WakuMessages and deliver them + let missingMsgsRet = await self.getMissingMsgsFromStore(missedHashes) + if missingMsgsRet.isOk(): + for msgTuple in missingMsgsRet.get(): + if self.processIncomingMessage(msgTuple.pubsubTopic, msgTuple.msg): + info "recv service store-recovered message", + msg_hash = shortLog(msgTuple.hash), pubsubTopic = msgTuple.pubsubTopic + else: + error "failed to retrieve missing messages: ", error = $missingMsgsRet.error + + ## update next check times + self.startTimeToCheck = self.endTimeToCheck + +proc msgChecker(self: RecvService) {.async.} = + ## Continuously checks if a message has been received + while true: + await sleepAsync(StoreCheckPeriod) + await self.checkStore() + +proc new*(T: typedesc[RecvService], node: WakuNode, s: SubscriptionManager): T = + ## The storeClient will help to acquire any possible missed messages + + let now = getNowInNanosecondTime() + var recvService = RecvService( + node: node, + startTimeToCheck: now, + brokerCtx: node.brokerCtx, + subscriptionManager: s, + recentReceivedMsgs: @[], + ) + + return recvService + +proc loopPruneOldMessages(self: RecvService) {.async.} = + while true: + let oldestAllowedTime = getNowInNanosecondTime() - MaxMessageLife.nanos + self.recentReceivedMsgs.keepItIf(it.rxTime > oldestAllowedTime) + await sleepAsync(PruneOldMsgsPeriod) + +proc startRecvService*(self: RecvService) = + self.msgCheckerHandler = self.msgChecker() + self.msgPrunerHandler = self.loopPruneOldMessages() + + self.seenMsgListener = MessageSeenEvent.listen( + self.brokerCtx, + proc(event: MessageSeenEvent) {.async: (raises: []).} = + discard self.processIncomingMessage(event.topic, event.message), + ).valueOr: + error "Failed to set MessageSeenEvent listener", error = error + quit(QuitFailure) + +proc stopRecvService*(self: RecvService) {.async.} = + await MessageSeenEvent.dropListener(self.brokerCtx, self.seenMsgListener) + if not self.msgCheckerHandler.isNil(): + await self.msgCheckerHandler.cancelAndWait() + self.msgCheckerHandler = nil + if not self.msgPrunerHandler.isNil(): + await self.msgPrunerHandler.cancelAndWait() + self.msgPrunerHandler = nil diff --git a/waku/node/delivery_service/send_service.nim b/waku/node/delivery_service/send_service.nim new file mode 100644 index 000000000..de0dbf6a3 --- /dev/null +++ b/waku/node/delivery_service/send_service.nim @@ -0,0 +1,6 @@ +## This module reinforces the publish operation with regular store-v3 requests. +## + +import ./send_service/[send_service, delivery_task] + +export send_service, delivery_task diff --git a/waku/node/delivery_service/send_service/delivery_task.nim b/waku/node/delivery_service/send_service/delivery_task.nim new file mode 100644 index 000000000..aa1dc17d7 --- /dev/null +++ b/waku/node/delivery_service/send_service/delivery_task.nim @@ -0,0 +1,74 @@ +import std/[options, times], chronos +import brokers/broker_context +import waku/waku_core, waku/api/types, waku/requests/node_requests + +type DeliveryState* {.pure.} = enum + Entry + SuccessfullyPropagated + # message is known to be sent to the network but not yet validated + SuccessfullyValidated + # message is known to be stored at least on one store node, thus validated + FallbackRetry # retry sending with fallback processor if available + NextRoundRetry # try sending in next loop + FailedToDeliver # final state of failed delivery + +type DeliveryTask* = ref object + requestId*: RequestId + pubsubTopic*: PubsubTopic + msg*: WakuMessage + msgHash*: WakuMessageHash + tryCount*: int + state*: DeliveryState + deliveryTime*: Moment + propagateEventEmitted*: bool + errorDesc*: string + +proc new*( + T: typedesc[DeliveryTask], + requestId: RequestId, + envelop: MessageEnvelope, + brokerCtx: BrokerContext, +): Result[T, string] = + let msg = envelop.toWakuMessage() + # TODO: use sync request for such as soon as available + let relayShardRes = ( + RequestRelayShard.request(brokerCtx, none[PubsubTopic](), envelop.contentTopic) + ).valueOr: + error "RequestRelayShard.request failed", error = error + return err("Failed create DeliveryTask: " & $error) + + let pubsubTopic = relayShardRes.relayShard.toPubsubTopic() + let msgHash = computeMessageHash(pubsubTopic, msg) + + return ok( + T( + requestId: requestId, + pubsubTopic: pubsubTopic, + msg: msg, + msgHash: msgHash, + tryCount: 0, + state: DeliveryState.Entry, + ) + ) + +func `==`*(r, l: DeliveryTask): bool = + if r.isNil() == l.isNil(): + r.isNil() or r.msgHash == l.msgHash + else: + false + +proc messageAge*(self: DeliveryTask): timer.Duration = + let actual = getNanosecondTime(getTime().toUnixFloat()) + if self.msg.timestamp >= 0 and self.msg.timestamp < actual: + nanoseconds(actual - self.msg.timestamp) + else: + ZeroDuration + +proc deliveryAge*(self: DeliveryTask): timer.Duration = + if self.state == DeliveryState.SuccessfullyPropagated: + timer.Moment.now() - self.deliveryTime + else: + ZeroDuration + +proc isEphemeral*(self: DeliveryTask): bool = + return self.msg.ephemeral diff --git a/waku/node/delivery_service/send_service/lightpush_processor.nim b/waku/node/delivery_service/send_service/lightpush_processor.nim new file mode 100644 index 000000000..7a9f65c71 --- /dev/null +++ b/waku/node/delivery_service/send_service/lightpush_processor.nim @@ -0,0 +1,77 @@ +import chronicles, chronos, results +import std/options +import brokers/broker_context +import waku/node/peer_manager, waku/waku_core, waku/waku_lightpush/[common, client, rpc] + +import ./[delivery_task, send_processor] + +logScope: + topics = "send service lightpush processor" + +type LightpushSendProcessor* = ref object of BaseSendProcessor + peerManager: PeerManager + lightpushClient: WakuLightPushClient + +proc new*( + T: typedesc[LightpushSendProcessor], + peerManager: PeerManager, + lightpushClient: WakuLightPushClient, + brokerCtx: BrokerContext, +): T = + return + T(peerManager: peerManager, lightpushClient: lightpushClient, brokerCtx: brokerCtx) + +proc isLightpushPeerAvailable( + self: LightpushSendProcessor, pubsubTopic: PubsubTopic +): bool = + return self.peerManager.selectPeer(WakuLightPushCodec, some(pubsubTopic)).isSome() + +method isValidProcessor*( + self: LightpushSendProcessor, task: DeliveryTask +): bool {.gcsafe.} = + return self.isLightpushPeerAvailable(task.pubsubTopic) + +method sendImpl*( + self: LightpushSendProcessor, task: DeliveryTask +): Future[void] {.async.} = + task.tryCount.inc() + info "Trying message delivery via Lightpush", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + tryCount = task.tryCount + + let peer = self.peerManager.selectPeer(WakuLightPushCodec, some(task.pubsubTopic)).valueOr: + debug "No peer available for Lightpush, request pushed back for next round", + requestId = task.requestId + task.state = DeliveryState.NextRoundRetry + return + + let numLightpushServers = ( + await self.lightpushClient.publish(some(task.pubsubTopic), task.msg, peer) + ).valueOr: + error "LightpushSendProcessor.sendImpl failed", error = error.desc.get($error.code) + case error.code + of LightPushErrorCode.NO_PEERS_TO_RELAY, LightPushErrorCode.TOO_MANY_REQUESTS, + LightPushErrorCode.OUT_OF_RLN_PROOF, LightPushErrorCode.SERVICE_NOT_AVAILABLE, + LightPushErrorCode.INTERNAL_SERVER_ERROR: + task.state = DeliveryState.NextRoundRetry + else: + # the message is malformed, send error + task.state = DeliveryState.FailedToDeliver + task.errorDesc = error.desc.get($error.code) + task.deliveryTime = Moment.now() + return + + if numLightpushServers > 0: + info "Message propagated via Lightpush", + requestId = task.requestId, msgHash = task.msgHash.to0xHex() + task.state = DeliveryState.SuccessfullyPropagated + task.deliveryTime = Moment.now() + # TODO: with a simple retry processor it might be more accurate to say `Sent` + else: + # Controversial state, publish says ok but no peer. It should not happen. + debug "Lightpush publish returned zero peers, request pushed back for next round", + requestId = task.requestId + task.state = DeliveryState.NextRoundRetry + + return diff --git a/waku/node/delivery_service/send_service/relay_processor.nim b/waku/node/delivery_service/send_service/relay_processor.nim new file mode 100644 index 000000000..e06b664fb --- /dev/null +++ b/waku/node/delivery_service/send_service/relay_processor.nim @@ -0,0 +1,80 @@ +import std/options +import chronos, chronicles +import brokers/broker_context +import waku/[waku_core], waku/waku_lightpush/[common, rpc] +import waku/requests/health_requests +import waku/api/types +import ./[delivery_task, send_processor] + +logScope: + topics = "send service relay processor" + +type RelaySendProcessor* = ref object of BaseSendProcessor + publishProc: PushMessageHandler + fallbackStateToSet: DeliveryState + +proc new*( + T: typedesc[RelaySendProcessor], + lightpushAvailable: bool, + publishProc: PushMessageHandler, + brokerCtx: BrokerContext, +): RelaySendProcessor = + let fallbackStateToSet = + if lightpushAvailable: + DeliveryState.FallbackRetry + else: + DeliveryState.FailedToDeliver + + return RelaySendProcessor( + publishProc: publishProc, + fallbackStateToSet: fallbackStateToSet, + brokerCtx: brokerCtx, + ) + +proc isTopicHealthy(self: RelaySendProcessor, topic: PubsubTopic): bool {.gcsafe.} = + let healthReport = RequestShardTopicsHealth.request(self.brokerCtx, @[topic]).valueOr: + error "isTopicHealthy: failed to get health report", topic = topic, error = error + return false + + if healthReport.topicHealth.len() < 1: + warn "isTopicHealthy: no topic health entries", topic = topic + return false + let health = healthReport.topicHealth[0].health + debug "isTopicHealthy: topic health is ", topic = topic, health = health + return health == MINIMALLY_HEALTHY or health == SUFFICIENTLY_HEALTHY + +method isValidProcessor*( + self: RelaySendProcessor, task: DeliveryTask +): bool {.gcsafe.} = + # Topic health query is not reliable enough after a fresh subscribe... + # return self.isTopicHealthy(task.pubsubTopic) + return true + +method sendImpl*(self: RelaySendProcessor, task: DeliveryTask) {.async.} = + task.tryCount.inc() + info "Trying message delivery via Relay", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + tryCount = task.tryCount + + let noOfPublishedPeers = (await self.publishProc(task.pubsubTopic, task.msg)).valueOr: + let errorMessage = error.desc.get($error.code) + error "Failed to publish message with relay", + request = task.requestId, msgHash = task.msgHash.to0xHex(), error = errorMessage + if error.code != LightPushErrorCode.NO_PEERS_TO_RELAY: + task.state = DeliveryState.FailedToDeliver + task.errorDesc = errorMessage + else: + task.state = self.fallbackStateToSet + return + + if noOfPublishedPeers > 0: + info "Message propagated via Relay", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + noOfPeers = noOfPublishedPeers + task.state = DeliveryState.SuccessfullyPropagated + task.deliveryTime = Moment.now() + else: + # It shall not happen, but still covering it + task.state = self.fallbackStateToSet diff --git a/waku/node/delivery_service/send_service/send_processor.nim b/waku/node/delivery_service/send_service/send_processor.nim new file mode 100644 index 000000000..3782b9d4e --- /dev/null +++ b/waku/node/delivery_service/send_service/send_processor.nim @@ -0,0 +1,36 @@ +import chronos +import brokers/broker_context +import ./delivery_task + +{.push raises: [].} + +type BaseSendProcessor* = ref object of RootObj + fallbackProcessor*: BaseSendProcessor + brokerCtx*: BrokerContext + +proc chain*(self: BaseSendProcessor, next: BaseSendProcessor) = + self.fallbackProcessor = next + +method isValidProcessor*( + self: BaseSendProcessor, task: DeliveryTask +): bool {.base, gcsafe.} = + return false + +method sendImpl*( + self: BaseSendProcessor, task: DeliveryTask +): Future[void] {.async, base.} = + assert false, "Not implemented" + +method process*( + self: BaseSendProcessor, task: DeliveryTask +): Future[void] {.async, base.} = + var currentProcessor: BaseSendProcessor = self + var keepTrying = true + while not currentProcessor.isNil() and keepTrying: + if currentProcessor.isValidProcessor(task): + await currentProcessor.sendImpl(task) + currentProcessor = currentProcessor.fallbackProcessor + keepTrying = task.state == DeliveryState.FallbackRetry + + if task.state == DeliveryState.FallbackRetry: + task.state = DeliveryState.NextRoundRetry diff --git a/waku/node/delivery_service/send_service/send_service.nim b/waku/node/delivery_service/send_service/send_service.nim new file mode 100644 index 000000000..902f3aa1c --- /dev/null +++ b/waku/node/delivery_service/send_service/send_service.nim @@ -0,0 +1,273 @@ +## This module reinforces the publish operation with regular store-v3 requests. +## + +import std/[sequtils, tables, options] +import chronos, chronicles, libp2p/utility +import brokers/broker_context +import + ./[send_processor, relay_processor, lightpush_processor, delivery_task], + ../[subscription_manager], + waku/[ + waku_core, + node/waku_node, + node/peer_manager, + waku_store/client, + waku_store/common, + waku_relay/protocol, + waku_rln_relay/rln_relay, + waku_lightpush/client, + waku_lightpush/callbacks, + events/message_events, + ] + +logScope: + topics = "send service" + +# This useful util is missing from sequtils, this extends applyIt with predicate... +template applyItIf*(varSeq, pred, op: untyped) = + for i in low(varSeq) .. high(varSeq): + let it {.inject.} = varSeq[i] + if pred: + op + varSeq[i] = it + +template forEach*(varSeq, op: untyped) = + for i in low(varSeq) .. high(varSeq): + let it {.inject.} = varSeq[i] + op + +const MaxTimeInCache* = chronos.minutes(1) + ## Messages older than this time will get completely forgotten on publication and a + ## feedback will be given when that happens + +const ServiceLoopInterval* = chronos.seconds(1) + ## Interval at which we check that messages have been properly received by a store node + +const ArchiveTime = chronos.seconds(3) + ## Estimation of the time we wait until we start confirming that a message has been properly + ## received and archived by a store node + +type SendService* = ref object of RootObj + brokerCtx: BrokerContext + taskCache: seq[DeliveryTask] + ## Cache that contains the delivery task per message hash. + ## This is needed to make sure the published messages are properly published + + serviceLoopHandle: Future[void] ## handle that allows to stop the async task + sendProcessor: BaseSendProcessor + + node: WakuNode + checkStoreForMessages: bool + subscriptionManager: SubscriptionManager + +proc setupSendProcessorChain( + peerManager: PeerManager, + lightpushClient: WakuLightPushClient, + relay: WakuRelay, + rlnRelay: WakuRLNRelay, + brokerCtx: BrokerContext, +): Result[BaseSendProcessor, string] = + let isRelayAvail = not relay.isNil() + let isLightPushAvail = not lightpushClient.isNil() + + if not isRelayAvail and not isLightPushAvail: + return err("No valid send processor found for the delivery task") + + var processors = newSeq[BaseSendProcessor]() + + if isRelayAvail: + let rln: Option[WakuRLNRelay] = + if rlnRelay.isNil(): + none[WakuRLNRelay]() + else: + some(rlnRelay) + let publishProc = getRelayPushHandler(relay, rln) + + processors.add(RelaySendProcessor.new(isLightPushAvail, publishProc, brokerCtx)) + if isLightPushAvail: + processors.add(LightpushSendProcessor.new(peerManager, lightpushClient, brokerCtx)) + + var currentProcessor: BaseSendProcessor = processors[0] + for i in 1 ..< processors.len: + currentProcessor.chain(processors[i]) + currentProcessor = processors[i] + trace "Send processor chain", index = i, processor = type(processors[i]).name + + return ok(processors[0]) + +proc new*( + T: typedesc[SendService], + preferP2PReliability: bool, + w: WakuNode, + s: SubscriptionManager, +): Result[T, string] = + if w.wakuRelay.isNil() and w.wakuLightpushClient.isNil(): + return err( + "Could not create SendService. wakuRelay or wakuLightpushClient should be set" + ) + + let checkStoreForMessages = preferP2PReliability and not w.wakuStoreClient.isNil() + + let sendProcessorChain = setupSendProcessorChain( + w.peerManager, w.wakuLightPushClient, w.wakuRelay, w.wakuRlnRelay, w.brokerCtx + ).valueOr: + return err("failed to setup SendProcessorChain: " & $error) + + let sendService = SendService( + brokerCtx: w.brokerCtx, + taskCache: newSeq[DeliveryTask](), + serviceLoopHandle: nil, + sendProcessor: sendProcessorChain, + node: w, + checkStoreForMessages: checkStoreForMessages, + subscriptionManager: s, + ) + + return ok(sendService) + +proc addTask(self: SendService, task: DeliveryTask) = + self.taskCache.addUnique(task) + +proc isStorePeerAvailable*(sendService: SendService): bool = + return sendService.node.peerManager.selectPeer(WakuStoreCodec).isSome() + +proc checkMsgsInStore(self: SendService, tasksToValidate: seq[DeliveryTask]) {.async.} = + if tasksToValidate.len() == 0: + return + + if not isStorePeerAvailable(self): + warn "Skipping store validation for ", + messageCount = tasksToValidate.len(), error = "no store peer available" + return + + var hashesToValidate = tasksToValidate.mapIt(it.msgHash) + # TODO: confirm hash format for store query!!! + + let storeResp: StoreQueryResponse = ( + await self.node.wakuStoreClient.queryToAny( + StoreQueryRequest(includeData: false, messageHashes: hashesToValidate) + ) + ).valueOr: + error "Failed to get store validation for messages", + hashes = hashesToValidate.mapIt(shortLog(it)), error = $error + return + + let storedItems = storeResp.messages.mapIt(it.messageHash) + + # Set success state for messages found in store + self.taskCache.applyItIf(storedItems.contains(it.msgHash)): + it.state = DeliveryState.SuccessfullyValidated + + # set retry state for messages not found in store + hashesToValidate.keepItIf(not storedItems.contains(it)) + self.taskCache.applyItIf(hashesToValidate.contains(it.msgHash)): + it.state = DeliveryState.NextRoundRetry + +proc checkStoredMessages(self: SendService) {.async.} = + if not self.checkStoreForMessages: + return + + let tasksToValidate = self.taskCache.filterIt( + it.state == DeliveryState.SuccessfullyPropagated and it.deliveryAge() > ArchiveTime and + not it.isEphemeral() + ) + + await self.checkMsgsInStore(tasksToValidate) + +proc reportTaskResult(self: SendService, task: DeliveryTask) = + case task.state + of DeliveryState.SuccessfullyPropagated: + # TODO: in case of unable to strore check messages shall we report success instead? + if not task.propagateEventEmitted: + info "Message successfully propagated", + requestId = task.requestId, msgHash = task.msgHash.to0xHex() + MessagePropagatedEvent.emit( + self.brokerCtx, task.requestId, task.msgHash.to0xHex() + ) + task.propagateEventEmitted = true + return + of DeliveryState.SuccessfullyValidated: + info "Message successfully sent", + requestId = task.requestId, msgHash = task.msgHash.to0xHex() + MessageSentEvent.emit(self.brokerCtx, task.requestId, task.msgHash.to0xHex()) + return + of DeliveryState.FailedToDeliver: + error "Failed to send message", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + error = task.errorDesc + MessageErrorEvent.emit( + self.brokerCtx, task.requestId, task.msgHash.to0xHex(), task.errorDesc + ) + return + else: + # rest of the states are intermediate and does not translate to event + discard + + if task.messageAge() > MaxTimeInCache: + error "Failed to send message", + requestId = task.requestId, + msgHash = task.msgHash.to0xHex(), + error = "Message too old", + age = task.messageAge() + task.state = DeliveryState.FailedToDeliver + MessageErrorEvent.emit( + self.brokerCtx, + task.requestId, + task.msgHash.to0xHex(), + "Unable to send within retry time window", + ) + +proc evaluateAndCleanUp(self: SendService) = + self.taskCache.forEach(self.reportTaskResult(it)) + self.taskCache.keepItIf( + it.state != DeliveryState.SuccessfullyValidated and + it.state != DeliveryState.FailedToDeliver + ) + + # remove propagated messages when no store confirmation will follow + self.taskCache.keepItIf( + not ( + it.state == DeliveryState.SuccessfullyPropagated and + (it.isEphemeral() or not self.checkStoreForMessages) + ) + ) + +proc trySendMessages(self: SendService) {.async.} = + let tasksToSend = self.taskCache.filterIt(it.state == DeliveryState.NextRoundRetry) + + for task in tasksToSend: + # Todo, check if it has any perf gain to run them concurrent... + await self.sendProcessor.process(task) + +proc serviceLoop(self: SendService) {.async.} = + ## Continuously monitors that the sent messages have been received by a store node + while true: + await self.trySendMessages() + await self.checkStoredMessages() + self.evaluateAndCleanUp() + ## TODO: add circuit breaker to avoid infinite looping in case of persistent failures + ## Use OnlineStateChange observers to pause/resume the loop + await sleepAsync(ServiceLoopInterval) + +proc startSendService*(self: SendService) = + self.serviceLoopHandle = self.serviceLoop() + +proc stopSendService*(self: SendService) {.async.} = + if not self.serviceLoopHandle.isNil(): + await self.serviceLoopHandle.cancelAndWait() + +proc send*(self: SendService, task: DeliveryTask) {.async.} = + assert(not task.isNil(), "task for send must not be nil") + + info "SendService.send: processing delivery task", + requestId = task.requestId, msgHash = task.msgHash.to0xHex() + + self.subscriptionManager.subscribe(task.msg.contentTopic).isOkOr: + error "SendService.send: failed to subscribe to content topic", + contentTopic = task.msg.contentTopic, error = error + + await self.sendProcessor.process(task) + reportTaskResult(self, task) + if task.state != DeliveryState.FailedToDeliver: + self.addTask(task) diff --git a/waku/node/delivery_service/subscription_manager.nim b/waku/node/delivery_service/subscription_manager.nim new file mode 100644 index 000000000..393a61eae --- /dev/null +++ b/waku/node/delivery_service/subscription_manager.nim @@ -0,0 +1,596 @@ +import std/[sequtils, sets, tables, options, strutils], chronos, chronicles, results +import libp2p/[peerid, peerinfo] +import brokers/broker_context + +import + waku/[ + waku_core, + waku_core/topics, + waku_core/topics/sharding, + waku_node, + waku_relay, + waku_filter_v2/common as filter_common, + waku_filter_v2/client as filter_client, + waku_filter_v2/protocol as filter_protocol, + events/health_events, + events/peer_events, + requests/health_requests, + node/peer_manager, + node/health_monitor/topic_health, + node/health_monitor/connection_status, + ] + +# --------------------------------------------------------------------------- +# Logos Messaging API SubscriptionManager +# +# Maps all topic subscription intent and centralizes all consistency +# maintenance of the pubsub and content topic subscription model across +# the various network drivers that handle topics (Edge/Filter and Core/Relay). +# --------------------------------------------------------------------------- + +type EdgeFilterSubState* = object + peers: seq[RemotePeerInfo] + ## Filter service peers with confirmed subscriptions on this shard. + pending: seq[Future[void]] ## In-flight dial futures for peers not yet confirmed. + pendingPeers: HashSet[PeerId] ## PeerIds of peers currently being dialed. + currentHealth: TopicHealth + ## Cached health derived from peers.len; updated on every peer set change. + +func toTopicHealth*(peersCount: int): TopicHealth = + if peersCount >= HealthyThreshold: + TopicHealth.SUFFICIENTLY_HEALTHY + elif peersCount > 0: + TopicHealth.MINIMALLY_HEALTHY + else: + TopicHealth.UNHEALTHY + +type SubscriptionManager* = ref object of RootObj + node: WakuNode + contentTopicSubs: Table[PubsubTopic, HashSet[ContentTopic]] + ## Map of Shard to ContentTopic needed because e.g. WakuRelay is PubsubTopic only. + ## A present key with an empty HashSet value means pubsubtopic already subscribed + ## (via subscribePubsubTopics()) but there's no specific content topic interest yet. + edgeFilterSubStates*: Table[PubsubTopic, EdgeFilterSubState] + ## Per-shard filter subscription state for edge mode. + edgeFilterWakeup: AsyncEvent + ## Signalled when the edge filter sub loop should re-reconcile. + edgeFilterSubLoopFut: Future[void] + edgeFilterHealthLoopFut: Future[void] + peerEventListener: WakuPeerEventListener + ## Listener for peer connect/disconnect events (edge filter wakeup). + +iterator subscribedTopics*( + self: SubscriptionManager +): (PubsubTopic, HashSet[ContentTopic]) = + ## Iterate over all subscribed content topics, batched per shard. + ## This is guaranteed to return a non-empty `topics` (content topics) list on iteration. + + for pubsub, topics in self.contentTopicSubs.pairs: + # We are iterating over subscribed content topics; if we are subscribed to + # a shard but have no subscription (interest) for any content topic in that + # shard, then avoid triggering an iteration that doesn't advance the intent + # to iterate over content topic subscriptions. + if topics.len == 0: + continue + yield (pubsub, topics) + +proc edgeFilterPeerCount*(sm: SubscriptionManager, shard: PubsubTopic): int = + sm.edgeFilterSubStates.withValue(shard, state): + return state.peers.len + return 0 + +proc new*(T: typedesc[SubscriptionManager], node: WakuNode): T = + SubscriptionManager( + node: node, contentTopicSubs: initTable[PubsubTopic, HashSet[ContentTopic]]() + ) + +proc addContentTopicInterest( + self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic +): Result[void, string] = + var changed = false + if not self.contentTopicSubs.hasKey(shard): + self.contentTopicSubs[shard] = initHashSet[ContentTopic]() + changed = true + + self.contentTopicSubs.withValue(shard, cTopics): + if not cTopics[].contains(topic): + cTopics[].incl(topic) + changed = true + + if changed and not isNil(self.edgeFilterWakeup): + self.edgeFilterWakeup.fire() + + return ok() + +proc removeContentTopicInterest( + self: SubscriptionManager, shard: PubsubTopic, topic: ContentTopic +): Result[void, string] = + var changed = false + self.contentTopicSubs.withValue(shard, cTopics): + if cTopics[].contains(topic): + cTopics[].excl(topic) + changed = true + + if cTopics[].len == 0 and isNil(self.node.wakuRelay): + self.contentTopicSubs.del(shard) # We're done with cTopics here + + if changed and not isNil(self.edgeFilterWakeup): + self.edgeFilterWakeup.fire() + + return ok() + +proc subscribePubsubTopics( + self: SubscriptionManager, shards: seq[PubsubTopic] +): Result[void, string] = + if isNil(self.node.wakuRelay): + return err("subscribePubsubTopics requires a Relay") + + var errors: seq[string] + + for shard in shards: + if not self.contentTopicSubs.hasKey(shard): + self.node.subscribe((kind: PubsubSub, topic: shard), nil).isOkOr: + errors.add("shard " & shard & ": " & error) + continue + + self.contentTopicSubs[shard] = initHashSet[ContentTopic]() + + if errors.len > 0: + return err("subscribeShard errors: " & errors.join("; ")) + + return ok() + +proc getShardForContentTopic( + self: SubscriptionManager, topic: ContentTopic +): Result[PubsubTopic, string] = + if self.node.wakuAutoSharding.isSome(): + let shardObj = ?self.node.wakuAutoSharding.get().getShard(topic) + return ok($shardObj) + + return err("SubscriptionManager requires AutoSharding") + +proc isSubscribed*( + self: SubscriptionManager, topic: ContentTopic +): Result[bool, string] = + let shard = ?self.getShardForContentTopic(topic) + return ok( + self.contentTopicSubs.hasKey(shard) and self.contentTopicSubs[shard].contains(topic) + ) + +proc isSubscribed*( + self: SubscriptionManager, shard: PubsubTopic, contentTopic: ContentTopic +): bool {.raises: [].} = + self.contentTopicSubs.withValue(shard, cTopics): + return cTopics[].contains(contentTopic) + return false + +proc subscribe*(self: SubscriptionManager, topic: ContentTopic): Result[void, string] = + if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient): + return err("SubscriptionManager requires either Relay or Filter Client.") + + let shard = ?self.getShardForContentTopic(topic) + + if not isNil(self.node.wakuRelay) and not self.contentTopicSubs.hasKey(shard): + ?self.subscribePubsubTopics(@[shard]) + + ?self.addContentTopicInterest(shard, topic) + + return ok() + +proc unsubscribe*( + self: SubscriptionManager, topic: ContentTopic +): Result[void, string] = + if isNil(self.node.wakuRelay) and isNil(self.node.wakuFilterClient): + return err("SubscriptionManager requires either Relay or Filter Client.") + + let shard = ?self.getShardForContentTopic(topic) + + if self.isSubscribed(shard, topic): + ?self.removeContentTopicInterest(shard, topic) + + return ok() + +# --------------------------------------------------------------------------- +# Edge Filter driver for the Logos Messaging API +# +# The SubscriptionManager absorbs natively the responsibility of using the +# Edge Filter protocol to effect subscriptions and message receipt for edge. +# --------------------------------------------------------------------------- + +const EdgeFilterSubscribeTimeout = chronos.seconds(15) + ## Timeout for a single filter subscribe/unsubscribe RPC to a service peer. +const EdgeFilterPingTimeout = chronos.seconds(5) + ## Timeout for a filter ping health check. +const EdgeFilterLoopInterval = chronos.seconds(30) + ## Interval for the edge filter health ping loop. +const EdgeFilterSubLoopDebounce = chronos.seconds(1) + ## Debounce delay to coalesce rapid-fire wakeups into a single reconciliation pass. + +type EdgeDialTask = object + peer: RemotePeerInfo + shard: PubsubTopic + topics: seq[ContentTopic] + +proc updateShardHealth( + self: SubscriptionManager, shard: PubsubTopic, state: var EdgeFilterSubState +) = + ## Recompute and emit health for a shard after its peer set changed. + let newHealth = toTopicHealth(state.peers.len) + if newHealth != state.currentHealth: + state.currentHealth = newHealth + EventShardTopicHealthChange.emit(self.node.brokerCtx, shard, newHealth) + +proc removePeer(self: SubscriptionManager, shard: PubsubTopic, peerId: PeerId) = + ## Remove a peer from edgeFilterSubStates for the given shard, + ## update health, and wake the sub loop to dial a replacement. + ## Best-effort unsubscribe so the service peer stops pushing to us. + self.edgeFilterSubStates.withValue(shard, state): + var peer: RemotePeerInfo + var found = false + for p in state.peers: + if p.peerId == peerId: + peer = p + found = true + break + if not found: + return + + state.peers.keepItIf(it.peerId != peerId) + self.updateShardHealth(shard, state[]) + self.edgeFilterWakeup.fire() + + if not self.node.wakuFilterClient.isNil(): + self.contentTopicSubs.withValue(shard, topics): + let ct = toSeq(topics[]) + if ct.len > 0: + proc doUnsubscribe() {.async.} = + discard await self.node.wakuFilterClient.unsubscribe(peer, shard, ct) + + asyncSpawn doUnsubscribe() + +type SendChunkedFilterRpcKind = enum + FilterSubscribe + FilterUnsubscribe + +proc sendChunkedFilterRpc( + self: SubscriptionManager, + peer: RemotePeerInfo, + shard: PubsubTopic, + topics: seq[ContentTopic], + kind: SendChunkedFilterRpcKind, +): Future[bool] {.async.} = + ## Send a chunked filter subscribe or unsubscribe RPC. Returns true on + ## success. On failure the peer is removed and false is returned. + try: + var i = 0 + while i < topics.len: + let chunk = + topics[i ..< min(i + filter_protocol.MaxContentTopicsPerRequest, topics.len)] + let fut = + case kind + of FilterSubscribe: + self.node.wakuFilterClient.subscribe(peer, shard, chunk) + of FilterUnsubscribe: + self.node.wakuFilterClient.unsubscribe(peer, shard, chunk) + if not (await fut.withTimeout(EdgeFilterSubscribeTimeout)) or fut.read().isErr(): + trace "sendChunkedFilterRpc: chunk failed", + op = kind, shard = shard, peer = peer.peerId + self.removePeer(shard, peer.peerId) + return false + i += filter_protocol.MaxContentTopicsPerRequest + except CatchableError as exc: + debug "sendChunkedFilterRpc: failed", + op = kind, shard = shard, peer = peer.peerId, err = exc.msg + self.removePeer(shard, peer.peerId) + return false + return true + +proc syncFilterDeltas( + self: SubscriptionManager, + peer: RemotePeerInfo, + shard: PubsubTopic, + added: seq[ContentTopic], + removed: seq[ContentTopic], +) {.async.} = + ## Push content topic changes (adds/removes) to an already-tracked peer. + if added.len > 0: + if not await self.sendChunkedFilterRpc(peer, shard, added, FilterSubscribe): + return + + if removed.len > 0: + discard await self.sendChunkedFilterRpc(peer, shard, removed, FilterUnsubscribe) + +proc dialFilterPeer( + self: SubscriptionManager, + peer: RemotePeerInfo, + shard: PubsubTopic, + contentTopics: seq[ContentTopic], +) {.async.} = + ## Subscribe a new peer to all content topics on a shard and start tracking it. + self.edgeFilterSubStates.withValue(shard, state): + state.pendingPeers.incl(peer.peerId) + + try: + if not await self.sendChunkedFilterRpc(peer, shard, contentTopics, FilterSubscribe): + return + + self.edgeFilterSubStates.withValue(shard, state): + if state.peers.anyIt(it.peerId == peer.peerId): + trace "dialFilterPeer: peer already tracked, skipping duplicate", + shard = shard, peer = peer.peerId + return + + state.peers.add(peer) + self.updateShardHealth(shard, state[]) + trace "dialFilterPeer: successfully subscribed to all chunks", + shard = shard, peer = peer.peerId, totalPeers = state.peers.len + do: + trace "dialFilterPeer: shard removed while subscribing, discarding result", + shard = shard, peer = peer.peerId + finally: + self.edgeFilterSubStates.withValue(shard, state): + state.pendingPeers.excl(peer.peerId) + +proc edgeFilterHealthLoop*(self: SubscriptionManager) {.async.} = + ## Periodically pings all connected filter service peers to verify they are + ## still alive at the application layer. Peers that fail the ping are removed. + while true: + await sleepAsync(EdgeFilterLoopInterval) + + if self.node.wakuFilterClient.isNil(): + warn "filter client is nil within edge filter health loop" + continue + + var connected = initTable[PeerId, RemotePeerInfo]() + for state in self.edgeFilterSubStates.values: + for peer in state.peers: + if self.node.peerManager.switch.peerStore.isConnected(peer.peerId): + connected[peer.peerId] = peer + + var alive = initHashSet[PeerId]() + + if connected.len > 0: + var pingTasks: seq[(PeerId, Future[FilterSubscribeResult])] + for peer in connected.values: + pingTasks.add( + (peer.peerId, self.node.wakuFilterClient.ping(peer, EdgeFilterPingTimeout)) + ) + + # extract future tasks from (PeerId, Future) tuples and await them + await allFutures(pingTasks.mapIt(it[1])) + + for (peerId, task) in pingTasks: + if task.read().isOk(): + alive.incl(peerId) + + var changed = false + for shard, state in self.edgeFilterSubStates.mpairs: + let oldLen = state.peers.len + state.peers.keepItIf(it.peerId notin connected or alive.contains(it.peerId)) + + if state.peers.len < oldLen: + changed = true + self.updateShardHealth(shard, state) + trace "Edge Filter health degraded by Ping failure", + shard = shard, new = state.currentHealth + + if changed: + self.edgeFilterWakeup.fire() + +proc selectFilterCandidates( + self: SubscriptionManager, shard: PubsubTopic, exclude: HashSet[PeerId], needed: int +): seq[RemotePeerInfo] = + ## Select filter service peer candidates for a shard. + + # Start with every filter server peer that can serve the shard + var allCandidates = self.node.peerManager.selectPeers( + filter_common.WakuFilterSubscribeCodec, some(shard) + ) + + # Remove all already used in this shard or being dialed for it + allCandidates.keepItIf(it.peerId notin exclude) + + # Collect peer IDs already tracked on other shards + var trackedOnOther = initHashSet[PeerId]() + for otherShard, otherState in self.edgeFilterSubStates.pairs: + if otherShard != shard: + for peer in otherState.peers: + trackedOnOther.incl(peer.peerId) + + # Prefer peers we already have a connection to first, preserving shuffle + var candidates = + allCandidates.filterIt(it.peerId in trackedOnOther) & + allCandidates.filterIt(it.peerId notin trackedOnOther) + + # We need to return 'needed' peers only + if candidates.len > needed: + candidates.setLen(needed) + return candidates + +proc edgeFilterSubLoop*(self: SubscriptionManager) {.async.} = + ## Reconciles filter subscriptions with the desired state from SubscriptionManager. + var lastSynced = initTable[PubsubTopic, HashSet[ContentTopic]]() + + while true: + await self.edgeFilterWakeup.wait() + await sleepAsync(EdgeFilterSubLoopDebounce) + self.edgeFilterWakeup.clear() + trace "edgeFilterSubLoop: woke up" + + if isNil(self.node.wakuFilterClient): + trace "edgeFilterSubLoop: wakuFilterClient is nil, skipping" + continue + + let desired = self.contentTopicSubs + + trace "edgeFilterSubLoop: desired state", numShards = desired.len + + let allShards = toHashSet(toSeq(desired.keys)) + toHashSet(toSeq(lastSynced.keys)) + + # Step 1: read state across all shards at once and + # create a list of peer dial tasks and shard tracking to delete. + + var dialTasks: seq[EdgeDialTask] + var shardsToDelete: seq[PubsubTopic] + + for shard in allShards: + let currTopics = desired.getOrDefault(shard) + let prevTopics = lastSynced.getOrDefault(shard) + + if shard notin self.edgeFilterSubStates: + self.edgeFilterSubStates[shard] = + EdgeFilterSubState(currentHealth: TopicHealth.UNHEALTHY) + + let addedTopics = toSeq(currTopics - prevTopics) + let removedTopics = toSeq(prevTopics - currTopics) + + self.edgeFilterSubStates.withValue(shard, state): + state.peers.keepItIf( + self.node.peerManager.switch.peerStore.isConnected(it.peerId) + ) + state.pending.keepItIf(not it.finished) + + if addedTopics.len > 0 or removedTopics.len > 0: + for peer in state.peers: + asyncSpawn self.syncFilterDeltas(peer, shard, addedTopics, removedTopics) + + if currTopics.len == 0: + shardsToDelete.add(shard) + else: + self.updateShardHealth(shard, state[]) + + let needed = max(0, HealthyThreshold - state.peers.len - state.pending.len) + + if needed > 0: + let tracked = state.peers.mapIt(it.peerId).toHashSet() + state.pendingPeers + let candidates = self.selectFilterCandidates(shard, tracked, needed) + let toDial = min(needed, candidates.len) + + trace "edgeFilterSubLoop: shard reconciliation", + shard = shard, + num_peers = state.peers.len, + num_pending = state.pending.len, + num_needed = needed, + num_available = candidates.len, + toDial = toDial + + for i in 0 ..< toDial: + dialTasks.add( + EdgeDialTask( + peer: candidates[i], shard: shard, topics: toSeq(currTopics) + ) + ) + + # Step 2: execute deferred shard tracking deletion and dial tasks. + + for shard in shardsToDelete: + self.edgeFilterSubStates.withValue(shard, state): + for fut in state.pending: + if not fut.finished: + await fut.cancelAndWait() + self.edgeFilterSubStates.del(shard) + + for task in dialTasks: + let fut = self.dialFilterPeer(task.peer, task.shard, task.topics) + self.edgeFilterSubStates.withValue(task.shard, state): + state.pending.add(fut) + + lastSynced = desired + +proc startEdgeFilterLoops(self: SubscriptionManager): Result[void, string] = + ## Start the edge filter orchestration loops. + ## Caller must ensure this is only called in edge mode (relay nil, filter client present). + self.edgeFilterWakeup = newAsyncEvent() + + self.peerEventListener = WakuPeerEvent.listen( + self.node.brokerCtx, + proc(evt: WakuPeerEvent) {.async: (raises: []), gcsafe.} = + if evt.kind == WakuPeerEventKind.EventDisconnected or + evt.kind == WakuPeerEventKind.EventMetadataUpdated: + self.edgeFilterWakeup.fire() + , + ).valueOr: + return err("Failed to listen to peer events for edge filter: " & error) + + self.edgeFilterSubLoopFut = self.edgeFilterSubLoop() + self.edgeFilterHealthLoopFut = self.edgeFilterHealthLoop() + return ok() + +proc stopEdgeFilterLoops(self: SubscriptionManager) {.async: (raises: []).} = + ## Stop the edge filter orchestration loops and clean up pending futures. + if not isNil(self.edgeFilterSubLoopFut): + await self.edgeFilterSubLoopFut.cancelAndWait() + self.edgeFilterSubLoopFut = nil + + if not isNil(self.edgeFilterHealthLoopFut): + await self.edgeFilterHealthLoopFut.cancelAndWait() + self.edgeFilterHealthLoopFut = nil + + for shard, state in self.edgeFilterSubStates: + for fut in state.pending: + if not fut.finished: + await fut.cancelAndWait() + + await WakuPeerEvent.dropListener(self.node.brokerCtx, self.peerEventListener) + +# --------------------------------------------------------------------------- +# SubscriptionManager Lifecycle (calls Edge behavior above) +# +# startSubscriptionManager and stopSubscriptionManager orchestrate both the +# core (relay) and edge (filter) paths, and register/clear broker providers. +# --------------------------------------------------------------------------- + +proc startSubscriptionManager*(self: SubscriptionManager): Result[void, string] = + # Register edge filter broker providers. The shard/content health providers + # in WakuNode query these via the broker as a fallback when relay health is + # not available. If edge mode is not active, these providers simply return + # NOT_SUBSCRIBED / strength 0, which is harmless. + RequestEdgeShardHealth.setProvider( + self.node.brokerCtx, + proc(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] = + self.edgeFilterSubStates.withValue(shard, state): + return ok(RequestEdgeShardHealth(health: state.currentHealth)) + return ok(RequestEdgeShardHealth(health: TopicHealth.NOT_SUBSCRIBED)), + ).isOkOr: + error "Can't set provider for RequestEdgeShardHealth", error = error + + RequestEdgeFilterPeerCount.setProvider( + self.node.brokerCtx, + proc(): Result[RequestEdgeFilterPeerCount, string] = + var minPeers = high(int) + for state in self.edgeFilterSubStates.values: + minPeers = min(minPeers, state.peers.len) + if minPeers == high(int): + minPeers = 0 + return ok(RequestEdgeFilterPeerCount(peerCount: minPeers)), + ).isOkOr: + error "Can't set provider for RequestEdgeFilterPeerCount", error = error + + if self.node.wakuRelay.isNil(): + return self.startEdgeFilterLoops() + + # Core mode: auto-subscribe relay to all shards in autosharding. + if self.node.wakuAutoSharding.isSome(): + let autoSharding = self.node.wakuAutoSharding.get() + let clusterId = autoSharding.clusterId + let numShards = autoSharding.shardCountGenZero + + if numShards > 0: + var clusterPubsubTopics = newSeqOfCap[PubsubTopic](numShards) + + for i in 0 ..< numShards: + let shardObj = RelayShard(clusterId: clusterId, shardId: uint16(i)) + clusterPubsubTopics.add(PubsubTopic($shardObj)) + + self.subscribePubsubTopics(clusterPubsubTopics).isOkOr: + error "Failed to auto-subscribe Relay to cluster shards: ", error = error + else: + info "SubscriptionManager has no AutoSharding configured; skipping auto-subscribe." + + return ok() + +proc stopSubscriptionManager*(self: SubscriptionManager) {.async: (raises: []).} = + if self.node.wakuRelay.isNil(): + await self.stopEdgeFilterLoops() + RequestEdgeShardHealth.clearProvider(self.node.brokerCtx) + RequestEdgeFilterPeerCount.clearProvider(self.node.brokerCtx) diff --git a/waku/node/health_monitor.nim b/waku/node/health_monitor.nim index 854a8bbc0..6e42352d4 100644 --- a/waku/node/health_monitor.nim +++ b/waku/node/health_monitor.nim @@ -1,4 +1,9 @@ import - health_monitor/[node_health_monitor, protocol_health, online_monitor, health_status] + health_monitor/[ + node_health_monitor, protocol_health, online_monitor, health_status, + connection_status, health_report, + ] -export node_health_monitor, protocol_health, online_monitor, health_status +export + node_health_monitor, protocol_health, online_monitor, health_status, + connection_status, health_report diff --git a/waku/node/health_monitor/connection_status.nim b/waku/node/health_monitor/connection_status.nim new file mode 100644 index 000000000..68ec9d4be --- /dev/null +++ b/waku/node/health_monitor/connection_status.nim @@ -0,0 +1,18 @@ +import chronos, results, std/strutils, ../../api/types + +export ConnectionStatus + +const HealthyThreshold* = 2 + ## Minimum peers required per service protocol for a "Connected" status (excluding Relay). + +proc init*( + t: typedesc[ConnectionStatus], strRep: string +): Result[ConnectionStatus, string] = + try: + let status = parseEnum[ConnectionStatus](strRep) + return ok(status) + except ValueError: + return err("Invalid ConnectionStatus string representation: " & strRep) + +type ConnectionStatusChangeHandler* = + proc(status: ConnectionStatus): Future[void] {.gcsafe, raises: [Defect].} diff --git a/waku/node/health_monitor/event_loop_monitor.nim b/waku/node/health_monitor/event_loop_monitor.nim new file mode 100644 index 000000000..bd1a33e4e --- /dev/null +++ b/waku/node/health_monitor/event_loop_monitor.nim @@ -0,0 +1,83 @@ +{.push raises: [].} + +import std/math +import chronos, chronicles, metrics + +logScope: + topics = "waku event_loop_monitor" + +declarePublicGauge event_loop_load, + "chronos event loop load EWMA by window (1.0 = sustained lag at MaxAcceptedLag)", + labels = ["window"] + +declarePublicCounter event_loop_accumulated_lag_secs, + "chronos event loop total accumulated lag in seconds since node start" + +type OnLagChange* = proc(lagTooHigh: bool) {.gcsafe, raises: [].} + +proc eventLoopMonitorLoop*(onLagChange: OnLagChange = nil) {.async.} = + ## Monitors chronos event loop responsiveness by measuring how much each + ## iteration oversleeps its `CheckInterval`. + ## + ## The lag is normalised against `MaxAcceptedLag` and tracked as an EWMA + ## over 1, 5, and 15-minute windows (Unix load-average decay model), + ## exposed via the `event_loop_load` gauge (labelled by window: 1m/5m/15m): + ## + ## load < 1.0 → within budget + ## load = 1.0 → sustained lag at MaxAcceptedLag (fully loaded) + ## load > 1.0 → over budget; e.g. 2.0 means twice the accepted lag + ## + ## `onLagChange` is called when instantaneous lag crosses `MaxAcceptedLag`. + + const CheckInterval = 5.seconds + const MaxAcceptedLag = 50.milliseconds + + # Decay factors: α = 1 − e^(−CheckInterval_secs / window_secs) + # Mirrors the Unix load-average convention so each EWMA has a half-life equal + # to its named window. + const alpha1m = 1.0 - exp(-5.0 / 60.0) # ≈ 0.0821 + const alpha5m = 1.0 - exp(-5.0 / 300.0) # ≈ 0.0165 + const alpha15m = 1.0 - exp(-5.0 / 900.0) # ≈ 0.0055 + + var ewma1m = 0.0 + var ewma5m = 0.0 + var ewma15m = 0.0 + + var now = Moment.now() + var lagWasHigh = false + + while true: + let lastWakeup = now + await sleepAsync(CheckInterval) + now = Moment.now() + + let actualElapsed = now - lastWakeup + let lag = max(ZeroDuration, actualElapsed - CheckInterval) + const maxAcceptedLagSecs = MaxAcceptedLag.nanoseconds.float64 / 1_000_000_000.0 + + let lagSecs = lag.nanoseconds.float64 / 1_000_000_000.0 + let load = lagSecs / maxAcceptedLagSecs + + event_loop_accumulated_lag_secs.inc(lagSecs) + + ewma1m = alpha1m * load + (1.0 - alpha1m) * ewma1m + ewma5m = alpha5m * load + (1.0 - alpha5m) * ewma5m + ewma15m = alpha15m * load + (1.0 - alpha15m) * ewma15m + + event_loop_load.set(round(ewma1m, 4), labelValues = ["1m"]) + event_loop_load.set(round(ewma5m, 4), labelValues = ["5m"]) + event_loop_load.set(round(ewma15m, 4), labelValues = ["15m"]) + + let lagIsHigh = lag > MaxAcceptedLag + + if lag > CheckInterval: + warn "chronos event loop severely lagging, many tasks may be accumulating", + expected_secs = CheckInterval.seconds, + lag_secs = round(lagSecs, 4), + load_1m = round(ewma1m, 4), + load_5m = round(ewma5m, 4), + load_15m = round(ewma15m, 4) + + if not onLagChange.isNil() and lagIsHigh != lagWasHigh: + lagWasHigh = lagIsHigh + onLagChange(lagIsHigh) diff --git a/waku/node/health_monitor/health_report.nim b/waku/node/health_monitor/health_report.nim new file mode 100644 index 000000000..d6c23cd28 --- /dev/null +++ b/waku/node/health_monitor/health_report.nim @@ -0,0 +1,10 @@ +{.push raises: [].} + +import ./health_status, ./connection_status, ./protocol_health + +type HealthReport* = object + ## Rest API type returned for /health endpoint + ## + nodeHealth*: HealthStatus # legacy "READY" health indicator + connectionStatus*: ConnectionStatus # new "Connected" health indicator + protocolsHealth*: seq[ProtocolHealth] diff --git a/waku/node/health_monitor/health_status.nim b/waku/node/health_monitor/health_status.nim index 4dd2bdd9a..91663a507 100644 --- a/waku/node/health_monitor/health_status.nim +++ b/waku/node/health_monitor/health_status.nim @@ -7,6 +7,7 @@ type HealthStatus* {.pure.} = enum NOT_READY NOT_MOUNTED SHUTTING_DOWN + EVENT_LOOP_LAGGING proc init*(t: typedesc[HealthStatus], strRep: string): Result[HealthStatus, string] = try: diff --git a/waku/node/health_monitor/node_health_monitor.nim b/waku/node/health_monitor/node_health_monitor.nim index a98e6577a..c652f7cea 100644 --- a/waku/node/health_monitor/node_health_monitor.nim +++ b/waku/node/health_monitor/node_health_monitor.nim @@ -1,65 +1,93 @@ {.push raises: [].} import - std/[options, sets, random, sequtils], + std/[options, sets, random, sequtils, json, strutils, tables], chronos, chronicles, - libp2p/protocols/rendezvous - -import - ../waku_node, - ../api, - ../../waku_rln_relay, - ../../waku_relay, - ../peer_manager, - ./online_monitor, - ./health_status, - ./protocol_health + libp2p/protocols/rendezvous, + libp2p/protocols/pubsub, + libp2p/protocols/pubsub/rpc/messages, + waku/[ + waku_relay, + waku_rln_relay, + api/types, + events/health_events, + events/peer_events, + node/waku_node, + node/peer_manager, + node/kernel_api, + node/health_monitor/online_monitor, + node/health_monitor/health_status, + node/health_monitor/health_report, + node/health_monitor/connection_status, + node/health_monitor/protocol_health, + node/health_monitor/event_loop_monitor, + requests/health_requests, + ] ## This module is aimed to check the state of the "self" Waku Node # randomize initializes sdt/random's random number generator # if not called, the outcome of randomization procedures will be the same in every run -randomize() +random.randomize() -type - HealthReport* = object - nodeHealth*: HealthStatus - protocolsHealth*: seq[ProtocolHealth] +type NodeHealthMonitor* = ref object + nodeHealth: HealthStatus + node: WakuNode + onlineMonitor*: OnlineMonitor + keepAliveFut: Future[void] + healthLoopFut: Future[void] + eventLoopMonitorFut: Future[void] + healthUpdateEvent: AsyncEvent + connectionStatus: ConnectionStatus + onConnectionStatusChange*: ConnectionStatusChangeHandler + cachedProtocols: seq[ProtocolHealth] + ## state of each protocol to report. + ## calculated on last event that can change any protocol's state so fetching a report is fast. + strength: Table[WakuProtocol, int] + ## latest known connectivity strength (e.g. connected peer count) metric for each protocol. + ## if it doesn't make sense for the protocol in question, this is set to zero. + relayObserver: PubSubObserver + peerEventListener: WakuPeerEventListener + shardHealthListener: EventShardTopicHealthChangeListener + eventLoopLagExceeded: bool + ## set to true when the chronos event loop lag exceeds the severe threshold, + ## causing the node health to be reported as EVENT_LOOP_LAGGING until lag recovers. - NodeHealthMonitor* = ref object - nodeHealth: HealthStatus - node: WakuNode - onlineMonitor*: OnlineMonitor - keepAliveFut: Future[void] +func getHealth*(report: HealthReport, kind: WakuProtocol): ProtocolHealth = + for h in report.protocolsHealth: + if h.protocol == $kind: + return h + # Shouldn't happen, but if it does, then assume protocol is not mounted + return ProtocolHealth.init(kind) -template checkWakuNodeNotNil(node: WakuNode, p: ProtocolHealth): untyped = - if node.isNil(): - warn "WakuNode is not set, cannot check health", protocol_health_instance = $p - return p.notMounted() +proc countCapablePeers(hm: NodeHealthMonitor, codec: string): int = + if isNil(hm.node.peerManager): + return 0 + + return hm.node.peerManager.getCapablePeersCount(codec) proc getRelayHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Relay") - checkWakuNodeNotNil(hm.node, p) + var p = ProtocolHealth.init(WakuProtocol.RelayProtocol) - if hm.node.wakuRelay == nil: + if isNil(hm.node.wakuRelay): + hm.strength[WakuProtocol.RelayProtocol] = 0 return p.notMounted() let relayPeers = hm.node.wakuRelay.getConnectedPubSubPeers(pubsubTopic = "").valueOr: + hm.strength[WakuProtocol.RelayProtocol] = 0 return p.notMounted() - if relayPeers.len() == 0: + let count = relayPeers.len + hm.strength[WakuProtocol.RelayProtocol] = count + if count == 0: return p.notReady("No connected peers") return p.ready() proc getRlnRelayHealth(hm: NodeHealthMonitor): Future[ProtocolHealth] {.async.} = - var p = ProtocolHealth.init("Rln Relay") - if hm.node.isNil(): - warn "WakuNode is not set, cannot check health", protocol_health_instance = $p - return p.notMounted() - - if hm.node.wakuRlnRelay.isNil(): + var p = ProtocolHealth.init(WakuProtocol.RlnRelayProtocol) + if isNil(hm.node.wakuRlnRelay): return p.notMounted() const FutIsReadyTimout = 5.seconds @@ -82,173 +110,427 @@ proc getRlnRelayHealth(hm: NodeHealthMonitor): Future[ProtocolHealth] {.async.} proc getLightpushHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = - var p = ProtocolHealth.init("Lightpush") - checkWakuNodeNotNil(hm.node, p) + var p = ProtocolHealth.init(WakuProtocol.LightpushProtocol) - if hm.node.wakuLightPush == nil: + if isNil(hm.node.wakuLightPush): + hm.strength[WakuProtocol.LightpushProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuLightPushCodec) + hm.strength[WakuProtocol.LightpushProtocol] = peerCount + if relayHealth == HealthStatus.READY: return p.ready() return p.notReady("Node has no relay peers to fullfill push requests") -proc getLightpushClientHealth( - hm: NodeHealthMonitor, relayHealth: HealthStatus -): ProtocolHealth = - var p = ProtocolHealth.init("Lightpush Client") - checkWakuNodeNotNil(hm.node, p) - - if hm.node.wakuLightpushClient == nil: - return p.notMounted() - - let selfServiceAvailable = - hm.node.wakuLightPush != nil and relayHealth == HealthStatus.READY - let servicePeerAvailable = hm.node.peerManager.selectPeer(WakuLightPushCodec).isSome() - - if selfServiceAvailable or servicePeerAvailable: - return p.ready() - - return p.notReady("No Lightpush service peer available yet") - proc getLegacyLightpushHealth( hm: NodeHealthMonitor, relayHealth: HealthStatus ): ProtocolHealth = - var p = ProtocolHealth.init("Legacy Lightpush") - checkWakuNodeNotNil(hm.node, p) + var p = ProtocolHealth.init(WakuProtocol.LegacyLightpushProtocol) - if hm.node.wakuLegacyLightPush == nil: + if isNil(hm.node.wakuLegacyLightPush): + hm.strength[WakuProtocol.LegacyLightpushProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuLegacyLightPushCodec) + hm.strength[WakuProtocol.LegacyLightpushProtocol] = peerCount + if relayHealth == HealthStatus.READY: return p.ready() return p.notReady("Node has no relay peers to fullfill push requests") -proc getLegacyLightpushClientHealth( - hm: NodeHealthMonitor, relayHealth: HealthStatus -): ProtocolHealth = - var p = ProtocolHealth.init("Legacy Lightpush Client") - checkWakuNodeNotNil(hm.node, p) - - if hm.node.wakuLegacyLightpushClient == nil: - return p.notMounted() - - if (hm.node.wakuLegacyLightPush != nil and relayHealth == HealthStatus.READY) or - hm.node.peerManager.selectPeer(WakuLegacyLightPushCodec).isSome(): - return p.ready() - - return p.notReady("No Lightpush service peer available yet") - proc getFilterHealth(hm: NodeHealthMonitor, relayHealth: HealthStatus): ProtocolHealth = - var p = ProtocolHealth.init("Filter") - checkWakuNodeNotNil(hm.node, p) + var p = ProtocolHealth.init(WakuProtocol.FilterProtocol) - if hm.node.wakuFilter == nil: + if isNil(hm.node.wakuFilter): + hm.strength[WakuProtocol.FilterProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuFilterSubscribeCodec) + hm.strength[WakuProtocol.FilterProtocol] = peerCount + if relayHealth == HealthStatus.READY: return p.ready() return p.notReady("Relay is not ready, filter will not be able to sort out messages") -proc getFilterClientHealth( - hm: NodeHealthMonitor, relayHealth: HealthStatus -): ProtocolHealth = - var p = ProtocolHealth.init("Filter Client") - checkWakuNodeNotNil(hm.node, p) - - if hm.node.wakuFilterClient == nil: - return p.notMounted() - - if hm.node.peerManager.selectPeer(WakuFilterSubscribeCodec).isSome(): - return p.ready() - - return p.notReady("No Filter service peer available yet") - proc getStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Store") - checkWakuNodeNotNil(hm.node, p) + var p = ProtocolHealth.init(WakuProtocol.StoreProtocol) - if hm.node.wakuStore == nil: + if isNil(hm.node.wakuStore): + hm.strength[WakuProtocol.StoreProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuStoreCodec) + hm.strength[WakuProtocol.StoreProtocol] = peerCount return p.ready() -proc getStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Store Client") - checkWakuNodeNotNil(hm.node, p) +proc getLightpushClientHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.LightpushClientProtocol) - if hm.node.wakuStoreClient == nil: + if isNil(hm.node.wakuLightpushClient): + hm.strength[WakuProtocol.LightpushClientProtocol] = 0 return p.notMounted() - if hm.node.peerManager.selectPeer(WakuStoreCodec).isSome() or hm.node.wakuStore != nil: + let peerCount = countCapablePeers(hm, WakuLightPushCodec) + hm.strength[WakuProtocol.LightpushClientProtocol] = peerCount + + if peerCount > 0: + return p.ready() + return p.notReady("No Lightpush service peer available yet") + +proc getLegacyLightpushClientHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.LegacyLightpushClientProtocol) + + if isNil(hm.node.wakuLegacyLightpushClient): + hm.strength[WakuProtocol.LegacyLightpushClientProtocol] = 0 + return p.notMounted() + + let peerCount = countCapablePeers(hm, WakuLegacyLightPushCodec) + hm.strength[WakuProtocol.LegacyLightpushClientProtocol] = peerCount + + if peerCount > 0: + return p.ready() + return p.notReady("No Lightpush service peer available yet") + +proc getFilterClientHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.FilterClientProtocol) + + if isNil(hm.node.wakuFilterClient): + hm.strength[WakuProtocol.FilterClientProtocol] = 0 + return p.notMounted() + + if isNil(hm.node.wakuRelay): + let edgeRes = RequestEdgeFilterPeerCount.request(hm.node.brokerCtx) + if edgeRes.isOk(): + let peerCount = edgeRes.get().peerCount + if peerCount > 0: + hm.strength[WakuProtocol.FilterClientProtocol] = peerCount + return p.ready() + else: + error "Failed to request edge filter peer count", error = edgeRes.error + return p.notReady("Failed to request edge filter peer count: " & edgeRes.error) + + let peerCount = countCapablePeers(hm, WakuFilterSubscribeCodec) + hm.strength[WakuProtocol.FilterClientProtocol] = peerCount + + if peerCount > 0: + return p.ready() + return p.notReady("No Filter service peer available yet") + +proc getStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = + var p = ProtocolHealth.init(WakuProtocol.StoreClientProtocol) + + if isNil(hm.node.wakuStoreClient): + hm.strength[WakuProtocol.StoreClientProtocol] = 0 + return p.notMounted() + + let peerCount = countCapablePeers(hm, WakuStoreCodec) + hm.strength[WakuProtocol.StoreClientProtocol] = peerCount + + if peerCount > 0 or not isNil(hm.node.wakuStore): return p.ready() return p.notReady( "No Store service peer available yet, neither Store service set up for the node" ) -proc getLegacyStoreHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Legacy Store") - checkWakuNodeNotNil(hm.node, p) - - if hm.node.wakuLegacyStore == nil: - return p.notMounted() - - return p.ready() - -proc getLegacyStoreClientHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Legacy Store Client") - checkWakuNodeNotNil(hm.node, p) - - if hm.node.wakuLegacyStoreClient == nil: - return p.notMounted() - - if hm.node.peerManager.selectPeer(WakuLegacyStoreCodec).isSome() or - hm.node.wakuLegacyStore != nil: - return p.ready() - - return p.notReady( - "No Legacy Store service peers are available yet, neither Store service set up for the node" - ) - proc getPeerExchangeHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Peer Exchange") - checkWakuNodeNotNil(hm.node, p) + var p = ProtocolHealth.init(WakuProtocol.PeerExchangeProtocol) - if hm.node.wakuPeerExchange == nil: + if isNil(hm.node.wakuPeerExchange): + hm.strength[WakuProtocol.PeerExchangeProtocol] = 0 return p.notMounted() + let peerCount = countCapablePeers(hm, WakuPeerExchangeCodec) + hm.strength[WakuProtocol.PeerExchangeProtocol] = peerCount + return p.ready() proc getRendezvousHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Rendezvous") - checkWakuNodeNotNil(hm.node, p) + var p = ProtocolHealth.init(WakuProtocol.RendezvousProtocol) - if hm.node.wakuRendezvous == nil: + if isNil(hm.node.wakuRendezvous): + hm.strength[WakuProtocol.RendezvousProtocol] = 0 return p.notMounted() - if hm.node.peerManager.switch.peerStore.peers(RendezVousCodec).len() == 0: + let peerCount = countCapablePeers(hm, RendezVousCodec) + hm.strength[WakuProtocol.RendezvousProtocol] = peerCount + if peerCount == 0: return p.notReady("No Rendezvous peers are available yet") return p.ready() proc getMixHealth(hm: NodeHealthMonitor): ProtocolHealth = - var p = ProtocolHealth.init("Mix") - checkWakuNodeNotNil(hm.node, p) + var p = ProtocolHealth.init(WakuProtocol.MixProtocol) - if hm.node.wakuMix.isNil(): + if isNil(hm.node.wakuMix): return p.notMounted() return p.ready() +proc getSyncProtocolHealthInfo*( + hm: NodeHealthMonitor, protocol: WakuProtocol +): ProtocolHealth = + ## Get ProtocolHealth for a given protocol that can provide it synchronously + ## + case protocol + of WakuProtocol.RelayProtocol: + return hm.getRelayHealth() + of WakuProtocol.StoreProtocol: + return hm.getStoreHealth() + of WakuProtocol.FilterProtocol: + return hm.getFilterHealth(hm.getRelayHealth().health) + of WakuProtocol.LightpushProtocol: + return hm.getLightpushHealth(hm.getRelayHealth().health) + of WakuProtocol.LegacyLightpushProtocol: + return hm.getLegacyLightpushHealth(hm.getRelayHealth().health) + of WakuProtocol.PeerExchangeProtocol: + return hm.getPeerExchangeHealth() + of WakuProtocol.RendezvousProtocol: + return hm.getRendezvousHealth() + of WakuProtocol.MixProtocol: + return hm.getMixHealth() + of WakuProtocol.StoreClientProtocol: + return hm.getStoreClientHealth() + of WakuProtocol.FilterClientProtocol: + return hm.getFilterClientHealth() + of WakuProtocol.LightpushClientProtocol: + return hm.getLightpushClientHealth() + of WakuProtocol.LegacyLightpushClientProtocol: + return hm.getLegacyLightpushClientHealth() + of WakuProtocol.RlnRelayProtocol: + # Could waitFor here but we don't want to block the main thread. + # Could also return a cached value from a previous check. + var p = ProtocolHealth.init(protocol) + return p.notReady("RLN Relay health check is async") + else: + var p = ProtocolHealth.init(protocol) + return p.notMounted() + +proc getProtocolHealthInfo*( + hm: NodeHealthMonitor, protocol: WakuProtocol +): Future[ProtocolHealth] {.async.} = + ## Get ProtocolHealth for a given protocol + ## + case protocol + of WakuProtocol.RlnRelayProtocol: + return await hm.getRlnRelayHealth() + else: + return hm.getSyncProtocolHealthInfo(protocol) + +proc getSyncAllProtocolHealthInfo(hm: NodeHealthMonitor): seq[ProtocolHealth] = + ## Get ProtocolHealth for the subset of protocols that can provide it synchronously + ## + var protocols: seq[ProtocolHealth] = @[] + let relayHealth = hm.getRelayHealth() + protocols.add(relayHealth) + + protocols.add(hm.getLightpushHealth(relayHealth.health)) + protocols.add(hm.getLegacyLightpushHealth(relayHealth.health)) + protocols.add(hm.getFilterHealth(relayHealth.health)) + protocols.add(hm.getStoreHealth()) + protocols.add(hm.getPeerExchangeHealth()) + protocols.add(hm.getRendezvousHealth()) + protocols.add(hm.getMixHealth()) + + protocols.add(hm.getLightpushClientHealth()) + protocols.add(hm.getLegacyLightpushClientHealth()) + protocols.add(hm.getStoreClientHealth()) + protocols.add(hm.getFilterClientHealth()) + return protocols + +proc getAllProtocolHealthInfo( + hm: NodeHealthMonitor +): Future[seq[ProtocolHealth]] {.async.} = + ## Get ProtocolHealth for all protocols + ## + var protocols = hm.getSyncAllProtocolHealthInfo() + + let rlnHealth = await hm.getRlnRelayHealth() + protocols.add(rlnHealth) + + return protocols + +proc calculateConnectionState*( + protocols: seq[ProtocolHealth], + strength: Table[WakuProtocol, int], ## latest connectivity strength (e.g. peer count) for a protocol + dLowOpt: Option[int], ## minimum relay peers for Connected status if in Core (Relay) mode +): ConnectionStatus = + var + relayCount = 0 + lightpushCount = 0 + filterCount = 0 + storeClientCount = 0 + + for p in protocols: + let kind = + try: + parseEnum[WakuProtocol](p.protocol) + except ValueError: + continue + + if p.health != HealthStatus.READY: + continue + + let strength = strength.getOrDefault(kind, 0) + + if kind in RelayProtocols: + relayCount = max(relayCount, strength) + elif kind in StoreClientProtocols: + storeClientCount = max(storeClientCount, strength) + elif kind in LightpushClientProtocols: + lightpushCount = max(lightpushCount, strength) + elif kind in FilterClientProtocols: + filterCount = max(filterCount, strength) + + debug "calculateConnectionState", + relayCount, storeClientCount, lightpushCount, filterCount + + # Relay connectivity should be a sufficient check in Core mode. + # "Store peers" are relay peers because incoming messages in + # the relay are input to the store server. + # But if Store server (or client, even) is not mounted as well, this logic assumes + # the user knows what they're doing. + + if dLowOpt.isSome(): + if relayCount >= dLowOpt.get(): + return ConnectionStatus.Connected + + if relayCount > 0: + return ConnectionStatus.PartiallyConnected + + # No relay connectivity. Relay might not be mounted, or may just have zero peers. + # Fall back to Edge check in any case to be sure. + + let canSend = lightpushCount > 0 + let canReceive = filterCount > 0 + let canStore = storeClientCount > 0 + + let meetsMinimum = canSend and canReceive and canStore + + if not meetsMinimum: + return ConnectionStatus.Disconnected + + let isEdgeRobust = + (lightpushCount >= HealthyThreshold) and (filterCount >= HealthyThreshold) and + (storeClientCount >= HealthyThreshold) + + if isEdgeRobust: + return ConnectionStatus.Connected + + return ConnectionStatus.PartiallyConnected + +proc calculateConnectionState*(hm: NodeHealthMonitor): ConnectionStatus = + let dLow = + if isNil(hm.node.wakuRelay): + none(int) + else: + some(hm.node.wakuRelay.parameters.dLow) + return calculateConnectionState(hm.cachedProtocols, hm.strength, dLow) + +proc getNodeHealthReport*(hm: NodeHealthMonitor): Future[HealthReport] {.async.} = + ## Get a HealthReport that includes all protocols + ## + var report: HealthReport + + if hm.nodeHealth == HealthStatus.INITIALIZING or + hm.nodeHealth == HealthStatus.SHUTTING_DOWN: + report.nodeHealth = hm.nodeHealth + report.connectionStatus = ConnectionStatus.Disconnected + return report + + if hm.cachedProtocols.len == 0: + hm.cachedProtocols = await hm.getAllProtocolHealthInfo() + hm.connectionStatus = hm.calculateConnectionState() + + report.nodeHealth = + if hm.eventLoopLagExceeded: HealthStatus.EVENT_LOOP_LAGGING else: HealthStatus.READY + report.connectionStatus = hm.connectionStatus + report.protocolsHealth = hm.cachedProtocols + return report + +proc getSyncNodeHealthReport*(hm: NodeHealthMonitor): HealthReport = + ## Get a HealthReport that includes the subset of protocols that inform health synchronously + ## + var report: HealthReport + + if hm.nodeHealth == HealthStatus.INITIALIZING or + hm.nodeHealth == HealthStatus.SHUTTING_DOWN: + report.nodeHealth = hm.nodeHealth + report.connectionStatus = ConnectionStatus.Disconnected + return report + + if hm.cachedProtocols.len == 0: + hm.cachedProtocols = hm.getSyncAllProtocolHealthInfo() + hm.connectionStatus = hm.calculateConnectionState() + + report.nodeHealth = + if hm.eventLoopLagExceeded: HealthStatus.EVENT_LOOP_LAGGING else: HealthStatus.READY + report.connectionStatus = hm.connectionStatus + report.protocolsHealth = hm.cachedProtocols + return report + +proc onRelayMsg( + hm: NodeHealthMonitor, peer: PubSubPeer, msg: var RPCMsg +) {.gcsafe, raises: [].} = + ## Inspect Relay events for health-update relevance in Core (Relay) mode. + ## + ## For Core (Relay) mode, the connectivity health state is mostly determined + ## by the relay protocol state (it is the dominant factor), and we know + ## that a peer Relay can only affect this Relay's health if there is a + ## subscription change or a mesh (GRAFT/PRUNE) change. + ## + + if msg.subscriptions.len == 0: + if msg.control.isNone(): + return + let ctrl = msg.control.get() + if ctrl.graft.len == 0 and ctrl.prune.len == 0: + return + + hm.healthUpdateEvent.fire() + +proc healthLoop(hm: NodeHealthMonitor) {.async.} = + ## Re-evaluate the global health state of the node when notified of a potential change, + ## and call back the application if an actual change from the last notified state happened. + info "Health monitor loop start" + while true: + try: + await hm.healthUpdateEvent.wait() + hm.healthUpdateEvent.clear() + + hm.cachedProtocols = await hm.getAllProtocolHealthInfo() + let newConnectionStatus = hm.calculateConnectionState() + + if newConnectionStatus != hm.connectionStatus: + debug "connectionStatus change", + oldstatus = hm.connectionStatus, newstatus = newConnectionStatus + + hm.connectionStatus = newConnectionStatus + + EventConnectionStatusChange.emit(hm.node.brokerCtx, newConnectionStatus) + + if not isNil(hm.onConnectionStatusChange): + await hm.onConnectionStatusChange(newConnectionStatus) + except CancelledError: + break + except Exception as e: + error "HealthMonitor: error in update loop", error = e.msg + + # safety cooldown to protect from edge cases + await sleepAsync(100.milliseconds) + + info "Health monitor loop end" + proc selectRandomPeersForKeepalive( node: WakuNode, outPeers: seq[PeerId], numRandomPeers: int ): Future[seq[PeerId]] {.async.} = ## Select peers for random keepalive, prioritizing mesh peers - if node.wakuRelay.isNil(): + if isNil(node.wakuRelay): return selectRandomPeers(outPeers, numRandomPeers) let meshPeers = node.wakuRelay.getPeersInMesh().valueOr: @@ -382,55 +664,87 @@ proc startKeepalive*( hm.keepAliveFut = hm.node.keepAliveLoop(randomPeersKeepalive, allPeersKeepalive) return ok() -proc getNodeHealthReport*(hm: NodeHealthMonitor): Future[HealthReport] {.async.} = - var report: HealthReport - report.nodeHealth = hm.nodeHealth - - if not hm.node.isNil(): - let relayHealth = hm.getRelayHealth() - report.protocolsHealth.add(relayHealth) - report.protocolsHealth.add(await hm.getRlnRelayHealth()) - report.protocolsHealth.add(hm.getLightpushHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getLegacyLightpushHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getFilterHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getStoreHealth()) - report.protocolsHealth.add(hm.getLegacyStoreHealth()) - report.protocolsHealth.add(hm.getPeerExchangeHealth()) - report.protocolsHealth.add(hm.getRendezvousHealth()) - report.protocolsHealth.add(hm.getMixHealth()) - - report.protocolsHealth.add(hm.getLightpushClientHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getLegacyLightpushClientHealth(relayHealth.health)) - report.protocolsHealth.add(hm.getStoreClientHealth()) - report.protocolsHealth.add(hm.getLegacyStoreClientHealth()) - report.protocolsHealth.add(hm.getFilterClientHealth(relayHealth.health)) - return report - -proc setNodeToHealthMonitor*(hm: NodeHealthMonitor, node: WakuNode) = - hm.node = node - proc setOverallHealth*(hm: NodeHealthMonitor, health: HealthStatus) = hm.nodeHealth = health proc startHealthMonitor*(hm: NodeHealthMonitor): Result[void, string] = hm.onlineMonitor.startOnlineMonitor() + + if isNil(hm.node.peerManager): + return err("startHealthMonitor: no node peerManager to monitor") + + if not isNil(hm.node.wakuRelay): + hm.relayObserver = PubSubObserver( + onRecv: proc(peer: PubSubPeer, msgs: var RPCMsg) {.gcsafe, raises: [].} = + hm.onRelayMsg(peer, msgs) + ) + hm.node.wakuRelay.addObserver(hm.relayObserver) + + hm.peerEventListener = WakuPeerEvent.listen( + hm.node.brokerCtx, + proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} = + ## Recompute health on any peer changing anything (join, leave, identify, metadata update) + hm.healthUpdateEvent.fire(), + ).valueOr: + return err("Failed to subscribe to peer events: " & error) + + hm.shardHealthListener = EventShardTopicHealthChange.listen( + hm.node.brokerCtx, + proc( + evt: EventShardTopicHealthChange + ): Future[void] {.async: (raises: []), gcsafe.} = + hm.healthUpdateEvent.fire(), + ).valueOr: + return err("Failed to subscribe to shard health events: " & error) + + hm.healthUpdateEvent = newAsyncEvent() + hm.healthUpdateEvent.fire() + + hm.healthLoopFut = hm.healthLoop() + hm.eventLoopMonitorFut = eventLoopMonitorLoop( + proc(lagTooHigh: bool) {.gcsafe, raises: [].} = + hm.eventLoopLagExceeded = lagTooHigh + hm.healthUpdateEvent.fire() + ) + hm.startKeepalive().isOkOr: return err("startHealthMonitor: failed starting keep alive: " & error) + return ok() proc stopHealthMonitor*(hm: NodeHealthMonitor) {.async.} = - if not hm.onlineMonitor.isNil(): + if not isNil(hm.onlineMonitor): await hm.onlineMonitor.stopOnlineMonitor() - if not hm.keepAliveFut.isNil(): + if not isNil(hm.keepAliveFut): await hm.keepAliveFut.cancelAndWait() + if not isNil(hm.healthLoopFut): + await hm.healthLoopFut.cancelAndWait() + + if not isNil(hm.eventLoopMonitorFut): + await hm.eventLoopMonitorFut.cancelAndWait() + + await WakuPeerEvent.dropListener(hm.node.brokerCtx, hm.peerEventListener) + await EventShardTopicHealthChange.dropListener( + hm.node.brokerCtx, hm.shardHealthListener + ) + + if not isNil(hm.node.wakuRelay) and not isNil(hm.relayObserver): + hm.node.wakuRelay.removeObserver(hm.relayObserver) + proc new*( T: type NodeHealthMonitor, + node: WakuNode, dnsNameServers = @[parseIpAddress("1.1.1.1"), parseIpAddress("1.0.0.1")], ): T = + let om = OnlineMonitor.init(dnsNameServers) + om.setPeerStoreToOnlineMonitor(node.switch.peerStore) + om.addOnlineStateObserver(node.peerManager.getOnlineStateObserver()) T( nodeHealth: INITIALIZING, - node: nil, - onlineMonitor: OnlineMonitor.init(dnsNameServers), + node: node, + onlineMonitor: om, + connectionStatus: ConnectionStatus.Disconnected, + strength: initTable[WakuProtocol, int](), ) diff --git a/waku/node/health_monitor/protocol_health.nim b/waku/node/health_monitor/protocol_health.nim index 7bacea94b..4479888c8 100644 --- a/waku/node/health_monitor/protocol_health.nim +++ b/waku/node/health_monitor/protocol_health.nim @@ -1,5 +1,8 @@ import std/[options, strformat] import ./health_status +import waku/common/waku_protocol + +export waku_protocol type ProtocolHealth* = object protocol*: string @@ -39,8 +42,7 @@ proc shuttingDown*(p: var ProtocolHealth): ProtocolHealth = proc `$`*(p: ProtocolHealth): string = return fmt"protocol: {p.protocol}, health: {p.health}, description: {p.desc}" -proc init*(p: typedesc[ProtocolHealth], protocol: string): ProtocolHealth = - let p = ProtocolHealth( - protocol: protocol, health: HealthStatus.NOT_MOUNTED, desc: none[string]() +proc init*(p: typedesc[ProtocolHealth], protocol: WakuProtocol): ProtocolHealth = + return ProtocolHealth( + protocol: $protocol, health: HealthStatus.NOT_MOUNTED, desc: none[string]() ) - return p diff --git a/waku/waku_relay/topic_health.nim b/waku/node/health_monitor/topic_health.nim similarity index 84% rename from waku/waku_relay/topic_health.nim rename to waku/node/health_monitor/topic_health.nim index 774abc584..5a1ea0a16 100644 --- a/waku/waku_relay/topic_health.nim +++ b/waku/node/health_monitor/topic_health.nim @@ -1,11 +1,12 @@ import chronos -import ../waku_core +import waku/waku_core type TopicHealth* = enum UNHEALTHY MINIMALLY_HEALTHY SUFFICIENTLY_HEALTHY + NOT_SUBSCRIBED proc `$`*(t: TopicHealth): string = result = @@ -13,6 +14,7 @@ proc `$`*(t: TopicHealth): string = of UNHEALTHY: "UnHealthy" of MINIMALLY_HEALTHY: "MinimallyHealthy" of SUFFICIENTLY_HEALTHY: "SufficientlyHealthy" + of NOT_SUBSCRIBED: "NotSubscribed" type TopicHealthChangeHandler* = proc( pubsubTopic: PubsubTopic, topicHealth: TopicHealth diff --git a/waku/node/kernel_api.nim b/waku/node/kernel_api.nim new file mode 100644 index 000000000..9d19acb07 --- /dev/null +++ b/waku/node/kernel_api.nim @@ -0,0 +1,9 @@ +import + ./kernel_api/filter as filter_api, + ./kernel_api/lightpush as lightpush_api, + ./kernel_api/store as store_api, + ./kernel_api/relay as relay_api, + ./kernel_api/peer_exchange as peer_exchange_api, + ./kernel_api/ping as ping_api + +export filter_api, lightpush_api, store_api, relay_api, peer_exchange_api, ping_api diff --git a/waku/node/api/filter.nim b/waku/node/kernel_api/filter.nim similarity index 90% rename from waku/node/api/filter.nim rename to waku/node/kernel_api/filter.nim index 242640a44..948035f14 100644 --- a/waku/node/api/filter.nim +++ b/waku/node/kernel_api/filter.nim @@ -108,13 +108,10 @@ proc filterSubscribe*( error = "waku filter client is not set up" return err(FilterSubscribeError.serviceUnavailable()) - let remotePeerRes = parsePeerInfo(peer) - if remotePeerRes.isErr(): - error "Couldn't parse the peer info properly", error = remotePeerRes.error + let remotePeer = parsePeerInfo(peer).valueOr: + error "Couldn't parse the peer info properly", error = error return err(FilterSubscribeError.serviceUnavailable("No peers available")) - let remotePeer = remotePeerRes.value - if pubsubTopic.isSome(): info "registering filter subscription to content", pubsubTopic = pubsubTopic.get(), @@ -143,15 +140,11 @@ proc filterSubscribe*( else: # No pubsub topic, autosharding is used to deduce it # but content topics must be well-formed for this - let topicMapRes = - node.wakuAutoSharding.get().getShardsFromContentTopics(contentTopics) - - let topicMap = - if topicMapRes.isErr(): - error "can't get shard", error = topicMapRes.error + let topicMap = node.wakuAutoSharding + .get() + .getShardsFromContentTopics(contentTopics).valueOr: + error "can't get shard", error = error return err(FilterSubscribeError.badResponse("can't get shard")) - else: - topicMapRes.get() var futures = collect(newSeq): for shard, topics in topicMap.pairs: @@ -195,13 +188,10 @@ proc filterUnsubscribe*( ): Future[FilterSubscribeResult] {.async: (raises: []).} = ## Unsubscribe from a content filter V2". - let remotePeerRes = parsePeerInfo(peer) - if remotePeerRes.isErr(): - error "couldn't parse remotePeerInfo", error = remotePeerRes.error + let remotePeer = parsePeerInfo(peer).valueOr: + error "couldn't parse remotePeerInfo", error = error return err(FilterSubscribeError.serviceUnavailable("No peers available")) - let remotePeer = remotePeerRes.value - if pubsubTopic.isSome(): info "deregistering filter subscription to content", pubsubTopic = pubsubTopic.get(), @@ -226,15 +216,11 @@ proc filterUnsubscribe*( error "Failed filter un-subscription, pubsub topic must be specified with static sharding" waku_node_errors.inc(labelValues = ["unsubscribe_filter_failure"]) else: # pubsubTopic.isNone - let topicMapRes = - node.wakuAutoSharding.get().getShardsFromContentTopics(contentTopics) - - let topicMap = - if topicMapRes.isErr(): - error "can't get shard", error = topicMapRes.error + let topicMap = node.wakuAutoSharding + .get() + .getShardsFromContentTopics(contentTopics).valueOr: + error "can't get shard", error = error return err(FilterSubscribeError.badResponse("can't get shard")) - else: - topicMapRes.get() var futures = collect(newSeq): for shard, topics in topicMap.pairs: @@ -275,13 +261,10 @@ proc filterUnsubscribeAll*( ): Future[FilterSubscribeResult] {.async: (raises: []).} = ## Unsubscribe from a content filter V2". - let remotePeerRes = parsePeerInfo(peer) - if remotePeerRes.isErr(): - error "couldn't parse remotePeerInfo", error = remotePeerRes.error + let remotePeer = parsePeerInfo(peer).valueOr: + error "couldn't parse remotePeerInfo", error = error return err(FilterSubscribeError.serviceUnavailable("No peers available")) - let remotePeer = remotePeerRes.value - info "deregistering all filter subscription to content", peer = remotePeer.peerId let unsubRes = await node.wakuFilterClient.unsubscribeAll(remotePeer) diff --git a/waku/node/api/lightpush.nim b/waku/node/kernel_api/lightpush.nim similarity index 80% rename from waku/node/api/lightpush.nim rename to waku/node/kernel_api/lightpush.nim index 550c5bd9f..ffe2afdac 100644 --- a/waku/node/api/lightpush.nim +++ b/waku/node/kernel_api/lightpush.nim @@ -34,26 +34,27 @@ import logScope: topics = "waku node lightpush api" +const MountWithoutRelayError* = "cannot mount lightpush because relay is not mounted" + ## Waku lightpush proc mountLegacyLightPush*( node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit -) {.async.} = +): Future[Result[void, string]] {.async.} = info "mounting legacy light push" - let pushHandler = - if node.wakuRelay.isNil: - info "mounting legacy lightpush without relay (nil)" - legacy_lightpush_protocol.getNilPushHandler() + if node.wakuRelay.isNil(): + return err(MountWithoutRelayError) + + info "mounting legacy lightpush with relay" + let rlnPeer = + if node.wakuRlnRelay.isNil(): + info "mounting legacy lightpush without rln-relay" + none(WakuRLNRelay) else: - info "mounting legacy lightpush with relay" - let rlnPeer = - if isNil(node.wakuRlnRelay): - info "mounting legacy lightpush without rln-relay" - none(WakuRLNRelay) - else: - info "mounting legacy lightpush with rln-relay" - some(node.wakuRlnRelay) - legacy_lightpush_protocol.getRelayPushHandler(node.wakuRelay, rlnPeer) + info "mounting legacy lightpush with rln-relay" + some(node.wakuRlnRelay) + let pushHandler = + legacy_lightpush_protocol.getRelayPushHandler(node.wakuRelay, rlnPeer) node.wakuLegacyLightPush = WakuLegacyLightPush.new(node.peerManager, node.rng, pushHandler, some(rateLimit)) @@ -64,6 +65,9 @@ proc mountLegacyLightPush*( node.switch.mount(node.wakuLegacyLightPush, protocolMatcher(WakuLegacyLightPushCodec)) + info "legacy lightpush mounted successfully" + return ok() + proc mountLegacyLightPushClient*(node: WakuNode) = info "mounting legacy light push client" @@ -114,14 +118,8 @@ proc legacyLightpushPublish*( if node.wakuAutoSharding.isNone(): return err("Pubsub topic must be specified when static sharding is enabled") - let topicMapRes = - node.wakuAutoSharding.get().getShardsFromContentTopics(message.contentTopic) - let topicMap = - if topicMapRes.isErr(): - return err(topicMapRes.error) - else: - topicMapRes.get() + ?node.wakuAutoSharding.get().getShardsFromContentTopics(message.contentTopic) for pubsub, _ in topicMap.pairs: # There's only one pair anyway return await internalPublish(node, $pubsub, message, peer) @@ -152,23 +150,21 @@ proc legacyLightpushPublish*( proc mountLightPush*( node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit -) {.async.} = +): Future[Result[void, string]] {.async.} = info "mounting light push" - let pushHandler = - if node.wakuRelay.isNil(): - info "mounting lightpush v2 without relay (nil)" - lightpush_protocol.getNilPushHandler() + if node.wakuRelay.isNil(): + return err(MountWithoutRelayError) + + info "mounting lightpush with relay" + let rlnPeer = + if node.wakuRlnRelay.isNil(): + info "mounting lightpush without rln-relay" + none(WakuRLNRelay) else: - info "mounting lightpush with relay" - let rlnPeer = - if isNil(node.wakuRlnRelay): - info "mounting lightpush without rln-relay" - none(WakuRLNRelay) - else: - info "mounting lightpush with rln-relay" - some(node.wakuRlnRelay) - lightpush_protocol.getRelayPushHandler(node.wakuRelay, rlnPeer) + info "mounting lightpush with rln-relay" + some(node.wakuRlnRelay) + let pushHandler = lightpush_protocol.getRelayPushHandler(node.wakuRelay, rlnPeer) node.wakuLightPush = WakuLightPush.new( node.peerManager, node.rng, pushHandler, node.wakuAutoSharding, some(rateLimit) @@ -180,6 +176,9 @@ proc mountLightPush*( node.switch.mount(node.wakuLightPush, protocolMatcher(WakuLightPushCodec)) + info "lightpush mounted successfully" + return ok() + proc mountLightPushClient*(node: WakuNode) = info "mounting light push client" @@ -194,7 +193,6 @@ proc lightpushPublishHandler( mixify: bool = false, ): Future[lightpush_protocol.WakuLightPushResult] {.async.} = let msgHash = pubsubTopic.computeMessageHash(message).to0xHex() - if not node.wakuLightpushClient.isNil(): notice "publishing message with lightpush", pubsubTopic = pubsubTopic, @@ -202,23 +200,23 @@ proc lightpushPublishHandler( target_peer_id = peer.peerId, msg_hash = msgHash, mixify = mixify - if mixify: #indicates we want to use mix to send the message - #TODO: How to handle multiple addresses? - let conn = node.wakuMix.toConnection( - MixDestination.init(peer.peerId, peer.addrs[0]), - WakuLightPushCodec, - MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), - # indicating we only want a single path to be used for reply hence numSurbs = 1 - ).valueOr: - error "could not create mix connection" - return lighpushErrorResult( - LightPushErrorCode.SERVICE_NOT_AVAILABLE, - "Waku lightpush with mix not available", - ) + if defined(libp2p_mix_experimental_exit_is_dest) and mixify: + #indicates we want to use mix to send the message + when defined(libp2p_mix_experimental_exit_is_dest): + #TODO: How to handle multiple addresses? + let conn = node.wakuMix.toConnection( + MixDestination.exitNode(peer.peerId), + WakuLightPushCodec, + MixParameters(expectReply: Opt.some(true), numSurbs: Opt.some(byte(1))), + # indicating we only want a single path to be used for reply hence numSurbs = 1 + ).valueOr: + error "could not create mix connection" + return lighpushErrorResult( + LightPushErrorCode.SERVICE_NOT_AVAILABLE, + "Waku lightpush with mix not available", + ) - return await node.wakuLightpushClient.publishWithConn( - pubsubTopic, message, conn, peer.peerId - ) + return await node.wakuLightpushClient.publish(some(pubsubTopic), message, conn) else: return await node.wakuLightpushClient.publish(some(pubsubTopic), message, peer) @@ -267,7 +265,7 @@ proc lightpushPublish*( LightPushErrorCode.NO_PEERS_TO_RELAY, "no suitable remote peers" ) - let pubsubForPublish = pubSubTopic.valueOr: + let pubsubForPublish = pubsubTopic.valueOr: if node.wakuAutoSharding.isNone(): let msg = "Pubsub topic must be specified when static sharding is enabled" error "lightpush publish error", error = msg diff --git a/waku/node/api/peer_exchange.nim b/waku/node/kernel_api/peer_exchange.nim similarity index 94% rename from waku/node/api/peer_exchange.nim rename to waku/node/kernel_api/peer_exchange.nim index d2e0f5575..a4bec727b 100644 --- a/waku/node/api/peer_exchange.nim +++ b/waku/node/kernel_api/peer_exchange.nim @@ -111,10 +111,9 @@ proc setPeerExchangePeer*( info "Set peer-exchange peer", peer = peer - let remotePeerRes = parsePeerInfo(peer) - if remotePeerRes.isErr(): - error "could not parse peer info", error = remotePeerRes.error + let remotePeer = parsePeerInfo(peer).valueOr: + error "could not parse peer info", error = error return - node.peerManager.addPeer(remotePeerRes.value, PeerExchange) + node.peerManager.addPeer(remotePeer, PeerExchange) waku_px_peers.inc() diff --git a/waku/node/api/ping.nim b/waku/node/kernel_api/ping.nim similarity index 100% rename from waku/node/api/ping.nim rename to waku/node/kernel_api/ping.nim diff --git a/waku/node/api/relay.nim b/waku/node/kernel_api/relay.nim similarity index 51% rename from waku/node/api/relay.nim rename to waku/node/kernel_api/relay.nim index 1e38c5535..f1b80cf19 100644 --- a/waku/node/api/relay.nim +++ b/waku/node/kernel_api/relay.nim @@ -16,19 +16,24 @@ import libp2p/builders, libp2p/transports/tcptransport, libp2p/transports/wstransport, - libp2p/utility + libp2p/utility, + brokers/broker_context import - ../waku_node, - ../../waku_relay, - ../../waku_core, - ../../waku_core/topics/sharding, - ../../waku_filter_v2, - ../../waku_archive_legacy, - ../../waku_archive, - ../../waku_store_sync, - ../peer_manager, - ../../waku_rln_relay + waku/[ + waku_relay, + waku_core, + waku_core/topics/sharding, + waku_filter_v2, + waku_archive, + waku_store_sync, + waku_rln_relay, + node/waku_node, + node/peer_manager, + events/message_events, + ] + +export waku_relay.WakuRelayHandler declarePublicHistogram waku_histogram_message_size, "message size histogram in kB", @@ -42,14 +47,25 @@ logScope: ## Waku relay proc registerRelayHandler( - node: WakuNode, topic: PubsubTopic, appHandler: WakuRelayHandler -) = + node: WakuNode, topic: PubsubTopic, appHandler: WakuRelayHandler = nil +): bool = ## Registers the only handler for the given topic. ## Notice that this handler internally calls other handlers, such as filter, ## archive, etc, plus the handler provided by the application. + ## Returns `true` if a mesh subscription was created or `false` if the relay + ## was already subscribed to the topic. - if node.wakuRelay.isSubscribed(topic): - return + let alreadySubscribed = node.wakuRelay.isSubscribed(topic) + + if not appHandler.isNil(): + if not alreadySubscribed or not node.legacyAppHandlers.hasKey(topic): + node.legacyAppHandlers[topic] = appHandler + else: + debug "Legacy appHandler already exists for active PubsubTopic, ignoring new handler", + topic = topic + + if alreadySubscribed: + return false proc traceHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = let msgSizeKB = msg.payload.len / 1000 @@ -64,11 +80,6 @@ proc registerRelayHandler( await node.wakuFilter.handleMessage(topic, msg) proc archiveHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = - if not node.wakuLegacyArchive.isNil(): - ## we try to store with legacy archive - await node.wakuLegacyArchive.handleMessage(topic, msg) - return - if node.wakuArchive.isNil(): return @@ -80,6 +91,9 @@ proc registerRelayHandler( node.wakuStoreReconciliation.messageIngress(topic, msg) + proc internalHandler(topic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + MessageSeenEvent.emit(node.brokerCtx, topic, msg) + let uniqueTopicHandler = proc( topic: PubsubTopic, msg: WakuMessage ): Future[void] {.async, gcsafe.} = @@ -87,43 +101,61 @@ proc registerRelayHandler( await filterHandler(topic, msg) await archiveHandler(topic, msg) await syncHandler(topic, msg) - await appHandler(topic, msg) + await internalHandler(topic, msg) + + # Call the legacy (kernel API) app handler if it exists. + # Normally, hasKey is false and the MessageSeenEvent bus (new API) is used instead. + # But we need to support legacy behavior (kernel API use), hence this. + # NOTE: We can delete `legacyAppHandlers` if instead we refactor WakuRelay to support multiple + # PubsubTopic handlers, since that's actually supported by libp2p PubSub (bigger refactor...) + if node.legacyAppHandlers.hasKey(topic) and not node.legacyAppHandlers[topic].isNil(): + await node.legacyAppHandlers[topic](topic, msg) node.wakuRelay.subscribe(topic, uniqueTopicHandler) +proc getTopicOfSubscriptionEvent( + node: WakuNode, subscription: SubscriptionEvent +): Result[(PubsubTopic, Option[ContentTopic]), string] = + case subscription.kind + of ContentSub, ContentUnsub: + if node.wakuAutoSharding.isSome(): + let shard = node.wakuAutoSharding.get().getShard((subscription.topic)).valueOr: + return err("Autosharding error: " & error) + return ok(($shard, some(subscription.topic))) + else: + return + err("Static sharding is used, relay subscriptions must specify a pubsub topic") + of PubsubSub, PubsubUnsub: + return ok((subscription.topic, none[ContentTopic]())) + else: + return err("Unsupported subscription type in relay getTopicOfSubscriptionEvent") + proc subscribe*( node: WakuNode, subscription: SubscriptionEvent, handler: WakuRelayHandler ): Result[void, string] = ## Subscribes to a PubSub or Content topic. Triggers handler when receiving messages on ## this topic. WakuRelayHandler is a method that takes a topic and a Waku message. + ## If `handler` is nil, the API call will subscribe to the topic in the relay mesh + ## but no app handler will be registered at this time (it can be registered later with + ## another call to this proc for the same gossipsub topic). - if node.wakuRelay.isNil(): + if isNil(node.wakuRelay): error "Invalid API call to `subscribe`. WakuRelay not mounted." return err("Invalid API call to `subscribe`. WakuRelay not mounted.") - let (pubsubTopic, contentTopicOp) = - case subscription.kind - of ContentSub: - if node.wakuAutoSharding.isSome(): - let shard = node.wakuAutoSharding.get().getShard((subscription.topic)).valueOr: - error "Autosharding error", error = error - return err("Autosharding error: " & error) - ($shard, some(subscription.topic)) - else: - return err( - "Static sharding is used, relay subscriptions must specify a pubsub topic" - ) - of PubsubSub: - (subscription.topic, none(ContentTopic)) + let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: + error "Failed to decode subscription event", error = error + return err("Failed to decode subscription event: " & error) + + if node.registerRelayHandler(pubsubTopic, handler): + info "subscribe", pubsubTopic, contentTopicOp + node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: pubsubTopic)) + else: + if isNil(handler): + warn "No-effect API call to subscribe. Already subscribed to topic", pubsubTopic else: - return err("Unsupported subscription type in relay subscribe") - - if node.wakuRelay.isSubscribed(pubsubTopic): - warn "No-effect API call to subscribe. Already subscribed to topic", pubsubTopic - return ok() - - node.registerRelayHandler(pubsubTopic, handler) - node.topicSubscriptionQueue.emit((kind: PubsubSub, topic: pubsubTopic)) + info "subscribe (was already subscribed in the mesh; appHandler set)", + pubsubTopic = pubsubTopic return ok() @@ -131,41 +163,50 @@ proc unsubscribe*( node: WakuNode, subscription: SubscriptionEvent ): Result[void, string] = ## Unsubscribes from a specific PubSub or Content topic. + ## This will both unsubscribe from the relay mesh and remove the app handler, if any. + ## NOTE: This works because using MAPI and Kernel API at the same time is unsupported. - if node.wakuRelay.isNil(): + if isNil(node.wakuRelay): error "Invalid API call to `unsubscribe`. WakuRelay not mounted." return err("Invalid API call to `unsubscribe`. WakuRelay not mounted.") - let (pubsubTopic, contentTopicOp) = - case subscription.kind - of ContentUnsub: - if node.wakuAutoSharding.isSome(): - let shard = node.wakuAutoSharding.get().getShard((subscription.topic)).valueOr: - error "Autosharding error", error = error - return err("Autosharding error: " & error) - ($shard, some(subscription.topic)) - else: - return err( - "Static sharding is used, relay subscriptions must specify a pubsub topic" - ) - of PubsubUnsub: - (subscription.topic, none(ContentTopic)) + let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: + error "Failed to decode unsubscribe event", error = error + return err("Failed to decode unsubscribe event: " & error) + + let hadHandler = node.legacyAppHandlers.hasKey(pubsubTopic) + if hadHandler: + node.legacyAppHandlers.del(pubsubTopic) + + if node.wakuRelay.isSubscribed(pubsubTopic): + info "unsubscribe", pubsubTopic, contentTopicOp + node.wakuRelay.unsubscribe(pubsubTopic) + node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: pubsubTopic)) + else: + if not hadHandler: + warn "No-effect API call to `unsubscribe`. Was not subscribed", pubsubTopic else: - return err("Unsupported subscription type in relay unsubscribe") - - if not node.wakuRelay.isSubscribed(pubsubTopic): - warn "No-effect API call to `unsubscribe`. Was not subscribed", pubsubTopic - return ok() - - info "unsubscribe", pubsubTopic, contentTopicOp - node.wakuRelay.unsubscribe(pubsubTopic) - node.topicSubscriptionQueue.emit((kind: PubsubUnsub, topic: pubsubTopic)) + info "unsubscribe (was not subscribed in the mesh; appHandler removed)", + pubsubTopic = pubsubTopic return ok() +proc isSubscribed*( + node: WakuNode, subscription: SubscriptionEvent +): Result[bool, string] = + if node.wakuRelay.isNil(): + error "Invalid API call to `isSubscribed`. WakuRelay not mounted." + return err("Invalid API call to `isSubscribed`. WakuRelay not mounted.") + + let (pubsubTopic, contentTopicOp) = getTopicOfSubscriptionEvent(node, subscription).valueOr: + error "Failed to decode subscription event", error = error + return err("Failed to decode subscription event: " & error) + + return ok(node.wakuRelay.isSubscribed(pubsubTopic)) + proc publish*( node: WakuNode, pubsubTopicOp: Option[PubsubTopic], message: WakuMessage -): Future[Result[void, string]] {.async, gcsafe.} = +): Future[Result[int, string]] {.async, gcsafe.} = ## Publish a `WakuMessage`. Pubsub topic contains; none, a named or static shard. ## `WakuMessage` should contain a `contentTopic` field for light node functionality. ## It is also used to determine the shard. @@ -184,16 +225,20 @@ proc publish*( let msg = "Autosharding error: " & error return err(msg) - #TODO instead of discard return error when 0 peers received the message - discard await node.wakuRelay.publish(pubsubTopic, message) + let numPeers = (await node.wakuRelay.publish(pubsubTopic, message)).valueOr: + warn "waku.relay did not publish", error = error + # Todo: If NoPeersToPublish, we might want to return ok(0) instead!!! + return err("publish failed in relay: " & $error) notice "waku.relay published", peerId = node.peerId, pubsubTopic = pubsubTopic, msg_hash = pubsubTopic.computeMessageHash(message).to0xHex(), - publishTime = getNowInNanosecondTime() + publishTime = getNowInNanosecondTime(), + numPeers = numPeers - return ok() + # TODO: investigate if we can return error in case numPeers is 0 + ok(numPeers) proc mountRelay*( node: WakuNode, @@ -218,7 +263,8 @@ proc mountRelay*( node.wakuRelay.routingRecordsHandler.add(peerExchangeHandler.get()) if node.started: - await node.startRelay() + await node.wakuRelay.start() + await node.reconnectRelayPeers() node.switch.mount(node.wakuRelay, protocolMatcher(WakuRelayCodec)) @@ -240,11 +286,8 @@ proc mountRlnRelay*( CatchableError, "WakuRelay protocol is not mounted, cannot mount WakuRlnRelay" ) - let rlnRelayRes = await WakuRlnRelay.new(rlnConf, registrationHandler) - if rlnRelayRes.isErr(): - raise - newException(CatchableError, "failed to mount WakuRlnRelay: " & rlnRelayRes.error) - let rlnRelay = rlnRelayRes.get() + let rlnRelay = (await WakuRlnRelay.new(rlnConf, registrationHandler)).valueOr: + raise newException(CatchableError, "failed to mount WakuRlnRelay: " & error) if (rlnConf.userMessageLimit > rlnRelay.groupManager.rlnRelayMaxMessageLimit): error "rln-relay-user-message-limit can't exceed the MAX_MESSAGE_LIMIT in the rln contract" let validator = generateRlnValidator(rlnRelay, spamHandler) diff --git a/waku/node/kernel_api/store.nim b/waku/node/kernel_api/store.nim new file mode 100644 index 000000000..fcf0dfc89 --- /dev/null +++ b/waku/node/kernel_api/store.nim @@ -0,0 +1,146 @@ +{.push raises: [].} + +import + std/[options], + chronos, + chronicles, + metrics, + results, + eth/keys, + eth/p2p/discoveryv5/enr, + libp2p/crypto/crypto, + libp2p/protocols/ping, + libp2p/protocols/pubsub/gossipsub, + libp2p/protocols/pubsub/rpc/messages, + libp2p/builders, + libp2p/transports/tcptransport, + libp2p/transports/wstransport, + libp2p/utility + +import + ../waku_node, + ../../waku_core, + ../../waku_store/protocol as store, + ../../waku_store/client as store_client, + ../../waku_store/common as store_common, + ../../waku_store/resume, + ../peer_manager, + ../../common/rate_limit/setting, + ../../waku_archive + +logScope: + topics = "waku node store api" + +## Waku archive +proc mountArchive*( + node: WakuNode, + driver: waku_archive.ArchiveDriver, + retentionPolicies = newSeq[waku_archive.RetentionPolicy](), +): Result[void, string] = + node.wakuArchive = waku_archive.WakuArchive.new( + driver = driver, retentionPolicies = retentionPolicies + ).valueOr: + return err("error in mountArchive: " & error) + + node.wakuArchive.start() + + return ok() + +## Waku Store + +proc toArchiveQuery(request: StoreQueryRequest): waku_archive.ArchiveQuery = + var query = waku_archive.ArchiveQuery() + + query.includeData = request.includeData + query.pubsubTopic = request.pubsubTopic + query.contentTopics = request.contentTopics + query.startTime = request.startTime + query.endTime = request.endTime + query.hashes = request.messageHashes + query.cursor = request.paginationCursor + query.direction = request.paginationForward + query.requestId = request.requestId + + if request.paginationLimit.isSome(): + query.pageSize = uint(request.paginationLimit.get()) + + return query + +proc toStoreResult(res: waku_archive.ArchiveResult): StoreQueryResult = + let response = res.valueOr: + return err(StoreError.new(300, "archive error: " & $error)) + + var res = StoreQueryResponse() + + res.statusCode = 200 + res.statusDesc = "OK" + + for i in 0 ..< response.hashes.len: + let hash = response.hashes[i] + + let kv = store_common.WakuMessageKeyValue(messageHash: hash) + + res.messages.add(kv) + + for i in 0 ..< response.messages.len: + res.messages[i].message = some(response.messages[i]) + res.messages[i].pubsubTopic = some(response.topics[i]) + + res.paginationCursor = response.cursor + + return ok(res) + +proc mountStore*( + node: WakuNode, rateLimit: RateLimitSetting = DefaultGlobalNonRelayRateLimit +) {.async.} = + if node.wakuArchive.isNil(): + error "failed to mount waku store protocol", error = "waku archive not set" + return + + info "mounting waku store protocol" + + let requestHandler: StoreQueryRequestHandler = proc( + request: StoreQueryRequest + ): Future[StoreQueryResult] {.async.} = + let request = request.toArchiveQuery() + let response = await node.wakuArchive.findMessages(request) + + return response.toStoreResult() + + node.wakuStore = + store.WakuStore.new(node.peerManager, node.rng, requestHandler, some(rateLimit)) + + if node.started: + await node.wakuStore.start() + + node.switch.mount(node.wakuStore, protocolMatcher(store_common.WakuStoreCodec)) + +proc mountStoreClient*(node: WakuNode) = + info "mounting store client" + + node.wakuStoreClient = store_client.WakuStoreClient.new(node.peerManager, node.rng) + +proc query*( + node: WakuNode, request: store_common.StoreQueryRequest, peer: RemotePeerInfo +): Future[store_common.WakuStoreResult[store_common.StoreQueryResponse]] {. + async, gcsafe +.} = + ## Queries known nodes for historical messages + if node.wakuStoreClient.isNil(): + return err("waku store v3 client is nil") + + let response = (await node.wakuStoreClient.query(request, peer)).valueOr: + var res = StoreQueryResponse() + res.statusCode = uint32(error.kind) + res.statusDesc = $error + + return ok(res) + + return ok(response) + +proc setupStoreResume*(node: WakuNode) = + node.wakuStoreResume = StoreResume.new( + node.peerManager, node.wakuArchive, node.wakuStoreClient + ).valueOr: + error "Failed to setup Store Resume", error = $error + return diff --git a/waku/node/peer_manager/peer_manager.nim b/waku/node/peer_manager/peer_manager.nim index 72b526aca..6602c049b 100644 --- a/waku/node/peer_manager/peer_manager.nim +++ b/waku/node/peer_manager/peer_manager.nim @@ -1,27 +1,31 @@ {.push raises: [].} import - std/[options, sets, sequtils, times, strformat, strutils, math, random, tables], + std/[ + options, sets, sequtils, times, strformat, strutils, math, random, tables, algorithm + ], chronos, chronicles, metrics, - libp2p/multistream, - libp2p/muxers/muxer, - libp2p/nameresolving/nameresolver, - libp2p/peerstore + libp2p/[multistream, muxers/muxer, nameresolving/nameresolver, peerstore], + brokers/broker_context import - ../../common/nimchronos, - ../../common/enr, - ../../common/callbacks, - ../../common/utils/parse_size_units, - ../../waku_core, - ../../waku_relay, - ../../waku_relay/protocol, - ../../waku_enr/sharding, - ../../waku_enr/capabilities, - ../../waku_metadata, - ../health_monitor/online_monitor, + waku/[ + waku_core, + waku_relay, + waku_metadata, + waku_core/topics/sharding, + waku_relay/protocol, + waku_enr/sharding, + waku_enr/capabilities, + events/peer_events, + common/nimchronos, + common/enr, + common/callbacks, + common/utils/parse_size_units, + node/health_monitor/online_monitor, + ], ./peer_store/peer_storage, ./waku_peer_store @@ -84,6 +88,7 @@ type ConnectionChangeHandler* = proc( ): Future[void] {.gcsafe, raises: [Defect].} type PeerManager* = ref object of RootObj + brokerCtx: BrokerContext switch*: Switch wakuMetadata*: WakuMetadata initialBackoffInSec*: int @@ -103,6 +108,8 @@ type PeerManager* = ref object of RootObj onConnectionChange*: ConnectionChangeHandler online: bool ## state managed by online_monitor module getShards: GetShards + maxConnections: int + activeStoreRequests*: Table[PeerId, int] #~~~~~~~~~~~~~~~~~~~# # Helper Functions # @@ -165,6 +172,23 @@ proc addPeer*( proc getPeer*(pm: PeerManager, peerId: PeerId): RemotePeerInfo = return pm.switch.peerStore.getPeer(peerId) +proc addActiveStoreRequest*(pm: PeerManager, peerId: PeerId) {.gcsafe.} = + pm.activeStoreRequests.mgetOrPut(peerId, 0).inc() + +proc removeActiveStoreRequest*(pm: PeerManager, peerId: PeerId) {.gcsafe.} = + let count = pm.activeStoreRequests.getOrDefault(peerId, 0) + if count == 0: + return + + let newCount = count - 1 + if newCount <= 0: + pm.activeStoreRequests.del(peerId) + else: + pm.activeStoreRequests[peerId] = newCount + +proc hasActiveStoreRequest*(pm: PeerManager, peerId: PeerId): bool {.gcsafe.} = + pm.activeStoreRequests.contains(peerId) + proc loadFromStorage(pm: PeerManager) {.gcsafe.} = ## Load peers from storage, if available @@ -211,19 +235,34 @@ proc loadFromStorage(pm: PeerManager) {.gcsafe.} = trace "recovered peers from storage", amount = amount +proc selectPeers*( + pm: PeerManager, proto: string, shard: Option[PubsubTopic] = none(PubsubTopic) +): seq[RemotePeerInfo] = + ## Returns all peers that support the given protocol (and optionally shard), + ## shuffled randomly. Callers can further filter or pick from this list. + var peers = pm.switch.peerStore.getPeersByProtocol(proto) + trace "Selecting peers from peerstore", + protocol = proto, num_peers = peers.len, address = cast[uint](pm.switch.peerStore) + + if shard.isSome(): + let shardInfo = RelayShard.parse(shard.get()).valueOr: + trace "Failed to parse shard from pubsub topic", topic = shard.get() + return @[] + + peers.keepItIf( + (it.enr.isSome() and it.enr.get().containsShard(shard.get())) or + (it.shards.len > 0 and it.shards.contains(shardInfo.shardId)) + ) + + shuffle(peers) + return peers + proc selectPeer*( pm: PeerManager, proto: string, shard: Option[PubsubTopic] = none(PubsubTopic) ): Option[RemotePeerInfo] = - # Selects the best peer for a given protocol - - var peers = pm.switch.peerStore.getPeersByProtocol(proto) - trace "Selecting peer from peerstore", - protocol = proto, peers, address = cast[uint](pm.switch.peerStore) - - if shard.isSome(): - peers.keepItIf((it.enr.isSome() and it.enr.get().containsShard(shard.get()))) - - shuffle(peers) + ## Selects a single peer for a given protocol, checking service slots first + ## (for non-relay protocols). + let peers = pm.selectPeers(proto, shard) # No criteria for selecting a peer for WakuRelay, random one if proto == WakuRelayCodec: @@ -482,8 +521,9 @@ proc canBeConnected*(pm: PeerManager, peerId: PeerId): bool = proc connectedPeers*( pm: PeerManager, protocol: string = "" ): (seq[PeerId], seq[PeerId]) = - ## Returns the peerIds of physical connections (in and out) - ## If a protocol is specified, only returns peers with at least one stream of that protocol + ## Returns the PeerIds of peers with an active socket connection. + ## If a protocol is specified, it returns peers that currently have one + ## or more active logical streams for that protocol. var inPeers: seq[PeerId] var outPeers: seq[PeerId] @@ -499,6 +539,74 @@ proc connectedPeers*( return (inPeers, outPeers) +proc evictPeer*(pm: PeerManager, peerId: PeerId) {.async.} = + ## Policy-based eviction (relay-peer limit, IP colocation, pruning). + ## Skips the disconnect when the peer has an in-flight store request to + ## avoid aborting active store requests. + if pm.hasActiveStoreRequest(peerId): + trace "skipping peer eviction: active store request", peerId = peerId + return + await pm.switch.disconnect(peerId) + +proc capablePeers*(pm: PeerManager, protocol: string): (seq[PeerId], seq[PeerId]) = + ## Returns the PeerIds of peers with an active socket connection. + ## If a protocol is specified, it returns peers that have identified + ## themselves as supporting the protocol. + + var inPeers: seq[PeerId] + var outPeers: seq[PeerId] + + for peerId, muxers in pm.switch.connManager.getConnections(): + # filter out peers that don't have the capability registered in the peer store + if pm.switch.peerStore.hasPeer(peerId, protocol): + for peerConn in muxers: + if peerConn.connection.transportDir == Direction.In: + inPeers.add(peerId) + elif peerConn.connection.transportDir == Direction.Out: + outPeers.add(peerId) + + return (inPeers, outPeers) + +proc getConnectedPeersCount*(pm: PeerManager, protocol: string): int = + ## Returns the total number of unique connected peers (inbound + outbound) + ## with active streams for a specific protocol. + let (inPeers, outPeers) = pm.connectedPeers(protocol) + var peers = initHashSet[PeerId](nextPowerOfTwo(inPeers.len + outPeers.len)) + for p in inPeers: + peers.incl(p) + for p in outPeers: + peers.incl(p) + return peers.len + +proc getCapablePeersCount*(pm: PeerManager, protocol: string): int = + ## Returns the total number of unique connected peers (inbound + outbound) + ## who have identified themselves as supporting the given protocol. + let (inPeers, outPeers) = pm.capablePeers(protocol) + var peers = initHashSet[PeerId](nextPowerOfTwo(inPeers.len + outPeers.len)) + for p in inPeers: + peers.incl(p) + for p in outPeers: + peers.incl(p) + return peers.len + +proc getPeersForShard*(pm: PeerManager, protocolId: string, shard: PubsubTopic): int = + let (inPeers, outPeers) = pm.connectedPeers(protocolId) + let connectedProtocolPeers = inPeers & outPeers + if connectedProtocolPeers.len == 0: + return 0 + + let shardInfo = RelayShard.parse(shard).valueOr: + # count raw peers of the given protocol if for some reason we can't get + # a shard mapping out of the gossipsub topic string. + return connectedProtocolPeers.len + + var shardPeers = 0 + for peerId in connectedProtocolPeers: + if pm.switch.peerStore.hasShard(peerId, shardInfo.clusterId, shardInfo.shardId): + shardPeers.inc() + + return shardPeers + proc disconnectAllPeers*(pm: PeerManager) {.async.} = let (inPeerIds, outPeerIds) = pm.connectedPeers() let connectedPeers = concat(inPeerIds, outPeerIds) @@ -634,7 +742,7 @@ proc getPeerIp(pm: PeerManager, peerId: PeerId): Option[string] = # Event Handling # #~~~~~~~~~~~~~~~~~# -proc onPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = +proc refreshPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = let res = catch: await pm.switch.dial(peerId, WakuMetadataCodec) @@ -658,6 +766,15 @@ proc onPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = $clusterId break guardClauses + # Store the shard information from metadata in the peer store + if pm.switch.peerStore.peerExists(peerId): + let shards = metadata.shards.mapIt(it.uint16) + pm.switch.peerStore.setShardInfo(peerId, shards) + + # TODO: should only trigger an event if metadata actually changed + # should include the shard subscription delta in the event when + # it is a MetadataUpdated event + WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventMetadataUpdated) return info "disconnecting from peer", peerId = peerId, reason = reason @@ -667,14 +784,14 @@ proc onPeerMetadata(pm: PeerManager, peerId: PeerId) {.async.} = # called when a peer i) first connects to us ii) disconnects all connections from us proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = if not pm.wakuMetadata.isNil() and event.kind == PeerEventKind.Joined: - await pm.onPeerMetadata(peerId) + await pm.refreshPeerMetadata(peerId) var peerStore = pm.switch.peerStore var direction: PeerDirection var connectedness: Connectedness case event.kind - of Joined: + of PeerEventKind.Joined: direction = if event.initiator: Outbound else: Inbound connectedness = Connected @@ -682,11 +799,11 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = let inRelayPeers = pm.connectedPeers(WakuRelayCodec)[0] if inRelayPeers.len > pm.inRelayPeersTarget and peerStore.hasPeer(peerId, WakuRelayCodec): - info "disconnecting relay peer because reached max num in-relay peers", + info "relay peer limit reached, evicting peer", peerId = peerId, inRelayPeers = inRelayPeers.len, inRelayPeersTarget = pm.inRelayPeersTarget - await pm.switch.disconnect(peerId) + await pm.evictPeer(peerId) ## Apply max ip colocation limit if (let ip = pm.getPeerIp(peerId); ip.isSome()): @@ -699,13 +816,15 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = if pm.colocationLimit != 0 and peersBehindIp.len > pm.colocationLimit: for peerId in peersBehindIp[0 ..< (peersBehindIp.len - pm.colocationLimit)]: info "Pruning connection due to ip colocation", peerId = peerId, ip = ip - asyncSpawn(pm.switch.disconnect(peerId)) + asyncSpawn(pm.evictPeer(peerId)) peerStore.delete(peerId) + WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventConnected) + if not pm.onConnectionChange.isNil(): # we don't want to await for the callback to finish asyncSpawn pm.onConnectionChange(peerId, Joined) - of Left: + of PeerEventKind.Left: direction = UnknownDirection connectedness = CanConnect @@ -717,12 +836,16 @@ proc onPeerEvent(pm: PeerManager, peerId: PeerId, event: PeerEvent) {.async.} = pm.ipTable.del(ip) break + WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventDisconnected) + if not pm.onConnectionChange.isNil(): # we don't want to await for the callback to finish asyncSpawn pm.onConnectionChange(peerId, Left) - of Identified: + of PeerEventKind.Identified: info "event identified", peerId = peerId + WakuPeerEvent.emit(pm.brokerCtx, peerId, WakuPeerEventKind.EventIdentified) + peerStore[ConnectionBook][peerId] = connectedness peerStore[DirectionBook][peerId] = direction @@ -743,7 +866,6 @@ proc logAndMetrics(pm: PeerManager) {.async.} = var peerStore = pm.switch.peerStore # log metrics let (inRelayPeers, outRelayPeers) = pm.connectedPeers(WakuRelayCodec) - let maxConnections = pm.switch.connManager.inSema.size let notConnectedPeers = peerStore.getDisconnectedPeers().mapIt(RemotePeerInfo.init(it.peerId, it.addrs)) let outsideBackoffPeers = notConnectedPeers.filterIt(pm.canBeConnected(it.peerId)) @@ -753,7 +875,7 @@ proc logAndMetrics(pm: PeerManager) {.async.} = info "Relay peer connections", inRelayConns = $inRelayPeers.len & "/" & $pm.inRelayPeersTarget, outRelayConns = $outRelayPeers.len & "/" & $pm.outRelayPeersTarget, - totalConnections = $totalConnections & "/" & $maxConnections, + totalConnections = $totalConnections & "/" & $pm.maxConnections, notConnectedPeers = notConnectedPeers.len, outsideBackoffPeers = outsideBackoffPeers.len @@ -1007,7 +1129,7 @@ proc pruneInRelayConns(pm: PeerManager, amount: int) {.async.} = for p in inRelayPeers[0 ..< connsToPrune]: trace "Pruning Peer", Peer = $p - asyncSpawn(pm.switch.disconnect(p)) + asyncSpawn(pm.evictPeer(p)) proc addExtPeerEventHandler*( pm: PeerManager, eventHandler: PeerEventHandler, eventKind: PeerEventKind @@ -1036,16 +1158,16 @@ proc new*( wakuMetadata: WakuMetadata = nil, maxRelayPeers: Option[int] = none(int), maxServicePeers: Option[int] = none(int), - relayServiceRatio: string = "60:40", + relayServiceRatio: string = "50:50", storage: PeerStorage = nil, initialBackoffInSec = InitialBackoffInSec, backoffFactor = BackoffFactor, maxFailedAttempts = MaxFailedAttempts, colocationLimit = DefaultColocationLimit, shardedPeerManagement = false, + maxConnections: int = MaxConnections, ): PeerManager {.gcsafe.} = let capacity = switch.peerStore.capacity - let maxConnections = switch.connManager.inSema.size if maxConnections > capacity: error "Max number of connections can't be greater than PeerManager capacity", capacity = capacity, maxConnections = maxConnections @@ -1080,8 +1202,11 @@ proc new*( error "Max backoff time can't be over 1 week", maxBackoff = backoff raise newException(Defect, "Max backoff time can't be over 1 week") + let brokerCtx = globalBrokerContext() + let pm = PeerManager( switch: switch, + brokerCtx: brokerCtx, wakuMetadata: wakuMetadata, storage: storage, initialBackoffInSec: initialBackoffInSec, @@ -1094,6 +1219,7 @@ proc new*( colocationLimit: colocationLimit, shardedPeerManagement: shardedPeerManagement, online: true, + maxConnections: maxConnections, ) proc peerHook( @@ -1117,6 +1243,7 @@ proc new*( pm.serviceSlots = initTable[string, RemotePeerInfo]() pm.ipTable = initTable[string, seq[PeerId]]() + pm.activeStoreRequests = initTable[PeerId, int]() if not storage.isNil(): trace "found persistent peer storage" diff --git a/waku/node/peer_manager/peer_store/migrations.nim b/waku/node/peer_manager/peer_store/migrations.nim index 61b416ed8..97961d25a 100644 --- a/waku/node/peer_manager/peer_store/migrations.nim +++ b/waku/node/peer_manager/peer_store/migrations.nim @@ -18,16 +18,14 @@ proc migrate*(db: SqliteDatabase, targetVersion = SchemaVersion): DatabaseResult ## it runs migration scripts if the `user_version` is outdated. The `migrationScriptsDir` path ## points to the directory holding the migrations scripts once the db is updated, it sets the ## `user_version` to the `tragetVersion`. - ## + ## ## If not `targetVersion` is provided, it defaults to `SchemaVersion`. ## ## NOTE: Down migration it is not currently supported info "starting peer store's sqlite database migration" - let migrationRes = - migrate(db, targetVersion, migrationsScriptsDir = PeerStoreMigrationPath) - if migrationRes.isErr(): - return err("failed to execute migration scripts: " & migrationRes.error) + migrate(db, targetVersion, migrationsScriptsDir = PeerStoreMigrationPath).isOkOr: + return err("failed to execute migration scripts: " & error) info "finished peer store's sqlite database migration" ok() diff --git a/waku/node/peer_manager/peer_store/waku_peer_storage.nim b/waku/node/peer_manager/peer_store/waku_peer_storage.nim index 876e8e258..dc1452618 100644 --- a/waku/node/peer_manager/peer_store/waku_peer_storage.nim +++ b/waku/node/peer_manager/peer_store/waku_peer_storage.nim @@ -67,7 +67,7 @@ proc encode*(remotePeerInfo: RemotePeerInfo): PeerStorageResult[ProtoBuffer] = let catchRes = catch: pb.write(4, remotePeerInfo.publicKey) - if catchRes.isErr(): + catchRes.isOkOr: return err("Enncoding public key failed: " & catchRes.error.msg) pb.write(5, uint32(ord(remotePeerInfo.connectedness))) @@ -154,14 +154,11 @@ method getAll*( let catchRes = catch: db.database.query("SELECT peerId, storedInfo FROM Peer", peer) - let queryRes = - if catchRes.isErr(): - return err("failed to extract peer from query result: " & catchRes.error.msg) - else: - catchRes.get() + let queryRes = catchRes.valueOr: + return err("failed to extract peer from query result: " & catchRes.error.msg) - if queryRes.isErr(): - return err("peer storage query failed: " & queryRes.error) + queryRes.isOkOr: + return err("peer storage query failed: " & error) return ok() diff --git a/waku/node/peer_manager/waku_peer_store.nim b/waku/node/peer_manager/waku_peer_store.nim index 0098c1687..93ac9ad2e 100644 --- a/waku/node/peer_manager/waku_peer_store.nim +++ b/waku/node/peer_manager/waku_peer_store.nim @@ -6,7 +6,8 @@ import chronicles, eth/p2p/discoveryv5/enr, libp2p/builders, - libp2p/peerstore + libp2p/peerstore, + libp2p/crypto/curve25519 import ../../waku_core, @@ -39,6 +40,9 @@ type # Keeps track of the ENR (Ethereum Node Record) of a peer ENRBook* = ref object of PeerBook[enr.Record] + # Keeps track of peer shards + ShardBook* = ref object of PeerBook[seq[uint16]] + proc getPeer*(peerStore: PeerStore, peerId: PeerId): RemotePeerInfo = let addresses = if peerStore[LastSeenBook][peerId].isSome(): @@ -55,6 +59,7 @@ proc getPeer*(peerStore: PeerStore, peerId: PeerId): RemotePeerInfo = else: none(enr.Record), protocols: peerStore[ProtoBook][peerId], + shards: peerStore[ShardBook][peerId], agent: peerStore[AgentBook][peerId], protoVersion: peerStore[ProtoVersionBook][peerId], publicKey: peerStore[KeyBook][peerId], @@ -64,6 +69,11 @@ proc getPeer*(peerStore: PeerStore, peerId: PeerId): RemotePeerInfo = direction: peerStore[DirectionBook][peerId], lastFailedConn: peerStore[LastFailedConnBook][peerId], numberFailedConn: peerStore[NumberFailedConnBook][peerId], + mixPubKey: + if peerStore[MixPubKeyBook][peerId] != default(Curve25519Key): + some(peerStore[MixPubKeyBook][peerId]) + else: + none(Curve25519Key), ) proc delete*(peerStore: PeerStore, peerId: PeerId) = @@ -72,16 +82,24 @@ proc delete*(peerStore: PeerStore, peerId: PeerId) = proc peers*(peerStore: PeerStore): seq[RemotePeerInfo] = let allKeys = concat( - toSeq(peerStore[LastSeenBook].book.keys()), + toSeq(peerStore[LastSeenOutboundBook].book.keys()), toSeq(peerStore[AddressBook].book.keys()), toSeq(peerStore[ProtoBook].book.keys()), toSeq(peerStore[KeyBook].book.keys()), + toSeq(peerStore[ShardBook].book.keys()), ) .toHashSet() return allKeys.mapIt(peerStore.getPeer(it)) proc addPeer*(peerStore: PeerStore, peer: RemotePeerInfo, origin = UnknownOrigin) = + ## Storing MixPubKey even if peer is already present as this info might be new + ## or updated. + if peer.mixPubKey.isSome(): + trace "adding mix pub key to peer store", + peer_id = $peer.peerId, mix_pub_key = $peer.mixPubKey.get() + peerStore[MixPubKeyBook].book[peer.peerId] = peer.mixPubKey.get() + ## Notice that the origin parameter is used to manually override the given peer origin. ## At the time of writing, this is used in waku_discv5 or waku_node (peer exchange.) if peerStore[AddressBook][peer.peerId] == peer.addrs and @@ -108,6 +126,7 @@ proc addPeer*(peerStore: PeerStore, peer: RemotePeerInfo, origin = UnknownOrigin peerStore[ProtoBook][peer.peerId] = protos ## We don't care whether the item was already present in the table or not. Hence, we always discard the hasKeyOrPut's bool returned value + discard peerStore[AgentBook].book.hasKeyOrPut(peer.peerId, peer.agent) discard peerStore[ProtoVersionBook].book.hasKeyOrPut(peer.peerId, peer.protoVersion) discard peerStore[KeyBook].book.hasKeyOrPut(peer.peerId, peer.publicKey) @@ -127,6 +146,9 @@ proc addPeer*(peerStore: PeerStore, peer: RemotePeerInfo, origin = UnknownOrigin if peer.enr.isSome(): peerStore[ENRBook][peer.peerId] = peer.enr.get() +proc setShardInfo*(peerStore: PeerStore, peerId: PeerID, shards: seq[uint16]) = + peerStore[ShardBook][peerId] = shards + proc peers*(peerStore: PeerStore, proto: string): seq[RemotePeerInfo] = peerStore.peers().filterIt(it.protocols.contains(proto)) @@ -137,7 +159,9 @@ proc connectedness*(peerStore: PeerStore, peerId: PeerId): Connectedness = peerStore[ConnectionBook].book.getOrDefault(peerId, NotConnected) proc hasShard*(peerStore: PeerStore, peerId: PeerID, cluster, shard: uint16): bool = - peerStore[ENRBook].book.getOrDefault(peerId).containsShard(cluster, shard) + return + peerStore[ENRBook].book.getOrDefault(peerId).containsShard(cluster, shard) or + peerStore[ShardBook].book.getOrDefault(peerId, @[]).contains(shard) proc hasCapability*(peerStore: PeerStore, peerId: PeerID, cap: Capabilities): bool = peerStore[ENRBook].book.getOrDefault(peerId).supportsCapability(cap) @@ -194,7 +218,8 @@ proc getPeersByShard*( peerStore: PeerStore, cluster, shard: uint16 ): seq[RemotePeerInfo] = return peerStore.peers.filterIt( - it.enr.isSome() and it.enr.get().containsShard(cluster, shard) + (it.enr.isSome() and it.enr.get().containsShard(cluster, shard)) or + it.shards.contains(shard) ) proc getPeersByCapability*( @@ -202,3 +227,17 @@ proc getPeersByCapability*( ): seq[RemotePeerInfo] = return peerStore.peers.filterIt(it.enr.isSome() and it.enr.get().supportsCapability(cap)) + +template forEnrPeers*( + peerStore: PeerStore, + peerId, peerConnectedness, peerOrigin, peerEnrRecord, body: untyped, +) = + let enrBook = peerStore[ENRBook] + let connBook = peerStore[ConnectionBook] + let sourceBook = peerStore[SourceBook] + for pid, enrRecord in tables.pairs(enrBook.book): + let peerId {.inject.} = pid + let peerConnectedness {.inject.} = connBook.book.getOrDefault(pid, NotConnected) + let peerOrigin {.inject.} = sourceBook.book.getOrDefault(pid, UnknownOrigin) + let peerEnrRecord {.inject.} = enrRecord + body diff --git a/waku/node/waku_metrics.nim b/waku/node/waku_metrics.nim index 8d38624c1..af74b1532 100644 --- a/waku/node/waku_metrics.nim +++ b/waku/node/waku_metrics.nim @@ -2,8 +2,7 @@ import chronicles, chronos, metrics, metrics/chronos_httpserver import - ../waku_rln_relay/protocol_metrics as rln_metrics, - ../utils/collector, + waku/[net/auto_port, waku_rln_relay/protocol_metrics as rln_metrics, utils/collector], ./peer_manager, ./waku_node @@ -57,27 +56,36 @@ proc startMetricsLog*() = discard setTimer(Moment.fromNow(LogInterval), logMetrics) +type StartedMetricsServer* = tuple[server: MetricsHttpServerRef, port: Port] + proc startMetricsServer( serverIp: IpAddress, serverPort: Port -): Future[Result[MetricsHttpServerRef, string]] {.async.} = - info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $serverPort +): Future[Result[StartedMetricsServer, string]] {.async.} = + proc attempt( + port: Port + ): Future[Result[StartedMetricsServer, string]] {.async: (raises: []).} = + info "Starting metrics HTTP server", serverIp = $serverIp, serverPort = $port - let server = MetricsHttpServerRef.new($serverIp, serverPort).valueOr: - return err("metrics HTTP server start failed: " & $error) + let server = MetricsHttpServerRef.new($serverIp, port).valueOr: + return err("fail to start service metrics server, attempt:" & $error) - try: - await server.start() - except CatchableError: - return err("metrics HTTP server start failed: " & getCurrentExceptionMsg()) + try: + await server.start() + except CatchableError: + return + err("exception while startMetricsServer, attempt: " & getCurrentExceptionMsg()) - info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $serverPort - return ok(server) + info "Metrics HTTP server started", serverIp = $serverIp, serverPort = $port + return ok((server: server, port: port)) + + let started = (await tryWithAutoPort[StartedMetricsServer](serverPort, attempt)).valueOr: + return err("metrics HTTP server start failed: " & error) + return ok(started) proc startMetricsServerAndLogging*( conf: MetricsServerConf, portsShift: uint16 -): Future[Result[MetricsHttpServerRef, string]] {.async.} = - var metricsServer: MetricsHttpServerRef - metricsServer = ( +): Future[Result[StartedMetricsServer, string]] {.async.} = + let started = ( await ( startMetricsServer(conf.httpAddress, Port(conf.httpPort.uint16 + portsShift)) ) @@ -87,4 +95,4 @@ proc startMetricsServerAndLogging*( if conf.logging: startMetricsLog() - return ok(metricsServer) + return ok(started) diff --git a/waku/node/waku_node.nim b/waku/node/waku_node.nim index f34a47a01..26a2b5a57 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -1,7 +1,7 @@ {.push raises: [].} import - std/[options, tables, strutils, sequtils, os, net, random], + std/[options, tables, strutils, sequtils, os, net, random, sets], chronos, chronicles, metrics, @@ -22,40 +22,50 @@ import libp2p/transports/tcptransport, libp2p/transports/wstransport, libp2p/utility, + libp2p/utils/offsettedseq, libp2p/protocols/mix, - libp2p/protocols/mix/mix_protocol + libp2p/protocols/mix/mix_protocol, + brokers/broker_context, + brokers/request_broker import - ../waku_core, - ../waku_core/topics/sharding, - ../waku_relay, - ../waku_archive, - ../waku_archive_legacy, - ../waku_store_legacy/protocol as legacy_store, - ../waku_store_legacy/client as legacy_store_client, - ../waku_store_legacy/common as legacy_store_common, - ../waku_store/protocol as store, - ../waku_store/client as store_client, - ../waku_store/common as store_common, - ../waku_store/resume, - ../waku_store_sync, - ../waku_filter_v2, - ../waku_filter_v2/client as filter_client, - ../waku_metadata, - ../waku_rendezvous/protocol, - ../waku_lightpush_legacy/client as legacy_ligntpuhs_client, - ../waku_lightpush_legacy as legacy_lightpush_protocol, - ../waku_lightpush/client as ligntpuhs_client, - ../waku_lightpush as lightpush_protocol, - ../waku_enr, - ../waku_peer_exchange, - ../waku_rln_relay, - ./net_config, + waku/[ + waku_core, + waku_core/topics/sharding, + waku_relay, + waku_archive, + waku_store/protocol as store, + waku_store/client as store_client, + waku_store/common as store_common, + waku_store/resume, + waku_store_sync, + waku_filter_v2, + waku_filter_v2/client as filter_client, + waku_metadata, + waku_rendezvous/protocol, + waku_rendezvous/client as rendezvous_client, + waku_rendezvous/waku_peer_record, + waku_lightpush_legacy/client as legacy_ligntpuhs_client, + waku_lightpush_legacy as legacy_lightpush_protocol, + waku_lightpush/client as ligntpuhs_client, + waku_lightpush as lightpush_protocol, + waku_enr, + waku_peer_exchange, + waku_rln_relay, + common/rate_limit/setting, + common/callbacks, + common/nimchronos, + waku_mix, + requests/node_requests, + requests/health_requests, + events/health_events, + events/message_events, + ], + waku/discovery/waku_kademlia, + waku/net/[bound_ports, net_config], ./peer_manager, - ../common/rate_limit/setting, - ../common/callbacks, - ../common/nimchronos, - ../waku_mix + ./health_monitor/health_status, + ./health_monitor/topic_health declarePublicCounter waku_node_messages, "number of messages received", ["type"] @@ -98,9 +108,6 @@ type switch*: Switch wakuRelay*: WakuRelay wakuArchive*: waku_archive.WakuArchive - wakuLegacyArchive*: waku_archive_legacy.WakuArchive - wakuLegacyStore*: legacy_store.WakuStore - wakuLegacyStoreClient*: legacy_store_client.WakuStoreClient wakuStore*: store.WakuStore wakuStoreClient*: store_client.WakuStoreClient wakuStoreResume*: StoreResume @@ -120,27 +127,60 @@ type enr*: enr.Record libp2pPing*: Ping rng*: ref rand.HmacDrbgContext + brokerCtx*: BrokerContext wakuRendezvous*: WakuRendezVous + wakuRendezvousClient*: rendezvous_client.WakuRendezVousClient announcedAddresses*: seq[MultiAddress] + extMultiAddrsOnly*: bool # When true, skip automatic IP address replacement started*: bool # Indicates that node has started listening topicSubscriptionQueue*: AsyncEventQueue[SubscriptionEvent] rateLimitSettings*: ProtocolRateLimitSettings + legacyAppHandlers*: Table[PubsubTopic, WakuRelayHandler] + ## Kernel API Relay appHandlers (if any) wakuMix*: WakuMix + kademliaDiscoveryLoop*: Future[void] + wakuKademlia*: WakuKademlia + ports*: BoundPorts -proc getShardsGetter(node: WakuNode): GetShards = +proc deduceRelayShard( + node: WakuNode, + contentTopic: ContentTopic, + pubsubTopicOp: Option[PubsubTopic] = none[PubsubTopic](), +): Result[RelayShard, string] = + let pubsubTopic = pubsubTopicOp.valueOr: + if node.wakuAutoSharding.isNone(): + return err("Pubsub topic must be specified when static sharding is enabled.") + let shard = node.wakuAutoSharding.get().getShard(contentTopic).valueOr: + let msg = "Deducing shard failed: " & error + return err(msg) + return ok(shard) + + let shard = RelayShard.parse(pubsubTopic).valueOr: + return err("Invalid topic:" & pubsubTopic & " " & $error) + return ok(shard) + +proc getShardsGetter(node: WakuNode, configuredShards: seq[uint16]): GetShards = return proc(): seq[uint16] {.closure, gcsafe, raises: [].} = # fetch pubsubTopics subscribed to relay and convert them to shards if node.wakuRelay.isNil(): - return @[] + # If relay is not mounted, return configured shards + return configuredShards + let subscribedTopics = node.wakuRelay.subscribedTopics() + + # If relay hasn't subscribed to any topics yet, return configured shards + if subscribedTopics.len == 0: + return configuredShards + let relayShards = topicsToRelayShards(subscribedTopics).valueOr: error "could not convert relay topics to shards", error = $error, topics = subscribedTopics - return @[] + # Fall back to configured shards on error + return configuredShards if relayShards.isSome(): let shards = relayShards.get().shardIds return shards - return @[] + return configuredShards proc getCapabilitiesGetter(node: WakuNode): GetCapabilities = return proc(): seq[Capabilities] {.closure, gcsafe, raises: [].} = @@ -148,6 +188,17 @@ proc getCapabilitiesGetter(node: WakuNode): GetCapabilities = return @[] return node.enr.getCapabilities() +proc getWakuPeerRecordGetter(node: WakuNode): GetWakuPeerRecord = + return proc(): WakuPeerRecord {.closure, gcsafe, raises: [].} = + var mixKey: string + if not node.wakuMix.isNil(): + mixKey = node.wakuMix.pubKey.to0xHex() + return WakuPeerRecord.init( + peerId = node.switch.peerInfo.peerId, + addresses = node.announcedAddresses, + mixKey = mixKey, + ) + proc new*( T: type WakuNode, netConfig: NetConfig, @@ -162,18 +213,22 @@ proc new*( info "Initializing networking", addrs = $netConfig.announcedAddresses + let brokerCtx = globalBrokerContext() + let queue = newAsyncEventQueue[SubscriptionEvent](0) let node = WakuNode( peerManager: peerManager, switch: switch, rng: rng, + brokerCtx: brokerCtx, enr: enr, announcedAddresses: netConfig.announcedAddresses, topicSubscriptionQueue: queue, rateLimitSettings: rateLimitSettings, + ports: BoundPorts.init(), ) - peerManager.setShardGetter(node.getShardsGetter()) + peerManager.setShardGetter(node.getShardsGetter(@[])) return node @@ -218,15 +273,15 @@ proc mountMetadata*( if not node.wakuMetadata.isNil(): return err("Waku metadata already mounted, skipping") - let metadata = WakuMetadata.new(clusterId, node.getShardsGetter()) + let metadata = WakuMetadata.new(clusterId, node.getShardsGetter(shards)) node.wakuMetadata = metadata node.peerManager.wakuMetadata = metadata let catchRes = catch: node.switch.mount(node.wakuMetadata, protocolMatcher(WakuMetadataCodec)) - if catchRes.isErr(): - return err(catchRes.error.msg) + catchRes.isOkOr: + return err(error.msg) return ok() @@ -237,10 +292,11 @@ proc mountAutoSharding*( info "mounting auto sharding", clusterId = clusterId, shardCount = shardCount node.wakuAutoSharding = some(Sharding(clusterId: clusterId, shardCountGenZero: shardCount)) + return ok() proc getMixNodePoolSize*(node: WakuNode): int = - return node.wakuMix.getNodePoolSize() + return node.wakuMix.poolSize() proc mountMix*( node: WakuNode, @@ -257,17 +313,17 @@ proc mountMix*( return err("Failed to convert multiaddress to string.") info "local addr", localaddr = localaddrStr - let nodeAddr = localaddrStr & "/p2p/" & $node.peerId node.wakuMix = WakuMix.new( - nodeAddr, node.peerManager, clusterId, mixPrivKey, mixnodes + localaddrStr, node.peerManager, clusterId, mixPrivKey, mixnodes ).valueOr: error "Waku Mix protocol initialization failed", err = error return + #TODO: should we do the below only for exit node? Also, what if multiple protocols use mix? node.wakuMix.registerDestReadBehavior(WakuLightPushCodec, readLp(int(-1))) let catchRes = catch: node.switch.mount(node.wakuMix) - if catchRes.isErr(): - return err(catchRes.error.msg) + catchRes.isOkOr: + return err(error.msg) return ok() ## Waku Sync @@ -287,12 +343,11 @@ proc mountStoreSync*( let pubsubTopics = shards.mapIt($RelayShard(clusterId: cluster, shardId: it)) - let recon = - ?await SyncReconciliation.new( - pubsubTopics, contentTopics, node.peerManager, node.wakuArchive, - storeSyncRange.seconds, storeSyncInterval.seconds, storeSyncRelayJitter.seconds, - idsChannel, wantsChannel, needsChannel, - ) + let recon = ?await SyncReconciliation.new( + pubsubTopics, contentTopics, node.peerManager, node.wakuArchive, + storeSyncRange.seconds, storeSyncInterval.seconds, storeSyncRelayJitter.seconds, + idsChannel, wantsChannel, needsChannel, + ) node.wakuStoreReconciliation = recon @@ -300,8 +355,8 @@ proc mountStoreSync*( node.switch.mount( node.wakuStoreReconciliation, protocolMatcher(WakuReconciliationCodec) ) - if reconMountRes.isErr(): - return err(reconMountRes.error.msg) + reconMountRes.isOkOr: + return err(error.msg) let transfer = SyncTransfer.new( node.peerManager, node.wakuArchive, idsChannel, wantsChannel, needsChannel @@ -311,56 +366,67 @@ proc mountStoreSync*( let transMountRes = catch: node.switch.mount(node.wakuStoreTransfer, protocolMatcher(WakuTransferCodec)) - if transMountRes.isErr(): - return err(transMountRes.error.msg) + transMountRes.isOkOr: + return err(error.msg) return ok() -proc startRelay*(node: WakuNode) {.async.} = - ## Setup and start relay protocol - info "starting relay protocol" - +proc reconnectRelayPeers*(node: WakuNode) {.async.} = + ## Reconnect to previously-seen WakuRelay peers. if node.wakuRelay.isNil(): - error "Failed to start relay. Not mounted." return - - ## Setup relay protocol - - # Resume previous relay connections - if node.peerManager.switch.peerStore.hasPeers(protocolMatcher(WakuRelayCodec)): - info "Found previous WakuRelay peers. Reconnecting." - - # Reconnect to previous relay peers. This will respect a backoff period, if necessary - let backoffPeriod = - node.wakuRelay.parameters.pruneBackoff + chronos.seconds(BackoffSlackTime) - - await node.peerManager.reconnectPeers(WakuRelayCodec, backoffPeriod) - - # Start the WakuRelay protocol - await node.wakuRelay.start() - - info "relay started successfully" + if not node.peerManager.switch.peerStore.hasPeers(protocolMatcher(WakuRelayCodec)): + return + info "Found previous WakuRelay peers. Reconnecting." + let backoffPeriod = + node.wakuRelay.parameters.pruneBackoff + chronos.seconds(BackoffSlackTime) + await node.peerManager.reconnectPeers(WakuRelayCodec, backoffPeriod) proc selectRandomPeers*(peers: seq[PeerId], numRandomPeers: int): seq[PeerId] = var randomPeers = peers shuffle(randomPeers) return randomPeers[0 ..< min(len(randomPeers), numRandomPeers)] -proc mountRendezvous*(node: WakuNode, clusterId: uint16) {.async: (raises: []).} = +proc mountRendezvousClient*(node: WakuNode, clusterId: uint16) {.async: (raises: []).} = + info "mounting rendezvous client" + + node.wakuRendezvousClient = rendezvous_client.WakuRendezVousClient.new( + node.switch, node.peerManager, clusterId + ).valueOr: + error "initializing waku rendezvous client failed", error = error + return + + if node.started: + await node.wakuRendezvousClient.start() + +proc mountRendezvous*( + node: WakuNode, clusterId: uint16, shards: seq[RelayShard] = @[] +) {.async: (raises: []).} = info "mounting rendezvous discovery protocol" + let configuredShards = shards.mapIt(it.shardId) + node.wakuRendezvous = WakuRendezVous.new( node.switch, node.peerManager, clusterId, - node.getShardsGetter(), + node.getShardsGetter(configuredShards), node.getCapabilitiesGetter(), + node.getWakuPeerRecordGetter(), ).valueOr: error "initializing waku rendezvous failed", error = error return if node.started: - await node.wakuRendezvous.start() + try: + await node.wakuRendezvous.start() + except CancelledError as exc: + error "failed to start wakuRendezvous", error = exc.msg + + try: + node.switch.mount(node.wakuRendezvous, protocolMatcher(WakuRendezVousCodec)) + except LPError: + error "failed to mount wakuRendezvous", error = getCurrentExceptionMsg() proc isBindIpWithZeroPort(inputMultiAdd: MultiAddress): bool = let inputStr = $inputMultiAdd @@ -370,6 +436,11 @@ proc isBindIpWithZeroPort(inputMultiAdd: MultiAddress): bool = return false proc updateAnnouncedAddrWithPrimaryIpAddr*(node: WakuNode): Result[void, string] = + # Skip automatic IP replacement if extMultiAddrsOnly is set + # This respects the user's explicitly configured announced addresses + if node.extMultiAddrsOnly: + return ok() + let peerInfo = node.switch.peerInfo var announcedStr = "" var listenStr = "" @@ -410,6 +481,82 @@ proc updateAnnouncedAddrWithPrimaryIpAddr*(node: WakuNode): Result[void, string] return ok() +proc startProvidersAndListeners*(node: WakuNode) = + RequestRelayShard.setProvider( + node.brokerCtx, + proc( + pubsubTopic: Option[PubsubTopic], contentTopic: ContentTopic + ): Result[RequestRelayShard, string] = + let shard = node.deduceRelayShard(contentTopic, pubsubTopic).valueOr: + return err($error) + return ok(RequestRelayShard(relayShard: shard)), + ).isOkOr: + error "Can't set provider for RequestRelayShard", error = error + + RequestShardTopicsHealth.setProvider( + node.brokerCtx, + proc(topics: seq[PubsubTopic]): Result[RequestShardTopicsHealth, string] = + var response: RequestShardTopicsHealth + + for shard in topics: + # Health resolution order: + # 1. Relay topicsHealth (computed from gossipsub mesh state) + # 2. If relay is active but topicsHealth hasn't computed yet, UNHEALTHY + # 3. Otherwise, ask edge filter (via broker; no-op if no provider set) + var healthStatus = TopicHealth.NOT_SUBSCRIBED + + if not node.wakuRelay.isNil: + healthStatus = + node.wakuRelay.topicsHealth.getOrDefault(shard, TopicHealth.NOT_SUBSCRIBED) + + if healthStatus == TopicHealth.NOT_SUBSCRIBED: + if not node.wakuRelay.isNil and node.wakuRelay.isSubscribed(shard): + healthStatus = TopicHealth.UNHEALTHY + else: + let edgeRes = RequestEdgeShardHealth.request(node.brokerCtx, shard) + if edgeRes.isOk(): + healthStatus = edgeRes.get().health + + response.topicHealth.add((shard, healthStatus)) + + return ok(response), + ).isOkOr: + error "Can't set provider for RequestShardTopicsHealth", error = error + + RequestContentTopicsHealth.setProvider( + node.brokerCtx, + proc(topics: seq[ContentTopic]): Result[RequestContentTopicsHealth, string] = + var response: RequestContentTopicsHealth + + for contentTopic in topics: + var topicHealth = TopicHealth.NOT_SUBSCRIBED + + let shardResult = node.deduceRelayShard(contentTopic, none[PubsubTopic]()) + + if shardResult.isOk(): + let shardObj = shardResult.get() + let pubsubTopic = $shardObj + if not isNil(node.wakuRelay): + topicHealth = node.wakuRelay.topicsHealth.getOrDefault( + pubsubTopic, TopicHealth.NOT_SUBSCRIBED + ) + + if topicHealth == TopicHealth.NOT_SUBSCRIBED: + let edgeRes = RequestEdgeShardHealth.request(node.brokerCtx, pubsubTopic) + if edgeRes.isOk(): + topicHealth = edgeRes.get().health + + response.contentTopicHealth.add((topic: contentTopic, health: topicHealth)) + + return ok(response), + ).isOkOr: + error "Can't set provider for RequestContentTopicsHealth", error = error + +proc stopProvidersAndListeners*(node: WakuNode) = + RequestRelayShard.clearProvider(node.brokerCtx) + RequestContentTopicsHealth.clearProvider(node.brokerCtx) + RequestShardTopicsHealth.clearProvider(node.brokerCtx) + proc start*(node: WakuNode) {.async.} = ## Starts a created Waku Node and ## all its mounted protocols. @@ -422,27 +569,11 @@ proc start*(node: WakuNode) {.async.} = if isBindIpWithZeroPort(address): zeroPortPresent = true - # Perform relay-specific startup tasks TODO: this should be rethought - if not node.wakuRelay.isNil(): - await node.startRelay() - - if not node.wakuMix.isNil(): - node.wakuMix.start() - - if not node.wakuMetadata.isNil(): - node.wakuMetadata.start() - if not node.wakuStoreResume.isNil(): await node.wakuStoreResume.start() - if not node.wakuRendezvous.isNil(): - await node.wakuRendezvous.start() - - if not node.wakuStoreReconciliation.isNil(): - node.wakuStoreReconciliation.start() - - if not node.wakuStoreTransfer.isNil(): - node.wakuStoreTransfer.start() + if not node.wakuRendezvousClient.isNil(): + await node.wakuRendezvousClient.start() ## The switch uses this mapper to update peer info addrs ## with announced addrs after start @@ -453,10 +584,22 @@ proc start*(node: WakuNode) {.async.} = node.switch.peerInfo.addressMappers.add(addressMapper) ## The switch will update addresses after start using the addressMapper + ## NOTE: This will dispatch gossipsub start to the WakuRelay.start method override await node.switch.start() + # After switch.start, run custom Logos Delivery relay start logic + await node.reconnectRelayPeers() + node.started = true + if not node.wakuFilterClient.isNil(): + node.wakuFilterClient.registerPushHandler( + proc(pubsubTopic: PubsubTopic, msg: WakuMessage) {.async, gcsafe.} = + MessageSeenEvent.emit(node.brokerCtx, pubsubTopic, msg) + ) + + node.startProvidersAndListeners() + if not zeroPortPresent: updateAnnouncedAddrWithPrimaryIpAddr(node).isOkOr: error "failed update announced addr", error = $error @@ -467,6 +610,10 @@ proc start*(node: WakuNode) {.async.} = proc stop*(node: WakuNode) {.async.} = ## By stopping the switch we are stopping all the underlying mounted protocols + + node.stopProvidersAndListeners() + + ## NOTE: This will dispatch gossipsub stop to the WakuRelay.stop method override await node.switch.stop() node.peerManager.stop() @@ -483,21 +630,15 @@ proc stop*(node: WakuNode) {.async.} = if not node.wakuStoreResume.isNil(): await node.wakuStoreResume.stopWait() - if not node.wakuStoreReconciliation.isNil(): - node.wakuStoreReconciliation.stop() - - if not node.wakuStoreTransfer.isNil(): - node.wakuStoreTransfer.stop() - - if not node.wakuPeerExchange.isNil() and not node.wakuPeerExchange.pxLoopHandle.isNil(): - await node.wakuPeerExchange.pxLoopHandle.cancelAndWait() - if not node.wakuPeerExchangeClient.isNil() and not node.wakuPeerExchangeClient.pxLoopHandle.isNil(): await node.wakuPeerExchangeClient.pxLoopHandle.cancelAndWait() - if not node.wakuRendezvous.isNil(): - await node.wakuRendezvous.stopWait() + if not node.wakuKademlia.isNil(): + await node.wakuKademlia.stop() + + if not node.wakuRendezvousClient.isNil(): + await node.wakuRendezvousClient.stopWait() node.started = false diff --git a/waku/persistency/backend_comm.nim b/waku/persistency/backend_comm.nim new file mode 100644 index 000000000..dd7e71297 --- /dev/null +++ b/waku/persistency/backend_comm.nim @@ -0,0 +1,161 @@ +## Cross-thread broker declarations for the persistency library. +## +## One EventBroker (writes, fire-and-forget) and five RequestBrokers (reads +## + acked delete). All in multi-thread (mt) mode: the listener / provider runs on the +## job's storage thread; callers on any thread reach it via the shared +## BrokerContext owned by the Job. +## +## ## Error type, important +## +## nim-brokers' RequestBroker macro hard-codes the response shape as +## `Future[Result[ResponseType, string]]` — the error channel is `string`, +## not our `PersistencyError`. We honour the broker contract here and lift +## back to `PersistencyError` at the public facade (persistency.nim). The +## convention for the broker-level string is `": "` so the +## facade can reconstruct the `PersistencyErrorKind`. +## +## ## Response shapes +## +## The five Kv* types are *response* objects (the value the provider +## returns). Per-request inputs sit on the `signature` proc parameters. + +{.push raises: [].} + +import std/[options, strutils] +import chronos, results +import brokers/[event_broker, request_broker, broker_context] +import brokers/internal/mt_codec +import ./types + +export broker_context + +# ── mt codec overloads for non-POD library types ──────────────────────── +# +# brokers 2.0.0's mtMarshalValue / mtUnmarshalValue handle scalars, enums, +# strings, seqs, arrays, and plain object/tuple recursion -- but they do +# not see through `distinct seq[byte]`, nor do they know how to dispatch +# a variant (case) object. We provide explicit overloads for the types +# that appear in our broker payloads. + +proc mtMarshalValue*( + buf: ptr UncheckedArray[byte], cap: int, value: Key, pos: var int +): bool {.gcsafe.} = + ## Encode a Key as the raw seq[byte] it wraps. + mtMarshalValue(buf, cap, bytes(value), pos) + +proc mtUnmarshalValue*( + buf: ptr UncheckedArray[byte], len: int, value: var Key, pos: var int +): bool {.gcsafe.} = + var s: seq[byte] + if not mtUnmarshalValue(buf, len, s, pos): + return false + value = Key(s) + return true + +proc mtMarshalValue*( + buf: ptr UncheckedArray[byte], cap: int, value: TxOp, pos: var int +): bool {.gcsafe.} = + ## TxOp is a case object: write the discriminator, then only the + ## fields that belong to the active branch. + if not mtMarshalValue(buf, cap, value.category, pos): + return false + if not mtMarshalValue(buf, cap, value.key, pos): + return false + let kind = uint8(ord(value.kind)) + if not mtMarshalValue(buf, cap, kind, pos): + return false + case value.kind + of txPut: + if not mtMarshalValue(buf, cap, value.payload, pos): + return false + of txDelete: + discard + return true + +proc mtUnmarshalValue*( + buf: ptr UncheckedArray[byte], len: int, value: var TxOp, pos: var int +): bool {.gcsafe.} = + var + category: string + key: Key + kindByte: uint8 + if not mtUnmarshalValue(buf, len, category, pos): + return false + if not mtUnmarshalValue(buf, len, key, pos): + return false + if not mtUnmarshalValue(buf, len, kindByte, pos): + return false + case TxOpKind(kindByte) + of txPut: + var payload: seq[byte] + if not mtUnmarshalValue(buf, len, payload, pos): + return false + value = TxOp(category: category, key: key, kind: txPut, payload: payload) + of txDelete: + value = TxOp(category: category, key: key, kind: txDelete) + return true + +EventBroker(mt): + type PersistEvent* = object + ops*: seq[TxOp] + +RequestBroker(mt): + type KvGet* = object + value*: Option[seq[byte]] + + proc signature*(category: string, key: Key): Future[Result[KvGet, string]] {.async.} + +RequestBroker(mt): + type KvExists* = object + value*: bool + + proc signature*( + category: string, key: Key + ): Future[Result[KvExists, string]] {.async.} + +RequestBroker(mt): + type KvScan* = object + rows*: seq[KvRow] + + proc signature*( + category: string, range: KeyRange, reverse: bool + ): Future[Result[KvScan, string]] {.async.} + +RequestBroker(mt): + type KvCount* = object + n*: int + + proc signature*( + category: string, range: KeyRange + ): Future[Result[KvCount, string]] {.async.} + +RequestBroker(mt): + type KvDelete* = object + existed*: bool + + proc signature*( + category: string, key: Key + ): Future[Result[KvDelete, string]] {.async.} + +# ── string<->PersistencyError boundary helpers ────────────────────────── + +const ErrSep = ": " + +proc encodeErr*(e: PersistencyError): string = + ## Encode a PersistencyError into the broker's string channel. The facade + ## decodes via `decodeErr`. + $e.kind & ErrSep & e.msg + +proc decodeErr*(s: string): PersistencyError = + ## Inverse of encodeErr. Falls back to peBackend if the prefix is missing. + let idx = s.find(ErrSep) + if idx < 0: + return persistencyErr(peBackend, s) + let head = s[0 ..< idx] + let tail = s[idx + ErrSep.len .. ^1] + for k in PersistencyErrorKind: + if $k == head: + return persistencyErr(k, tail) + persistencyErr(peBackend, s) + +{.pop.} diff --git a/waku/persistency/backend_sqlite.nim b/waku/persistency/backend_sqlite.nim new file mode 100644 index 000000000..6851febc1 --- /dev/null +++ b/waku/persistency/backend_sqlite.nim @@ -0,0 +1,247 @@ +## Synchronous SQLite backend for the persistency library. +## +## Plain procs against a SqliteDatabase connection. Phase 3 wraps these in +## per-job storage threads driven by brokers; phase 2 verifies the SQL +## itself against an in-memory database. + +import std/options +import results, sqlite3_abi +import ../common/databases/[common, db_sqlite] +import ./[types, schema] + +type + KvBackend* = ref object + db*: SqliteDatabase + putStmt: SqliteStmt[(seq[byte], seq[byte], seq[byte]), void] + deleteStmt: SqliteStmt[(seq[byte], seq[byte]), void] + + RowHandler = proc(s: ptr sqlite3_stmt) {.gcsafe, raises: [].} + +proc toErr(msg: string): PersistencyError {.inline.} = + persistencyErr(peBackend, msg) + +proc catBytes(category: string): seq[byte] = + var buf = newSeq[byte](category.len) + for i, c in category: + buf[i] = byte(c) + return buf + +proc keyBytes(key: Key): seq[byte] {.inline.} = + bytes(key) + +proc readBlob(s: ptr sqlite3_stmt, col: cint): seq[byte] = + let n = sqlite3_column_bytes(s, col) + var buf = newSeq[byte](n) + if n > 0: + let src = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, col)) + for i in 0 ..< n: + buf[i] = src[i] + return buf + +proc bindBlob(s: ptr sqlite3_stmt, n: cint, val: seq[byte]): cint = + if val.len > 0: + sqlite3_bind_blob(s, n, unsafeAddr val[0], val.len.cint, SQLITE_TRANSIENT) + else: + sqlite3_bind_blob(s, n, nil, 0.cint, SQLITE_TRANSIENT) + +proc runRead( + db: SqliteDatabase, sql: string, params: openArray[seq[byte]], onRow: RowHandler +): Result[void, PersistencyError] = + var s: ptr sqlite3_stmt + let rc = sqlite3_prepare_v2(db.env, sql.cstring, sql.len.cint, addr s, nil) + if rc != SQLITE_OK: + return err(toErr("prepare: " & $sqlite3_errstr(rc))) + defer: + discard sqlite3_finalize(s) + + for i, p in params: + let bc = bindBlob(s, cint(i + 1), p) + if bc != SQLITE_OK: + return err(toErr("bind: " & $sqlite3_errstr(bc))) + + while true: + let v = sqlite3_step(s) + case v + of SQLITE_ROW: + onRow(s) + of SQLITE_DONE: + break + else: + return err(toErr("step: " & $sqlite3_errstr(v))) + return ok() + +proc prepareStatements(b: KvBackend): DatabaseResult[void] = + b.putStmt = ?b.db.prepareStmt( + "INSERT OR REPLACE INTO kv(category, key, payload) VALUES (?, ?, ?);", + (seq[byte], seq[byte], seq[byte]), + void, + ) + b.deleteStmt = ?b.db.prepareStmt( + "DELETE FROM kv WHERE category = ? AND key = ?;", (seq[byte], seq[byte]), void + ) + return ok() + +proc openBackend*(path: string): Result[KvBackend, PersistencyError] = + let dbRes = SqliteDatabase.new(path) + if dbRes.isErr: + return err(toErr("open " & path & " failed: " & dbRes.error)) + let db = dbRes.get() + + applyPragmas(db).isOkOr: + return err(toErr(error)) + ensureSchema(db).isOkOr: + return err(toErr(error)) + + let b = KvBackend(db: db) + prepareStatements(b).isOkOr: + return err(toErr(error)) + return ok(b) + +proc openBackendInMemory*(): Result[KvBackend, PersistencyError] = + ## Convenience for tests. + let dbRes = SqliteDatabase.new(":memory:") + if dbRes.isErr: + return err(toErr("open :memory: failed: " & dbRes.error)) + let db = dbRes.get() + + applyPragmas(db).isOkOr: + return err(toErr(error)) + ensureSchema(db).isOkOr: + return err(toErr(error)) + + let b = KvBackend(db: db) + prepareStatements(b).isOkOr: + return err(toErr(error)) + return ok(b) + +proc close*(b: KvBackend) = + if b.db != nil: + dispose(b.putStmt) + dispose(b.deleteStmt) + b.db.close() + b.db = nil + +proc applyOne(b: KvBackend, op: TxOp): Result[void, PersistencyError] = + case op.kind + of txPut: + let r = b.putStmt.exec((catBytes(op.category), keyBytes(op.key), op.payload)) + if r.isErr: + return err(toErr("put failed: " & r.error)) + of txDelete: + let r = b.deleteStmt.exec((catBytes(op.category), keyBytes(op.key))) + if r.isErr: + return err(toErr("delete failed: " & r.error)) + return ok() + +proc execSql(b: KvBackend, sql: string): Result[void, PersistencyError] = + let r = b.db.query(sql, NoopRowHandler) + if r.isErr: + return err(toErr(sql & ": " & r.error)) + return ok() + +proc applyOps*(b: KvBackend, ops: openArray[TxOp]): Result[void, PersistencyError] = + ## Single op = auto-commit. Multiple ops = BEGIN IMMEDIATE / COMMIT, with + ## ROLLBACK on first failure. This is the single source of truth for write + ## SQL — Phase 3's PersistEvent listener calls straight into here. + if ops.len == 0: + return ok() + if ops.len == 1: + return b.applyOne(ops[0]) + + ?b.execSql("BEGIN IMMEDIATE;") + for op in ops: + let r = b.applyOne(op) + if r.isErr: + discard b.execSql("ROLLBACK;") + return r + ?b.execSql("COMMIT;") + return ok() + +proc getOne*( + b: KvBackend, category: string, key: Key +): Result[Option[seq[byte]], PersistencyError] = + var found: Option[seq[byte]] = none(seq[byte]) + proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} = + found = some(readBlob(rs, 0.cint)) + + ?b.db.runRead( + "SELECT payload FROM kv WHERE category = ? AND key = ? LIMIT 1;", + [catBytes(category), keyBytes(key)], + onRow, + ) + return ok(found) + +proc existsOne*( + b: KvBackend, category: string, key: Key +): Result[bool, PersistencyError] = + var present = false + proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} = + present = true + + ?b.db.runRead( + "SELECT 1 FROM kv WHERE category = ? AND key = ? LIMIT 1;", + [catBytes(category), keyBytes(key)], + onRow, + ) + return ok(present) + +proc deleteOne*( + b: KvBackend, category: string, key: Key +): Result[bool, PersistencyError] = + ## Returns true if a row was actually removed. + let existed = ?b.existsOne(category, key) + if not existed: + return ok(false) + let r = b.deleteStmt.exec((catBytes(category), keyBytes(key))) + if r.isErr: + return err(toErr("delete: " & r.error)) + return ok(true) + +proc scanRange*( + b: KvBackend, category: string, range: KeyRange, reverse = false +): Result[seq[KvRow], PersistencyError] = + let openEnded = bytes(range.stop).len == 0 + let direction = if reverse: "DESC" else: "ASC" + let sql = + if openEnded: + "SELECT key, payload FROM kv WHERE category = ? AND key >= ? ORDER BY key " & + direction & ";" + else: + "SELECT key, payload FROM kv WHERE category = ? AND key >= ? AND key < ? ORDER BY key " & + direction & ";" + + var rows: seq[KvRow] = @[] + proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} = + let k = readBlob(rs, 0.cint) + let p = readBlob(rs, 1.cint) + rows.add((rawKey(k), p)) + + if openEnded: + ?b.db.runRead(sql, [catBytes(category), keyBytes(range.start)], onRow) + else: + ?b.db.runRead( + sql, [catBytes(category), keyBytes(range.start), keyBytes(range.stop)], onRow + ) + return ok(rows) + +proc countRange*( + b: KvBackend, category: string, range: KeyRange +): Result[int, PersistencyError] = + let openEnded = bytes(range.stop).len == 0 + let sql = + if openEnded: + "SELECT COUNT(*) FROM kv WHERE category = ? AND key >= ?;" + else: + "SELECT COUNT(*) FROM kv WHERE category = ? AND key >= ? AND key < ?;" + + var n: int64 = 0 + proc onRow(rs: ptr sqlite3_stmt) {.gcsafe, raises: [].} = + n = sqlite3_column_int64(rs, 0.cint) + + if openEnded: + ?b.db.runRead(sql, [catBytes(category), keyBytes(range.start)], onRow) + else: + ?b.db.runRead( + sql, [catBytes(category), keyBytes(range.start), keyBytes(range.stop)], onRow + ) + return ok(int(n)) diff --git a/waku/persistency/backend_thread.nim b/waku/persistency/backend_thread.nim new file mode 100644 index 000000000..e32e5c209 --- /dev/null +++ b/waku/persistency/backend_thread.nim @@ -0,0 +1,271 @@ +## Internal per-job storage thread. +## +## Exposes two operations to ``persistency.nim``: +## * ``startStorageThread(ctx, dbPath)`` — spawn one worker, block until +## it signals ready (or error). Returns a ``JobRuntime``. +## * ``stopStorageThread(rt)`` — signal shutdown, join, free. +## +## The worker: +## 1. installs the supplied BrokerContext on its threadvar +## 2. opens the SQLite backend (creating the file + schema if absent) +## 3. registers the PersistEvent listener and the 5 RequestBroker +## providers under that context +## 4. runs the chronos event loop until shutdown is signalled +## 5. clears providers + listeners, closes the backend +## +## The arg struct lives in shared memory (``allocShared0``). The dbPath is +## carried as a shared cstring buffer rather than a Nim string to avoid +## refc ref-count traffic across threads. The arg is freed by +## ``stopStorageThread`` after ``joinThread`` returns. + +import std/[options, os] +import std/atomics # std/concurrency/atomics is the same module in Nim 2.2 +import chronos, chronicles, results +import brokers/[event_broker, request_broker, broker_context] +import ./[types, backend_comm, backend_sqlite] + +export broker_context, backend_comm + +logScope: + topics = "persistency thread" + +type + ReadyState {.pure.} = enum + Pending = 0 + Ready = 1 + Error = 2 + + StorageThreadArg = object + ctx: BrokerContext + dbPath: cstring ## allocShared0'd; freed in closeJob + dbPathLen: int ## bytes including the trailing NUL + shutdownFlag: Atomic[int] + readyFlag: Atomic[int] ## values from ReadyState + errBuf: array[256, char] ## last error message, NUL-terminated + + StorageThread = Thread[ptr StorageThreadArg] + +# ── arg helpers ───────────────────────────────────────────────────────── + +proc allocArg(ctx: BrokerContext, dbPath: string): ptr StorageThreadArg = + let arg = cast[ptr StorageThreadArg](allocShared0(sizeof(StorageThreadArg))) + arg.ctx = ctx + arg.dbPathLen = dbPath.len + 1 + arg.dbPath = cast[cstring](allocShared0(arg.dbPathLen)) + if dbPath.len > 0: + copyMem(arg.dbPath, unsafeAddr dbPath[0], dbPath.len) + return arg + +proc freeArg(a: ptr StorageThreadArg) = + if a.isNil(): + return + if a.dbPath != nil: + deallocShared(a.dbPath) + deallocShared(a) + +proc recordErr(a: ptr StorageThreadArg, msg: string) = + let n = min(msg.len, a.errBuf.len - 1) + for i in 0 ..< n: + a.errBuf[i] = msg[i] + a.errBuf[n] = '\0' + a.readyFlag.store(int(ReadyState.Error), moRelease) + +proc errMsg(a: ptr StorageThreadArg): string = + $cast[cstring](a.errBuf[0].addr) + +# ── provider closures ─────────────────────────────────────────────────── + +proc encode(e: PersistencyError): string = + encodeErr(e) + +template unwrapErr(r: untyped): string = + ## Disambiguates Result's `error` accessor from chronicles' `error` macro + ## by binding through an explicitly-typed local before stringifying. + block: + let pe: PersistencyError = r.error() + encode(pe) + +proc registerProviders(backend: KvBackend, ctx: BrokerContext): Result[void, string] = + ## Wires the 5 RequestBroker providers + the PersistEvent listener. + ## All closures capture `backend` by reference (it lives for the entire + ## thread lifetime). + + proc onGet(category: string, key: Key): Future[Result[KvGet, string]] {.async.} = + let r = backend.getOne(category, key) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvGet(value: r.get())) + + proc onExists( + category: string, key: Key + ): Future[Result[KvExists, string]] {.async.} = + let r = backend.existsOne(category, key) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvExists(value: r.get())) + + proc onScan( + category: string, range: KeyRange, reverse: bool + ): Future[Result[KvScan, string]] {.async.} = + let r = backend.scanRange(category, range, reverse) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvScan(rows: r.get())) + + proc onCount( + category: string, range: KeyRange + ): Future[Result[KvCount, string]] {.async.} = + let r = backend.countRange(category, range) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvCount(n: r.get())) + + proc onDelete( + category: string, key: Key + ): Future[Result[KvDelete, string]] {.async.} = + let r = backend.deleteOne(category, key) + if r.isErr: + return err(unwrapErr(r)) + return ok(KvDelete(existed: r.get())) + + # PersistEvent listener — fire-and-forget; we log on backend failure + # because the caller has no return channel. + proc onPersist(ev: PersistEvent): Future[void] {.async: (raises: []).} = + let r = backend.applyOps(ev.ops) + if r.isErr: + let pe: PersistencyError = r.error() + error "PersistEvent applyOps failed", + error = pe.msg, kind = $pe.kind, opCount = ev.ops.len + + KvGet.setProvider(ctx, onGet).isOkOr: + return err("KvGet.setProvider: " & error) + + let existsRes = KvExists.setProvider(ctx, onExists) + if existsRes.isErr: + return err("KvExists.setProvider: " & existsRes.error()) + + let scanRes = KvScan.setProvider(ctx, onScan) + if scanRes.isErr: + return err("KvScan.setProvider: " & scanRes.error()) + + let countRes = KvCount.setProvider(ctx, onCount) + if countRes.isErr: + return err("KvCount.setProvider: " & countRes.error()) + + let delRes = KvDelete.setProvider(ctx, onDelete) + if delRes.isErr: + return err("KvDelete.setProvider: " & delRes.error()) + + let listenRes = PersistEvent.listen(ctx, onPersist) + if listenRes.isErr: + return err("PersistEvent.listen: " & listenRes.error()) + + return ok() + +proc clearProviders(ctx: BrokerContext) = + KvGet.clearProvider(ctx) + KvExists.clearProvider(ctx) + KvScan.clearProvider(ctx) + KvCount.clearProvider(ctx) + KvDelete.clearProvider(ctx) + PersistEvent.dropAllListeners(ctx) + +# ── thread proc ───────────────────────────────────────────────────────── + +proc storageThreadMain(arg: ptr StorageThreadArg) {.thread.} = + ## Worker thread entrypoint. Errors during setup are surfaced via + ## arg.errBuf + readyFlag=ReadyState.Error; the spawning thread checks both. + + setThreadBrokerContext(arg.ctx) + + let path = $arg.dbPath + + let backendRes = + try: + openBackend(path) + except CatchableError as e: + arg.recordErr("openBackend raised: " & e.msg) + return + if backendRes.isErr: + arg.recordErr("openBackend: " & backendRes.error.msg) + return + let backend = backendRes.get() + + let regRes = + try: + registerProviders(backend, arg.ctx) + except CatchableError as e: + backend.close() + arg.recordErr("registerProviders raised: " & e.msg) + return + if regRes.isErr: + backend.close() + arg.recordErr(regRes.error) + return + + arg.readyFlag.store(int(ReadyState.Ready), moRelease) + + proc awaitShutdown() {.async.} = + while arg.shutdownFlag.load(moAcquire) != 1: + try: + await sleepAsync(milliseconds(10)) + except CatchableError: + discard + + try: + waitFor awaitShutdown() + except CatchableError as e: + error "storage thread loop crashed", err = e.msg + + clearProviders(arg.ctx) + backend.close() + +# ── lifecycle ─────────────────────────────────────────────────────────── + +type JobRuntime* = ref object + ## Opaque per-job runtime owned by `persistency.nim`. Holds the typed + ## Thread handle + shared arg pointer so closeJob can shut the worker + ## down. Created by `startStorageThread` and torn down by + ## `stopStorageThread`. + arg*: ptr StorageThreadArg + thread*: StorageThread + +proc startStorageThread*( + ctx: BrokerContext, dbPath: string +): Result[JobRuntime, PersistencyError] = + ## Spawn a storage worker for one job. Blocks until the worker either + ## signals ready (returns the runtime) or signals error (joins, frees, + ## returns peBackend with the worker's error message). + let arg = allocArg(ctx, dbPath) + arg.shutdownFlag.store(0, moRelease) + arg.readyFlag.store(int(ReadyState.Pending), moRelease) + + var rt = JobRuntime(arg: arg) + try: + createThread(rt.thread, storageThreadMain, arg) + except ResourceExhaustedError as e: + freeArg(arg) + return err(persistencyErr(peBackend, "createThread: " & e.msg)) + + # Spin-wait for ready or error. The thread does its setup synchronously + # before signaling, so this is bounded by SQLite open time. + while true: + let s = arg.readyFlag.load(moAcquire) + if s == int(ReadyState.Ready): + return ok(rt) + if s == int(ReadyState.Error): + let msg = errMsg(arg) + joinThread(rt.thread) + freeArg(arg) + return err(persistencyErr(peBackend, msg)) + sleep(1) + +proc stopStorageThread*(rt: JobRuntime) = + ## Signal shutdown, join the worker, free the shared arg. Idempotent in + ## the sense that it tolerates a nil arg (already stopped). + if rt == nil or rt.arg == nil: + return + rt.arg.shutdownFlag.store(1, moRelease) + joinThread(rt.thread) + freeArg(rt.arg) + rt.arg = nil diff --git a/waku/persistency/keys.nim b/waku/persistency/keys.nim new file mode 100644 index 000000000..6a3199b8c --- /dev/null +++ b/waku/persistency/keys.nim @@ -0,0 +1,180 @@ +## Composite-key encoding. +## +## Keys are byte-wise lexicographically comparable so SQLite's BLOB +## ordering reproduces tuple ordering of the original components. Each +## component contributes a self-delimiting, sort-stable byte sequence +## through an `encodePart` overload; the generic fallback recurses through +## `tuple | object` fields, so any user type whose fields are themselves +## encodable can be used as a key part without ceremony. +## +## ## Encoding by type +## +## | Nim type | Bytes emitted | +## |-------------------------|------------------------------------------------------------------| +## | `string`, `openArray[byte]` | 2-byte BE length prefix + payload bytes (max 65535 bytes) | +## | `int64`, `int`, .. | XOR with 0x8000_0000_0000_0000 then 8-byte BE (sign-flip) | +## | `uint64`, `uint32`, .. | 8-byte BE | +## | `bool` | 1 byte (0/1) | +## | `byte`, `char` | 1 byte | +## | `enum E` | sign-flipped 8-byte BE of `ord(v).int64` | +## | `Key` | raw bytes (lets you embed a pre-built key inside another) | +## | `tuple | object` | each field encoded in declaration order, concatenated | +## +## ## Sort-order caveats +## +## - Length-prefixed strings sort by **length first, then byte order**. For +## uniform-length components (channel ids, hashes) this is identical to +## natural lex order; for variable-length text it is not. +## - `int64.low < -1 < 0 < 1 < int64.high` after byte comparison thanks to +## the sign flip. +## - Tuple/object ordering is component-major: field 0 dominates field 1 +## dominates field 2, like a multi-column ORDER BY. +## +## ## Building keys +## +## `key(...)` is a variadic macro that calls `encodePart` per argument. It +## accepts mixed types in one call: +## +## ```nim +## let k = key("channel-42", 1'i64) +## let k2 = key("channel-42", (epoch: 1'i64, seqNum: 7'u64)) +## let k3 = key(myEnumValue, myObject) +## ``` +## +## For a single value, `toKey(v)` is the simpler form (same semantics). + +{.push raises: [].} + +import std/macros +import ./types + +const + StringLenMax* = 0xFFFF + SignFlip = 0x8000_0000_0000_0000'u64 + +# ── Low-level byte helpers ────────────────────────────────────────────── + +proc appendBE16(buf: var seq[byte], v: uint16) = + buf.add(byte((v shr 8) and 0xFF'u16)) + buf.add(byte(v and 0xFF'u16)) + +proc appendBE64(buf: var seq[byte], v: uint64) = + for shift in countdown(56, 0, 8): + buf.add(byte((v shr shift) and 0xFF'u64)) + +# ── encodePart: primitives ────────────────────────────────────────────── + +proc encodePart*(dest: var seq[byte], s: string) = + doAssert s.len <= StringLenMax, "string component exceeds 65535 bytes" + appendBE16(dest, uint16(s.len)) + for c in s: + dest.add(byte(c)) + +proc encodePart*(dest: var seq[byte], raw: openArray[byte]) = + doAssert raw.len <= StringLenMax, "byte component exceeds 65535 bytes" + appendBE16(dest, uint16(raw.len)) + for b in raw: + dest.add(b) + +proc encodePart*(dest: var seq[byte], i: int64) = + appendBE64(dest, cast[uint64](i) xor SignFlip) + +proc encodePart*(dest: var seq[byte], u: uint64) = + appendBE64(dest, u) + +proc encodePart*(dest: var seq[byte], i: int) {.inline.} = + encodePart(dest, i.int64) + +proc encodePart*(dest: var seq[byte], i: int32) {.inline.} = + encodePart(dest, i.int64) + +proc encodePart*(dest: var seq[byte], i: int16) {.inline.} = + encodePart(dest, i.int64) + +proc encodePart*(dest: var seq[byte], i: int8) {.inline.} = + encodePart(dest, i.int64) + +proc encodePart*(dest: var seq[byte], u: uint32) {.inline.} = + encodePart(dest, u.uint64) + +proc encodePart*(dest: var seq[byte], u: uint16) {.inline.} = + encodePart(dest, u.uint64) + +proc encodePart*(dest: var seq[byte], b: bool) = + dest.add(if b: 1'u8 else: 0'u8) + +proc encodePart*(dest: var seq[byte], b: byte) = + dest.add(b) + +proc encodePart*(dest: var seq[byte], c: char) = + dest.add(byte(c)) + +proc encodePart*(dest: var seq[byte], k: Key) = + ## Embed an already-encoded Key (e.g. a pre-built prefix) verbatim. + for b in bytes(k): + dest.add(b) + +# ── encodePart: generic structural fallback ───────────────────────────── + +proc encodePart*[E: enum](dest: var seq[byte], v: E) {.inline.} = + encodePart(dest, int64(ord(v))) + +proc encodePart*[T: tuple | object](dest: var seq[byte], v: T) = + ## Walks the type's fields in declaration order. Each field must itself + ## have an `encodePart` overload (primitive, Key, or another struct). + for f in fields(v): + encodePart(dest, f) + +# ── Public Key constructors ───────────────────────────────────────────── + +proc add*[T](k: var Key, v: T) = + ## In-place key extension. Equivalent to writing `encodePart` against the + ## underlying byte buffer. + var buf = seq[byte](k) + encodePart(buf, v) + k = Key(buf) + +proc toKey*[T](v: T): Key = + ## Single-value Key constructor. Equivalent to `key(v)`. + var buf: seq[byte] = @[] + encodePart(buf, v) + return Key(buf) + +macro key*(parts: varargs[typed]): Key = + ## Variadic Key builder. Accepts any mix of types for which `encodePart` + ## resolves -- including tuples and objects via the structural fallback. + ## + ## ```nim + ## key() # empty Key + ## key("ch", 1'i64) # 2-component + ## key("ch", (1'i64, 7'u64)) # nested tuple flattens + ## ``` + let bufSym = genSym(nskVar, "keyBuf") + var body = newStmtList() + body.add quote do: + var `bufSym`: seq[byte] = @[] + for p in parts: + body.add quote do: + encodePart(`bufSym`, `p`) + body.add quote do: + Key(`bufSym`) + return newBlockStmt(body) + +# ── Range helpers ─────────────────────────────────────────────────────── + +proc prefixRange*(prefix: Key): KeyRange = + ## Build [prefix, prefix++) — a half-open range that captures every key + ## starting with `prefix`. If `prefix` is all 0xFF, the upper bound is + ## empty (open-ended); the backend treats `stop.len == 0` as "no upper + ## bound". + var stop = bytes(prefix) + var i = stop.len - 1 + while i >= 0: + if stop[i] != 0xFF'u8: + stop[i] = stop[i] + 1'u8 + stop.setLen(i + 1) + return KeyRange(start: prefix, stop: Key(stop)) + dec i + return KeyRange(start: prefix, stop: Key(@[])) + +{.pop.} diff --git a/waku/persistency/payload.nim b/waku/persistency/payload.nim new file mode 100644 index 000000000..222de4177 --- /dev/null +++ b/waku/persistency/payload.nim @@ -0,0 +1,53 @@ +## Generic payload encoding. +## +## Symmetric with `keys.nim`: reuses the same `encodePart` family so any +## Nim type composable from primitives + tuples/objects can be turned +## into a `seq[byte]` for storage. Unlike keys, payloads do **not** need +## byte-wise lex order — but using the same encoder keeps the system +## small. If a tenant needs a different on-disk format (CBOR, protobuf, +## SSZ, ...) they can write their own `toPayload` overload or pass an +## already-encoded `seq[byte]` to `persistPut`. +## +## ```nim +## # Primitives: +## let p1 = payload("hello") # length-prefixed string bytes +## let p2 = payload(42'i64) # 8 bytes, sign-flipped BE +## +## # Composites: +## type Msg = object +## sender: string +## epoch: int64 +## body: seq[byte] +## let p3 = toPayload(Msg(sender: "alice", epoch: 7, body: @[1'u8, 2, 3])) +## +## # Variadic when you want multiple values back-to-back: +## let p4 = payload("v1", 1'i64, body) +## ``` + +{.push raises: [].} + +import std/macros +import ./keys + +export keys.encodePart + +proc toPayload*[T](v: T): seq[byte] = + ## Single-value payload constructor. Equivalent to `payload(v)`. + var buf: seq[byte] = @[] + encodePart(buf, v) + return buf + +macro payload*(parts: varargs[typed]): seq[byte] = + ## Variadic payload builder. Same encoder as `key(...)`; only the return + ## type differs. + let bufSym = genSym(nskVar, "payloadBuf") + var body = newStmtList() + body.add quote do: + var `bufSym`: seq[byte] = @[] + for p in parts: + body.add quote do: + encodePart(`bufSym`, `p`) + body.add bufSym + return newBlockStmt(body) + +{.pop.} diff --git a/waku/persistency/persistency.nim b/waku/persistency/persistency.nim new file mode 100644 index 000000000..916f3ac8b --- /dev/null +++ b/waku/persistency/persistency.nim @@ -0,0 +1,433 @@ +## Public facade and main driver types for the persistency library. +## +## ``Persistency`` is the per-root coordinator; one instance owns one +## directory and any number of named jobs. ``Job`` is the per-job handle: +## one tenant, one DB file, one worker thread, one BrokerContext. +## +## ## Two ways to drive a job +## +## **By Job ref** — capture the handle from `openJob` and call methods on +## it. Cheapest, no map lookup per call: +## +## ```nim +## let p = Persistency.instance("/var/lib/wakustore").get() +## let j = p.openJob("alpha").get() +## await j.persistPut("msg", k, payload) +## let v = await j.get("msg", k) +## ``` +## +## **By job id string** — useful when the caller doesn't want to thread +## the ``Job`` ref around (config-driven services, RPC dispatchers). The +## Job must still have been opened previously; the string-form procs look +## it up in `Persistency.jobs`: +## +## ```nim +## discard p.openJob("alpha") +## await p.persistPut("alpha", "msg", k, payload) # logs and resolves if not open +## let v = await p.get("alpha", "msg", k) # Result, peJobNotFound if missing +## ``` +## +## ## Drain semantics +## +## Writes return a ``Future[void]`` that resolves once the PersistEvent +## has been pushed onto the worker thread's channel — **not** once the +## SQL has run. The listener is still fire-and-forget on the SQL side, so +## a read issued immediately after an awaited write is still racy by +## design in v1. To bridge the race: +## * use ``deleteAcked`` (it round-trips through the read path), or +## * poll ``exists`` until it returns true, or +## * yield with ``await sleepAsync(...)``. + +{.push raises: [].} + +import std/[locks, options, os, sequtils, tables] +import chronos, chronicles, results +import brokers/[event_broker, request_broker, broker_context] +import ./[types, keys, payload, backend_comm, backend_thread] + +export types, keys, payload + +logScope: + topics = "persistency" + +const DefaultStoragePath* = "./data" + +# ── Driver types ──────────────────────────────────────────────────────── + +type + Job* = ref object + ## Per-job handle. Owns its BrokerContext and the worker thread that + ## services it. Created and torn down via `Persistency.openJob` / + ## `Persistency.closeJob`. + id*: string + context*: BrokerContext + runtime: JobRuntime ## internal — managed by openJob/closeJob + running*: bool + + Persistency* = ref object + ## Per-root coordinator. One Persistency instance manages a directory + ## of per-job SQLite files at ``rootDir/.db``. + rootDir*: string + jobs*: Table[string, Job] + +# ── Singleton state ───────────────────────────────────────────────────── +# +# Persistency is a process-wide singleton: one rootDir at a time. The +# `instance` factory is the only public constructor; `new` below is +# private and skips the singleton bookkeeping (used internally and never +# called twice with conflicting rootDirs). + +var + gPersistency {.global.}: Persistency + gPersistencyLock {.global.}: Lock + +once: + gPersistencyLock.initLock() + +# ── Lifecycle ─────────────────────────────────────────────────────────── + +proc dbPathFor(p: Persistency, jobId: string): string = + p.rootDir / (jobId & ".db") + +proc new(T: type Persistency, rootDir: string): Result[T, PersistencyError] = + ## Private. Build a Persistency value without touching the singleton + ## slot. Validates ``rootDir`` but does **not** create it — directory + ## materialisation is deferred to the first ``openJob`` call. Semantics: + ## + ## * If ``rootDir`` is empty, returns ``peInvalidArgument``. + ## * If ``rootDir`` exists and is a directory, accept it. + ## * If ``rootDir`` exists but is not a directory, returns + ## ``peInvalidArgument``. + ## * If ``rootDir`` does not exist, walk up the parent chain: the first + ## existing ancestor must be a directory; otherwise returns + ## ``peInvalidArgument``. This catches "obviously broken" paths early + ## without actually touching the filesystem. + if rootDir.len == 0: + return err(persistencyErr(peInvalidArgument, "rootDir is empty")) + if fileExists(rootDir) and not dirExists(rootDir): + return err( + persistencyErr( + peInvalidArgument, "rootDir exists and is not a directory: " & rootDir + ) + ) + if not dirExists(rootDir): + var parent = parentDir(rootDir) + while parent.len > 0 and not dirExists(parent): + if fileExists(parent): + return err( + persistencyErr( + peInvalidArgument, + "rootDir ancestor exists and is not a directory: " & parent, + ) + ) + parent = parentDir(parent) + return ok(T(rootDir: rootDir, jobs: initTable[string, Job]())) + +proc ensureRootDir(p: Persistency): Result[void, PersistencyError] = + ## Materialise ``rootDir`` on demand. Idempotent; called from + ## ``openJob`` so an unused Persistency leaves no directory behind. + if dirExists(p.rootDir): + return ok() + try: + createDir(p.rootDir) + except OSError, IOError: + return + err(persistencyErr(peBackend, "createDir failed: " & getCurrentExceptionMsg())) + return ok() + +proc reset*(T: type Persistency) {.gcsafe.} = + ## Tear down the singleton: close every open job, clear the Teardown + ## provider, and free the slot so a subsequent ``Persistency.instance`` + ## starts fresh. Idempotent. Tests use this in `defer`;. + {.cast(gcsafe).}: + acquire(gPersistencyLock) + defer: + release(gPersistencyLock) + if gPersistency != nil: + let p = gPersistency + gPersistency = nil + p.close() + +proc instance*( + T: type Persistency, rootDir: string +): Result[T, PersistencyError] {.gcsafe.} = + ## Get-or-init the process-wide Persistency singleton. + ## + ## * First call: validates ``rootDir`` (without creating it) and + ## registers the Teardown handler. The directory itself is created + ## lazily by the first ``openJob`` call, so a Persistency that never + ## opens a job leaves no filesystem footprint. + ## * Later calls with the same ``rootDir``: returns the live instance + ## (idempotent). + ## * Later calls with a different ``rootDir``: returns + ## ``peInvalidArgument`` — the singleton can only be re-targeted via + ## ``Persistency.reset`` (or by the Teardown shutdown flow). + {.cast(gcsafe).}: + acquire(gPersistencyLock) + defer: + release(gPersistencyLock) + + if gPersistency != nil: + if gPersistency.rootDir == rootDir: + return ok(gPersistency) + return err( + persistencyErr( + peInvalidArgument, + "Persistency already initialised with rootDir " & gPersistency.rootDir & + "; cannot re-init with " & rootDir, + ) + ) + + let p = ?Persistency.new(rootDir) + gPersistency = p + return ok(p) + +proc instance*(T: type Persistency): Result[T, PersistencyError] {.gcsafe.} = + ## No-args form: succeeds only if the singleton is already initialised. + ## Use this from services that must not be the first to touch + ## persistency. + {.cast(gcsafe).}: + acquire(gPersistencyLock) + defer: + release(gPersistencyLock) + if gPersistency.isNil: + return err(persistencyErr(peClosed, "Persistency not initialised")) + return ok(gPersistency) + +proc openJob*(p: Persistency, jobId: string): Result[Job, PersistencyError] = + ## Open-or-create a job under this Persistency. + ## + ## * If the job is already open in this process, the existing ``Job`` + ## ref is returned (idempotent). + ## * Otherwise ``rootDir`` is materialised on demand (created with + ## missing parents on first use; no-op on subsequent calls), a worker + ## thread is spawned, and the SQLite file at + ## ``/.db`` is opened. If the file does not exist it + ## is created and the schema initialised; if it already exists it is + ## reopened in place and its data is preserved. + let existing = p.jobs.getOrDefault(jobId, nil) + if existing != nil: + return ok(existing) + + ?p.ensureRootDir() + + let ctx = NewBrokerContext() + let rt = ?startStorageThread(ctx, dbPathFor(p, jobId)) + let job = Job(id: jobId, context: ctx, runtime: rt, running: true) + p.jobs[jobId] = job + return ok(job) + +proc closeJob*(p: Persistency, jobId: string) = + ## Stop the worker, join its thread, and forget the job. No-op if the + ## job isn't open. + let job = p.jobs.getOrDefault(jobId, nil) + if job == nil: + return + stopStorageThread(job.runtime) + job.runtime = nil + job.running = false + p.jobs.del(jobId) + +proc close*(p: Persistency) = + ## Close every open job. Idempotent. + var ids: seq[string] + for id in p.jobs.keys: + ids.add(id) + for id in ids: + p.closeJob(id) + +proc dropJob*(p: Persistency, jobId: string) = + ## Close the job if open, then delete its DB file (plus -wal / -shm + ## sidecars). Best-effort: a missing file is not an error. + p.closeJob(jobId) + let path = dbPathFor(p, jobId) + for suffix in ["", "-wal", "-shm"]: + try: + removeFile(path & suffix) + except OSError, IOError: + discard + +# ── String lookup ─────────────────────────────────────────────────────── + +proc job*(p: Persistency, jobId: string): Result[Job, PersistencyError] = + ## Look up an already-open job. Returns ``peJobNotFound`` if no such + ## job has been opened (``openJob`` first). + let j = p.jobs.getOrDefault(jobId, nil) + if j != nil: + return ok(j) + else: + return err(persistencyErr(peJobNotFound, "no open job with id: " & jobId)) + +proc `[]`*(p: Persistency, jobId: string): Job {.raises: [KeyError].} = + ## Subscript sugar for `job` — raises ``KeyError`` if the job isn't + ## open. Prefer `job(p, id)` when you want a typed error. + p.jobs[jobId] + +proc hasJob*(p: Persistency, jobId: string): bool {.inline.} = + p.jobs.hasKey(jobId) + +# ── Writes (fire-and-forget) — Job form ───────────────────────────────── + +proc persist*(t: Job, ops: seq[TxOp]): Future[void] {.async.} = + ## Emit a batched persist event. The handler treats >1 ops as a single + ## BEGIN IMMEDIATE/COMMIT transaction (see backend_sqlite.applyOps). + await PersistEvent.emit(t.context, PersistEvent(ops: ops)) + +proc persist*(t: Job, op: TxOp): Future[void] {.async.} = + await persist(t, @[op]) + +proc persistPut*( + t: Job, category: string, key: Key, payload: seq[byte] +): Future[void] {.async.} = + await persist(t, TxOp(category: category, key: key, kind: txPut, payload: payload)) + +proc persistDelete*(t: Job, category: string, key: Key): Future[void] {.async.} = + await persist(t, TxOp(category: category, key: key, kind: txDelete)) + +proc persistEncoded*[T]( + t: Job, category: string, key: Key, value: T +): Future[void] {.async.} = + ## Convenience: encode `value` via `toPayload` and put it. Use the raw + ## `persistPut(..., seq[byte])` form when you already have bytes + ## (e.g. an externally-produced CBOR blob). + await persistPut(t, category, key, toPayload(value)) + +# ── Writes (fire-and-forget) — string-lookup form ─────────────────────── +# +# These look up the Job by id and dispatch. If the job isn't open we log +# a warning and drop the write — consistent with the fire-and-forget +# contract; the caller has no return channel to inspect. + +proc jobOrWarn(p: Persistency, jobId: string): Job = + ## Lookup helper for the fire-and-forget write paths. Returns nil and + ## logs a warning if the job isn't open. Isolated as a non-generic proc + ## so chronicles' `warn` macro expands cleanly (it doesn't, when called + ## from inside a generic proc's body). + let job = p.jobs.getOrDefault(jobId, nil) + if job.isNil(): + warn "persistency: write dropped, job not open", jobId + return job + +template withJobOrWarn(p: Persistency, jobId: string, j, body: untyped) = + let `j` = p.jobOrWarn(jobId) + if not `j`.isNil(): + body + +proc persist*(p: Persistency, jobId: string, ops: seq[TxOp]): Future[void] {.async.} = + let j = p.jobOrWarn(jobId) + if not j.isNil(): + await j.persist(ops) + +proc persist*(p: Persistency, jobId: string, op: TxOp): Future[void] {.async.} = + await p.persist(jobId, @[op]) + +proc persistPut*( + p: Persistency, jobId: string, category: string, key: Key, payload: seq[byte] +): Future[void] {.async.} = + let j = p.jobOrWarn(jobId) + if not j.isNil(): + await j.persistPut(category, key, payload) + +proc persistDelete*( + p: Persistency, jobId: string, category: string, key: Key +): Future[void] {.async.} = + let j = p.jobOrWarn(jobId) + if not j.isNil(): + await j.persistDelete(category, key) + +proc persistEncoded*[T]( + p: Persistency, jobId: string, category: string, key: Key, value: T +): Future[void] {.async.} = + let j = p.jobOrWarn(jobId) + if not j.isNil(): + await j.persistEncoded(category, key, value) + +# ── Reads (async, typed errors) — Job form ────────────────────────────── + +template liftErr(s: string): PersistencyError = + decodeErr(s) + +proc get*( + t: Job, category: string, key: Key +): Future[Result[Option[seq[byte]], PersistencyError]] {.async.} = + let r = (await KvGet.request(t.context, category, key)).valueOr: + return err(liftErr(error)) + return ok(r.value) + +proc exists*( + t: Job, category: string, key: Key +): Future[Result[bool, PersistencyError]] {.async.} = + let r = (await KvExists.request(t.context, category, key)).valueOr: + return err(liftErr(error)) + return ok(r.value) + +proc scan*( + t: Job, category: string, range: KeyRange, reverse = false +): Future[Result[seq[KvRow], PersistencyError]] {.async.} = + let r = (await KvScan.request(t.context, category, range, reverse)).valueOr: + return err(liftErr(error)) + return ok(r.rows) + +proc scanPrefix*( + t: Job, category: string, prefix: Key, reverse = false +): Future[Result[seq[KvRow], PersistencyError]] {.async.} = + let rng = prefixRange(prefix) + let r = (await KvScan.request(t.context, category, rng, reverse)).valueOr: + return err(liftErr(error)) + return ok(r.rows) + +proc count*( + t: Job, category: string, range: KeyRange +): Future[Result[int, PersistencyError]] {.async.} = + let r = (await KvCount.request(t.context, category, range)).valueOr: + return err(liftErr(error)) + return ok(r.n) + +proc deleteAcked*( + t: Job, category: string, key: Key +): Future[Result[bool, PersistencyError]] {.async.} = + ## Goes through the read path so the caller learns whether a row was + ## actually removed. + let r = (await KvDelete.request(t.context, category, key)).valueOr: + return err(liftErr(error)) + return ok(r.existed) + +# ── Reads (async, typed errors) — string-lookup form ──────────────────── + +proc get*( + p: Persistency, jobId: string, category: string, key: Key +): Future[Result[Option[seq[byte]], PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.get(category, key) + +proc exists*( + p: Persistency, jobId: string, category: string, key: Key +): Future[Result[bool, PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.exists(category, key) + +proc scan*( + p: Persistency, jobId: string, category: string, range: KeyRange, reverse = false +): Future[Result[seq[KvRow], PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.scan(category, range, reverse) + +proc scanPrefix*( + p: Persistency, jobId: string, category: string, prefix: Key, reverse = false +): Future[Result[seq[KvRow], PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.scanPrefix(category, prefix, reverse) + +proc count*( + p: Persistency, jobId: string, category: string, range: KeyRange +): Future[Result[int, PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.count(category, range) + +proc deleteAcked*( + p: Persistency, jobId: string, category: string, key: Key +): Future[Result[bool, PersistencyError]] {.async.} = + let j = ?p.job(jobId) + return await j.deleteAcked(category, key) + +{.pop.} diff --git a/waku/persistency/schema.nim b/waku/persistency/schema.nim new file mode 100644 index 000000000..1d13014f2 --- /dev/null +++ b/waku/persistency/schema.nim @@ -0,0 +1,58 @@ +## SQL schema and pragma setup for the persistency library. +## +## Single uniform schema per job DB file: +## kv(category BLOB, key BLOB, payload BLOB) PRIMARY KEY (category, key) +## WITHOUT ROWID +## +## category is declared BLOB (not TEXT) so it round-trips via the existing +## sqlite3_abi binding helpers (which do not yet expose bind_text). SQLite +## compares BLOBs byte-wise, which is exactly the ordering we want. + +{.push raises: [].} + +import results +import ../common/databases/[common, db_sqlite] + +const + PersistencyUserVersion* = 1'i64 + + CreateKvTableSql* = """ + CREATE TABLE IF NOT EXISTS kv ( + category BLOB NOT NULL, + key BLOB NOT NULL, + payload BLOB NOT NULL, + PRIMARY KEY (category, key) + ) WITHOUT ROWID; + """ + + ApplyPragmasSql* = """ + PRAGMA synchronous = NORMAL; + PRAGMA temp_store = MEMORY; + PRAGMA busy_timeout = 5000; + PRAGMA foreign_keys = OFF; + """ + +proc applyPragmas*(db: SqliteDatabase): DatabaseResult[void] = + ## Apply the connection-level pragmas. journal_mode=WAL is already set by + ## SqliteDatabase.new. + for stmt in [ + "PRAGMA synchronous = NORMAL;", "PRAGMA temp_store = MEMORY;", + "PRAGMA busy_timeout = 5000;", "PRAGMA foreign_keys = OFF;", + ]: + db.query(stmt, NoopRowHandler).isOkOr: + return err("pragma failed: " & stmt & ": " & error) + return ok() + +proc ensureSchema*(db: SqliteDatabase): DatabaseResult[void] = + db.query(CreateKvTableSql, NoopRowHandler).isOkOr: + return err("create kv table failed: " & error) + + let userVersion = ?db.getUserVersion() + if userVersion == 0: + ?db.setUserVersion(PersistencyUserVersion) + elif userVersion != PersistencyUserVersion: + return err( + "incompatible persistency user_version: got " & $userVersion & ", expected " & + $PersistencyUserVersion + ) + return ok() diff --git a/waku/persistency/types.nim b/waku/persistency/types.nim new file mode 100644 index 000000000..4c4c2de3f --- /dev/null +++ b/waku/persistency/types.nim @@ -0,0 +1,81 @@ +## Core types for the logos-delivery persistency library. +## +## The library is backend-neutral CRUD: jobs own their domain ports and +## map them onto the primitives exposed in persistency.nim. See +## persistency.nim for the public facade and brokers.nim for the +## cross-thread plumbing. + +{.push raises: [].} + +type + Key* = distinct seq[byte] + + KeyRange* = object + start*: Key + stop*: Key ## exclusive; an empty `stop` means "no upper bound" + + KvRow* = tuple[key: Key, payload: seq[byte]] + + TxOpKind* = enum + txPut + txDelete + + TxOp* = object + category*: string + key*: Key + case kind*: TxOpKind + of txPut: + payload*: seq[byte] + of txDelete: + discard + + PersistencyErrorKind* = enum + peBackend + peClosed + peInvalidArgument + peTimeout + peJobNotFound + + PersistencyError* = object + kind*: PersistencyErrorKind + msg*: string + backendCode*: int + +proc bytes*(k: Key): lent seq[byte] {.inline.} = + seq[byte](k) + +proc len*(k: Key): int {.inline.} = + seq[byte](k).len + +proc `==`*(a, b: Key): bool {.inline.} = + seq[byte](a) == seq[byte](b) + +proc `<`*(a, b: Key): bool = + let ab = seq[byte](a) + let bb = seq[byte](b) + let n = min(ab.len, bb.len) + for i in 0 ..< n: + if ab[i] != bb[i]: + return ab[i] < bb[i] + return ab.len < bb.len + +proc `<=`*(a, b: Key): bool {.inline.} = + a == b or a < b + +proc rawKey*(b: openArray[byte]): Key = + var s = newSeq[byte](b.len) + for i, v in b: + s[i] = v + return Key(s) + +proc rawKey*(b: sink seq[byte]): Key {.inline.} = + Key(b) + +proc persistencyErr*( + kind: PersistencyErrorKind, msg: string, backendCode = 0 +): PersistencyError {.inline.} = + PersistencyError(kind: kind, msg: msg, backendCode: backendCode) + +proc `$`*(e: PersistencyError): string = + "PersistencyError(" & $e.kind & ": " & e.msg & + (if e.backendCode != 0: ", code=" & $e.backendCode else: "") & ")" diff --git a/waku/requests/health_requests.nim b/waku/requests/health_requests.nim new file mode 100644 index 000000000..d48b3278f --- /dev/null +++ b/waku/requests/health_requests.nim @@ -0,0 +1,51 @@ +import brokers/request_broker + +import waku/api/types +import waku/node/health_monitor/[protocol_health, topic_health, health_report] +import waku/waku_core/topics +import waku/common/waku_protocol + +export protocol_health, topic_health + +# Get the overall node connectivity status +RequestBroker(sync): + type RequestConnectionStatus* = object + connectionStatus*: ConnectionStatus + +# Get the health status of a set of content topics +RequestBroker(sync): + type RequestContentTopicsHealth* = object + contentTopicHealth*: seq[tuple[topic: ContentTopic, health: TopicHealth]] + + proc signature(topics: seq[ContentTopic]): Result[RequestContentTopicsHealth, string] + +# Get a consolidated node health report +RequestBroker: + type RequestHealthReport* = object + healthReport*: HealthReport + +# Get the health status of a set of shards (pubsub topics) +RequestBroker(sync): + type RequestShardTopicsHealth* = object + topicHealth*: seq[tuple[topic: PubsubTopic, health: TopicHealth]] + + proc signature(topics: seq[PubsubTopic]): Result[RequestShardTopicsHealth, string] + +# Get the health status of a mounted protocol +RequestBroker: + type RequestProtocolHealth* = object + healthStatus*: ProtocolHealth + + proc signature(protocol: WakuProtocol): Future[Result[RequestProtocolHealth, string]] + +# Get edge filter health for a single shard (set by DeliveryService when edge mode is active) +RequestBroker(sync): + type RequestEdgeShardHealth* = object + health*: TopicHealth + + proc signature(shard: PubsubTopic): Result[RequestEdgeShardHealth, string] + +# Get edge filter confirmed peer count (set by DeliveryService when edge mode is active) +RequestBroker(sync): + type RequestEdgeFilterPeerCount* = object + peerCount*: int diff --git a/waku/requests/node_requests.nim b/waku/requests/node_requests.nim new file mode 100644 index 000000000..93c6b1159 --- /dev/null +++ b/waku/requests/node_requests.nim @@ -0,0 +1,11 @@ +import std/options +import brokers/[request_broker, multi_request_broker] +import waku/waku_core/[topics] + +RequestBroker(sync): + type RequestRelayShard* = object + relayShard*: RelayShard + + proc signature( + pubsubTopic: Option[PubsubTopic], contentTopic: ContentTopic + ): Result[RequestRelayShard, string] diff --git a/waku/requests/requests.nim b/waku/requests/requests.nim new file mode 100644 index 000000000..9225c0f3e --- /dev/null +++ b/waku/requests/requests.nim @@ -0,0 +1,3 @@ +import ./[health_requests, rln_requests, node_requests] + +export health_requests, rln_requests, node_requests diff --git a/waku/requests/rln_requests.nim b/waku/requests/rln_requests.nim new file mode 100644 index 000000000..ffd747bed --- /dev/null +++ b/waku/requests/rln_requests.nim @@ -0,0 +1,10 @@ +import brokers/request_broker +import waku/waku_core/message/message + +RequestBroker: + type RequestGenerateRlnProof* = object + proof*: seq[byte] + + proc signature( + message: WakuMessage, senderEpoch: float64 + ): Future[Result[RequestGenerateRlnProof, string]] {.async.} diff --git a/waku/waku_api/rest/admin/client.nim b/waku/rest_api/endpoint/admin/client.nim similarity index 100% rename from waku/waku_api/rest/admin/client.nim rename to waku/rest_api/endpoint/admin/client.nim diff --git a/waku/waku_api/rest/admin/handlers.nim b/waku/rest_api/endpoint/admin/handlers.nim similarity index 97% rename from waku/waku_api/rest/admin/handlers.nim rename to waku/rest_api/endpoint/admin/handlers.nim index 172172376..304fdabf8 100644 --- a/waku/waku_api/rest/admin/handlers.nim +++ b/waku/rest_api/endpoint/admin/handlers.nim @@ -12,7 +12,6 @@ import waku/[ waku_core, waku_core/topics/pubsub_topic, - waku_store_legacy/common, waku_store/common, waku_filter_v2, waku_lightpush_legacy/common, @@ -172,7 +171,7 @@ proc installAdminV1GetPeersHandler(router: var RestRouter, node: WakuNode) = let peers = populateAdminPeerInfoForCodecs( node, @[ - WakuRelayCodec, WakuFilterSubscribeCodec, WakuStoreCodec, WakuLegacyStoreCodec, + WakuRelayCodec, WakuFilterSubscribeCodec, WakuStoreCodec, WakuLegacyLightPushCodec, WakuLightPushCodec, WakuPeerExchangeCodec, WakuReconciliationCodec, WakuTransferCodec, ], @@ -345,7 +344,7 @@ proc installAdminV1GetPeersHandler(router: var RestRouter, node: WakuNode) = for ps in relayPeers: totalRelayPeers += ps.peers.len stat[$ps.shard] = ps.peers.len - stat["Total relay peers"] = relayPeers.len + stat["Total relay peers"] = totalRelayPeers stat # stats of mesh peers @@ -356,7 +355,7 @@ proc installAdminV1GetPeersHandler(router: var RestRouter, node: WakuNode) = for ps in meshPeers: totalMeshPeers += ps.peers.len stat[$ps.shard] = ps.peers.len - stat["Total mesh peers"] = meshPeers.len + stat["Total mesh peers"] = totalMeshPeers stat var protoStats = initOrderedTable[string, int]() @@ -366,8 +365,6 @@ proc installAdminV1GetPeersHandler(router: var RestRouter, node: WakuNode) = protoStats[WakuFilterPushCodec] = peers.countIt(it.protocols.contains(WakuFilterPushCodec)) protoStats[WakuStoreCodec] = peers.countIt(it.protocols.contains(WakuStoreCodec)) - protoStats[WakuLegacyStoreCodec] = - peers.countIt(it.protocols.contains(WakuLegacyStoreCodec)) protoStats[WakuLightPushCodec] = peers.countIt(it.protocols.contains(WakuLightPushCodec)) protoStats[WakuLegacyLightPushCodec] = @@ -426,14 +423,13 @@ proc installAdminV1GetFilterSubsHandler(router: var RestRouter, node: WakuNode) FilterSubscription(peerId: $peerId, filterCriteria: filterCriteria) ) - let resp = RestApiResponse.jsonResponse(subscriptions, status = Http200) - if resp.isErr(): - error "An error ocurred while building the json respose: ", error = resp.error + let resp = RestApiResponse.jsonResponse(subscriptions, status = Http200).valueOr: + error "An error ocurred while building the json respose", error = error return RestApiResponse.internalServerError( - fmt("An error ocurred while building the json respose: {resp.error}") + fmt("An error ocurred while building the json respose: {error}") ) - return resp.get() + return resp proc installAdminV1PostLogLevelHandler(router: var RestRouter, node: WakuNode) = router.api(MethodPost, ROUTE_ADMIN_V1_POST_LOG_LEVEL) do( diff --git a/waku/waku_api/rest/admin/types.nim b/waku/rest_api/endpoint/admin/types.nim similarity index 100% rename from waku/waku_api/rest/admin/types.nim rename to waku/rest_api/endpoint/admin/types.nim diff --git a/waku/waku_api/rest/builder.nim b/waku/rest_api/endpoint/builder.nim similarity index 84% rename from waku/waku_api/rest/builder.nim rename to waku/rest_api/endpoint/builder.nim index eb514439f..9b4ecf662 100644 --- a/waku/waku_api/rest/builder.nim +++ b/waku/rest_api/endpoint/builder.nim @@ -5,18 +5,17 @@ import presto import waku/waku_node, waku/discovery/waku_discv5, - waku/waku_api/message_cache, - waku/waku_api/handlers, - waku/waku_api/rest/server, - waku/waku_api/rest/debug/handlers as rest_debug_api, - waku/waku_api/rest/relay/handlers as rest_relay_api, - waku/waku_api/rest/filter/handlers as rest_filter_api, - waku/waku_api/rest/legacy_lightpush/handlers as rest_legacy_lightpush_api, - waku/waku_api/rest/lightpush/handlers as rest_lightpush_api, - waku/waku_api/rest/store/handlers as rest_store_api, - waku/waku_api/rest/legacy_store/handlers as rest_store_legacy_api, - waku/waku_api/rest/health/handlers as rest_health_api, - waku/waku_api/rest/admin/handlers as rest_admin_api, + waku/rest_api/message_cache, + waku/rest_api/handlers, + waku/rest_api/endpoint/server, + waku/rest_api/endpoint/debug/handlers as rest_debug_endpoint, + waku/rest_api/endpoint/relay/handlers as rest_relay_endpoint, + waku/rest_api/endpoint/filter/handlers as rest_filter_endpoint, + waku/rest_api/endpoint/legacy_lightpush/handlers as rest_legacy_lightpush_endpoint, + waku/rest_api/endpoint/lightpush/handlers as rest_lightpush_endpoint, + waku/rest_api/endpoint/store/handlers as rest_store_endpoint, + waku/rest_api/endpoint/health/handlers as rest_health_endpoint, + waku/rest_api/endpoint/admin/handlers as rest_admin_endpoint, waku/waku_core/topics, waku/waku_relay/protocol @@ -28,7 +27,6 @@ import # It will always be called from main thread anyway. # Ref: https://nim-lang.org/docs/manual.html#threads-gc-safety var restServerNotInstalledTab {.threadvar.}: TableRef[string, string] -restServerNotInstalledTab = newTable[string, string]() export WakuRestServerRef @@ -42,6 +40,9 @@ type RestServerConf* = object proc startRestServerEssentials*( nodeHealthMonitor: NodeHealthMonitor, conf: RestServerConf, portsShift: uint16 ): Result[WakuRestServerRef, string] = + if restServerNotInstalledTab.isNil: + restServerNotInstalledTab = newTable[string, string]() + let requestErrorHandler: RestRequestErrorHandler = proc( error: RestRequestError, request: HttpRequestRef ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = @@ -84,13 +85,12 @@ proc startRestServerEssentials*( let address = conf.listenAddress let port = Port(conf.port.uint16 + portsShift) - let server = - ?newRestHttpServer( - address, - port, - allowedOrigin = allowedOrigin, - requestErrorHandler = requestErrorHandler, - ) + let server = ?newRestHttpServer( + address, + port, + allowedOrigin = allowedOrigin, + requestErrorHandler = requestErrorHandler, + ) ## Health REST API installHealthApiHandler(server.router, nodeHealthMonitor) @@ -180,7 +180,7 @@ proc startRestServerProtocolSupport*( else: none(DiscoveryHandler) - rest_filter_api.installFilterRestApiHandlers( + rest_filter_endpoint.installFilterRestApiHandlers( router, node, filterCache, filterDiscoHandler ) else: @@ -193,8 +193,7 @@ proc startRestServerProtocolSupport*( else: none(DiscoveryHandler) - rest_store_api.installStoreApiHandlers(router, node, storeDiscoHandler) - rest_store_legacy_api.installStoreApiHandlers(router, node, storeDiscoHandler) + rest_store_endpoint.installStoreApiHandlers(router, node, storeDiscoHandler) ## Light push API ## Install it either if client is mounted) @@ -208,10 +207,12 @@ proc startRestServerProtocolSupport*( else: none(DiscoveryHandler) - rest_legacy_lightpush_api.installLightPushRequestHandler( + rest_legacy_lightpush_endpoint.installLightPushRequestHandler( + router, node, lightDiscoHandler + ) + rest_lightpush_endpoint.installLightPushRequestHandler( router, node, lightDiscoHandler ) - rest_lightpush_api.installLightPushRequestHandler(router, node, lightDiscoHandler) else: restServerNotInstalledTab["lightpush"] = "/lightpush endpoints are not available." diff --git a/waku/waku_api/rest/client.nim b/waku/rest_api/endpoint/client.nim similarity index 100% rename from waku/waku_api/rest/client.nim rename to waku/rest_api/endpoint/client.nim diff --git a/waku/waku_api/rest/debug/client.nim b/waku/rest_api/endpoint/debug/client.nim similarity index 100% rename from waku/waku_api/rest/debug/client.nim rename to waku/rest_api/endpoint/debug/client.nim diff --git a/waku/waku_api/rest/debug/handlers.nim b/waku/rest_api/endpoint/debug/handlers.nim similarity index 95% rename from waku/waku_api/rest/debug/handlers.nim rename to waku/rest_api/endpoint/debug/handlers.nim index eb1529759..43b6fbbf1 100644 --- a/waku/waku_api/rest/debug/handlers.nim +++ b/waku/rest_api/endpoint/debug/handlers.nim @@ -15,12 +15,11 @@ const ROUTE_DEBUG_INFOV1 = "/debug/v1/info" proc installDebugInfoV1Handler(router: var RestRouter, node: WakuNode) = let getInfo = proc(): RestApiResponse = let info = node.info().toDebugWakuInfo() - let resp = RestApiResponse.jsonResponse(info, status = Http200) - if resp.isErr(): - info "An error occurred while building the json respose", error = resp.error + let resp = RestApiResponse.jsonResponse(info, status = Http200).valueOr: + info "An error occurred while building the json respose", error = error return RestApiResponse.internalServerError() - return resp.get() + return resp # /debug route is deprecated, will be removed router.api(MethodGet, ROUTE_DEBUG_INFOV1) do() -> RestApiResponse: diff --git a/waku/waku_api/rest/debug/types.nim b/waku/rest_api/endpoint/debug/types.nim similarity index 100% rename from waku/waku_api/rest/debug/types.nim rename to waku/rest_api/endpoint/debug/types.nim diff --git a/waku/waku_api/rest/filter/client.nim b/waku/rest_api/endpoint/filter/client.nim similarity index 100% rename from waku/waku_api/rest/filter/client.nim rename to waku/rest_api/endpoint/filter/client.nim diff --git a/waku/waku_api/rest/filter/handlers.nim b/waku/rest_api/endpoint/filter/handlers.nim similarity index 87% rename from waku/waku_api/rest/filter/handlers.nim rename to waku/rest_api/endpoint/filter/handlers.nim index f3f6e4837..61d7eb96f 100644 --- a/waku/waku_api/rest/filter/handlers.nim +++ b/waku/rest_api/endpoint/filter/handlers.nim @@ -49,15 +49,12 @@ func decodeRequestBody[T]( let reqBodyData = contentBody.get().data - let requestResult = decodeFromJsonBytes(T, reqBodyData) - if requestResult.isErr(): + let requestResult = decodeFromJsonBytes(T, reqBodyData).valueOr: return err( - RestApiResponse.badRequest( - "Invalid content body, could not decode. " & $requestResult.error - ) + RestApiResponse.badRequest("Invalid content body, could not decode. " & $error) ) - return ok(requestResult.get()) + return ok(requestResult) proc getStatusDesc( protocolClientRes: filter_protocol_type.FilterSubscribeResult @@ -129,16 +126,15 @@ proc makeRestResponse( httpStatus = convertErrorKindToHttpStatus(protocolClientRes.error().kind) # TODO: convert status codes! - let resp = - RestApiResponse.jsonResponse(filterSubscriptionResponse, status = httpStatus) - - if resp.isErr(): - error "An error ocurred while building the json respose: ", error = resp.error + let resp = RestApiResponse.jsonResponse( + filterSubscriptionResponse, status = httpStatus + ).valueOr: + error "An error ocurred while building the json respose: ", error = error return RestApiResponse.internalServerError( - fmt("An error ocurred while building the json respose: {resp.error}") + fmt("An error ocurred while building the json respose: {error}") ) - return resp.get() + return resp proc makeRestResponse( requestId: string, protocolClientRes: filter_protocol_type.FilterSubscribeError @@ -149,16 +145,15 @@ proc makeRestResponse( let httpStatus = convertErrorKindToHttpStatus(protocolClientRes.kind) # TODO: convert status codes! - let resp = - RestApiResponse.jsonResponse(filterSubscriptionResponse, status = httpStatus) - - if resp.isErr(): - error "An error ocurred while building the json respose: ", error = resp.error + let resp = RestApiResponse.jsonResponse( + filterSubscriptionResponse, status = httpStatus + ).valueOr: + error "An error ocurred while building the json respose: ", error = error return RestApiResponse.internalServerError( - fmt("An error ocurred while building the json respose: {resp.error}") + fmt("An error ocurred while building the json respose: {error}") ) - return resp.get() + return resp const NoPeerNoDiscoError = FilterSubscribeError.serviceUnavailable( "No suitable service peer & no discovery method" @@ -175,18 +170,14 @@ proc filterPostPutSubscriptionRequestHandler( ): Future[RestApiResponse] {.async.} = ## handles any filter subscription requests, adds or modifies. - let decodedBody = decodeRequestBody[FilterSubscribeRequest](contentBody) - - if decodedBody.isErr(): + let req: FilterSubscribeRequest = decodeRequestBody[FilterSubscribeRequest]( + contentBody + ).valueOr: return makeRestResponse( "unknown", - FilterSubscribeError.badRequest( - fmt("Failed to decode request: {decodedBody.error}") - ), + FilterSubscribeError.badRequest(fmt("Failed to decode request: {error}")), ) - let req: FilterSubscribeRequest = decodedBody.value() - let peer = node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: let handler = discHandler.valueOr: return makeRestResponse(req.requestId, NoPeerNoDiscoError) @@ -256,18 +247,14 @@ proc installFilterDeleteSubscriptionsHandler( ## Subscribes a node to a list of contentTopics of a PubSub topic info "delete", ROUTE_FILTER_SUBSCRIPTIONS, contentBody - let decodedBody = decodeRequestBody[FilterUnsubscribeRequest](contentBody) - - if decodedBody.isErr(): + let req: FilterUnsubscribeRequest = decodeRequestBody[FilterUnsubscribeRequest]( + contentBody + ).valueOr: return makeRestResponse( "unknown", - FilterSubscribeError.badRequest( - fmt("Failed to decode request: {decodedBody.error}") - ), + FilterSubscribeError.badRequest(fmt("Failed to decode request: {error}")), ) - let req: FilterUnsubscribeRequest = decodedBody.value() - let peer = node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: let handler = discHandler.valueOr: return makeRestResponse(req.requestId, NoPeerNoDiscoError) @@ -308,18 +295,14 @@ proc installFilterDeleteAllSubscriptionsHandler( ## Subscribes a node to a list of contentTopics of a PubSub topic info "delete", ROUTE_FILTER_ALL_SUBSCRIPTIONS, contentBody - let decodedBody = decodeRequestBody[FilterUnsubscribeAllRequest](contentBody) - - if decodedBody.isErr(): + let req: FilterUnsubscribeAllRequest = decodeRequestBody[ + FilterUnsubscribeAllRequest + ](contentBody).valueOr: return makeRestResponse( "unknown", - FilterSubscribeError.badRequest( - fmt("Failed to decode request: {decodedBody.error}") - ), + FilterSubscribeError.badRequest(fmt("Failed to decode request: {error}")), ) - let req: FilterUnsubscribeAllRequest = decodedBody.value() - let peer = node.peerManager.selectPeer(WakuFilterSubscribeCodec).valueOr: let handler = discHandler.valueOr: return makeRestResponse(req.requestId, NoPeerNoDiscoError) @@ -399,24 +382,20 @@ proc installFilterGetMessagesHandler( ## TODO: ability to specify a return message limit, maybe use cursor to control paging response. info "get", ROUTE_FILTER_MESSAGES, contentTopic = contentTopic - if contentTopic.isErr(): + let contentTopic = contentTopic.valueOr: return RestApiResponse.badRequest("Missing contentTopic") - let contentTopic = contentTopic.get() - - let msgRes = cache.getAutoMessages(contentTopic, clear = true) - if msgRes.isErr(): + let msg = cache.getAutoMessages(contentTopic, clear = true).valueOr: return RestApiResponse.badRequest("Not subscribed to topic: " & contentTopic) - let data = FilterGetMessagesResponse(msgRes.get().map(toFilterWakuMessage)) - let resp = RestApiResponse.jsonResponse(data, status = Http200) - if resp.isErr(): - error "An error ocurred while building the json respose: ", error = resp.error + let data = FilterGetMessagesResponse(msg.map(toFilterWakuMessage)) + let resp = RestApiResponse.jsonResponse(data, status = Http200).valueOr: + error "An error ocurred while building the json respose: ", error = error return RestApiResponse.internalServerError( "An error ocurred while building the json respose" ) - return resp.get() + return resp proc installFilterRestApiHandlers*( router: var RestRouter, diff --git a/waku/waku_api/rest/filter/types.nim b/waku/rest_api/endpoint/filter/types.nim similarity index 100% rename from waku/waku_api/rest/filter/types.nim rename to waku/rest_api/endpoint/filter/types.nim diff --git a/waku/waku_api/rest/health/client.nim b/waku/rest_api/endpoint/health/client.nim similarity index 100% rename from waku/waku_api/rest/health/client.nim rename to waku/rest_api/endpoint/health/client.nim diff --git a/waku/waku_api/rest/health/handlers.nim b/waku/rest_api/endpoint/health/handlers.nim similarity index 100% rename from waku/waku_api/rest/health/handlers.nim rename to waku/rest_api/endpoint/health/handlers.nim diff --git a/waku/waku_api/rest/health/types.nim b/waku/rest_api/endpoint/health/types.nim similarity index 78% rename from waku/waku_api/rest/health/types.nim rename to waku/rest_api/endpoint/health/types.nim index 57f8b284c..88fa736a8 100644 --- a/waku/waku_api/rest/health/types.nim +++ b/waku/rest_api/endpoint/health/types.nim @@ -2,7 +2,8 @@ import results import chronicles, json_serialization, json_serialization/std/options -import ../../../waku_node, ../serdes +import ../serdes +import waku/[waku_node, api/types] #### Serialization and deserialization @@ -44,6 +45,7 @@ proc writeValue*( ) {.raises: [IOError].} = writer.beginRecord() writer.writeField("nodeHealth", $value.nodeHealth) + writer.writeField("connectionStatus", $value.connectionStatus) writer.writeField("protocolsHealth", value.protocolsHealth) writer.endRecord() @@ -52,6 +54,7 @@ proc readValue*( ) {.raises: [SerializationError, IOError].} = var nodeHealth: Option[HealthStatus] + connectionStatus: Option[ConnectionStatus] protocolsHealth: Option[seq[ProtocolHealth]] for fieldName in readObjectFields(reader): @@ -66,6 +69,16 @@ proc readValue*( reader.raiseUnexpectedValue("Invalid `health` value: " & $error) nodeHealth = some(health) + of "connectionStatus": + if connectionStatus.isSome(): + reader.raiseUnexpectedField( + "Multiple `connectionStatus` fields found", "HealthReport" + ) + + let state = ConnectionStatus.init(reader.readValue(string)).valueOr: + reader.raiseUnexpectedValue("Invalid `connectionStatus` value: " & $error) + + connectionStatus = some(state) of "protocolsHealth": if protocolsHealth.isSome(): reader.raiseUnexpectedField( @@ -79,5 +92,8 @@ proc readValue*( if nodeHealth.isNone(): reader.raiseUnexpectedValue("Field `nodeHealth` is missing") - value = - HealthReport(nodeHealth: nodeHealth.get, protocolsHealth: protocolsHealth.get(@[])) + value = HealthReport( + nodeHealth: nodeHealth.get, + connectionStatus: connectionStatus.get, + protocolsHealth: protocolsHealth.get(@[]), + ) diff --git a/waku/waku_api/rest/legacy_lightpush/client.nim b/waku/rest_api/endpoint/legacy_lightpush/client.nim similarity index 100% rename from waku/waku_api/rest/legacy_lightpush/client.nim rename to waku/rest_api/endpoint/legacy_lightpush/client.nim diff --git a/waku/waku_api/rest/legacy_lightpush/handlers.nim b/waku/rest_api/endpoint/legacy_lightpush/handlers.nim similarity index 87% rename from waku/waku_api/rest/legacy_lightpush/handlers.nim rename to waku/rest_api/endpoint/legacy_lightpush/handlers.nim index b129f3ffc..7a3c5b1ed 100644 --- a/waku/waku_api/rest/legacy_lightpush/handlers.nim +++ b/waku/rest_api/endpoint/legacy_lightpush/handlers.nim @@ -50,12 +50,8 @@ proc installLightPushRequestHandler*( ## Send a request to push a waku message info "post", ROUTE_LIGHTPUSH, contentBody - let decodedBody = decodeRequestBody[PushRequest](contentBody) - - if decodedBody.isErr(): - return decodedBody.error() - - let req: PushRequest = decodedBody.value() + let req: PushRequest = decodeRequestBody[PushRequest](contentBody).valueOr: + return error let msg = req.message.toWakuMessage().valueOr: return RestApiResponse.badRequest("Invalid message: " & $error) @@ -80,12 +76,12 @@ proc installLightPushRequestHandler*( error "Failed to request a message push due to timeout!" return RestApiResponse.serviceUnavailable("Push request timed out") - if subFut.value().isErr(): - if subFut.value().error == TooManyRequestsMessage: + subFut.value().isOkOr: + if error == TooManyRequestsMessage: return RestApiResponse.tooManyRequests("Request rate limmit reached") return RestApiResponse.serviceUnavailable( - fmt("Failed to request a message push: {subFut.value().error}") + fmt("Failed to request a message push: {error}") ) return RestApiResponse.ok() diff --git a/waku/waku_api/rest/legacy_lightpush/types.nim b/waku/rest_api/endpoint/legacy_lightpush/types.nim similarity index 100% rename from waku/waku_api/rest/legacy_lightpush/types.nim rename to waku/rest_api/endpoint/legacy_lightpush/types.nim diff --git a/waku/waku_api/rest/lightpush/client.nim b/waku/rest_api/endpoint/lightpush/client.nim similarity index 100% rename from waku/waku_api/rest/lightpush/client.nim rename to waku/rest_api/endpoint/lightpush/client.nim diff --git a/waku/waku_api/rest/lightpush/handlers.nim b/waku/rest_api/endpoint/lightpush/handlers.nim similarity index 100% rename from waku/waku_api/rest/lightpush/handlers.nim rename to waku/rest_api/endpoint/lightpush/handlers.nim diff --git a/waku/waku_api/rest/lightpush/types.nim b/waku/rest_api/endpoint/lightpush/types.nim similarity index 100% rename from waku/waku_api/rest/lightpush/types.nim rename to waku/rest_api/endpoint/lightpush/types.nim diff --git a/waku/waku_api/rest/origin_handler.nim b/waku/rest_api/endpoint/origin_handler.nim similarity index 98% rename from waku/waku_api/rest/origin_handler.nim rename to waku/rest_api/endpoint/origin_handler.nim index 2317c945f..9752bfb56 100644 --- a/waku/waku_api/rest/origin_handler.nim +++ b/waku/rest_api/endpoint/origin_handler.nim @@ -74,13 +74,12 @@ proc originMiddlewareProc( reqfence: RequestFence, nextHandler: HttpProcessCallback2, ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = - if reqfence.isErr(): + let request = reqfence.valueOr: # Ignore request errors that detected before our middleware. # Let final handler deal with it. return await nextHandler(reqfence) let self = OriginHandlerMiddlewareRef(middleware) - let request = reqfence.get() var reqHeaders = request.headers var response = request.getResponse() diff --git a/waku/waku_api/rest/relay/client.nim b/waku/rest_api/endpoint/relay/client.nim similarity index 100% rename from waku/waku_api/rest/relay/client.nim rename to waku/rest_api/endpoint/relay/client.nim diff --git a/waku/waku_api/rest/relay/handlers.nim b/waku/rest_api/endpoint/relay/handlers.nim similarity index 95% rename from waku/waku_api/rest/relay/handlers.nim rename to waku/rest_api/endpoint/relay/handlers.nim index f59c445a8..4a1415361 100644 --- a/waku/waku_api/rest/relay/handlers.nim +++ b/waku/rest_api/endpoint/relay/handlers.nim @@ -126,29 +126,25 @@ proc installRelayApiHandlers*( # ## TODO: ability to specify a return message limit # info "get_waku_v2_relay_v1_messages", topic=topic - if pubsubTopic.isErr(): + let pubSubTopic = pubsubTopic.valueOr: return RestApiResponse.badRequest() - let pubSubTopic = pubsubTopic.get() - let messages = cache.getMessages(pubSubTopic, clear = true) - if messages.isErr(): + let messages = cache.getMessages(pubSubTopic, clear = true).valueOr: info "Not subscribed to topic", topic = pubSubTopic return RestApiResponse.notFound() - let data = RelayGetMessagesResponse(messages.get().map(toRelayWakuMessage)) - let resp = RestApiResponse.jsonResponse(data, status = Http200) - if resp.isErr(): - info "An error ocurred while building the json respose", error = resp.error + let data = RelayGetMessagesResponse(messages.map(toRelayWakuMessage)) + let resp = RestApiResponse.jsonResponse(data, status = Http200).valueOr: + info "An error ocurred while building the json respose", error = error return RestApiResponse.internalServerError() - return resp.get() + return resp router.api(MethodPost, ROUTE_RELAY_MESSAGESV1) do( pubsubTopic: string, contentBody: Option[ContentBody] ) -> RestApiResponse: - if pubsubTopic.isErr(): + let pubSubTopic = pubsubTopic.valueOr: return RestApiResponse.badRequest() - let pubSubTopic = pubsubTopic.get() # ensure the node is subscribed to the topic. otherwise it risks publishing # to a topic with no connected peers @@ -318,9 +314,7 @@ proc installRelayApiHandlers*( if not await publishFut.withTimeout(futTimeout): return RestApiResponse.internalServerError("Failed to publish: timedout") - var res = publishFut.read() - - if res.isErr(): - return RestApiResponse.badRequest("Failed to publish. " & res.error) + publishFut.read().isOkOr: + return RestApiResponse.badRequest("Failed to publish: " & error) return RestApiResponse.ok() diff --git a/waku/waku_api/rest/relay/types.nim b/waku/rest_api/endpoint/relay/types.nim similarity index 100% rename from waku/waku_api/rest/relay/types.nim rename to waku/rest_api/endpoint/relay/types.nim diff --git a/waku/waku_api/rest/responses.nim b/waku/rest_api/endpoint/responses.nim similarity index 100% rename from waku/waku_api/rest/responses.nim rename to waku/rest_api/endpoint/responses.nim diff --git a/waku/waku_api/rest/rest_serdes.nim b/waku/rest_api/endpoint/rest_serdes.nim similarity index 90% rename from waku/waku_api/rest/rest_serdes.nim rename to waku/rest_api/endpoint/rest_serdes.nim index 1b6d5a98d..8dcb7c8f1 100644 --- a/waku/waku_api/rest/rest_serdes.nim +++ b/waku/rest_api/endpoint/rest_serdes.nim @@ -45,15 +45,12 @@ func decodeRequestBody*[T]( let reqBodyData = contentBody.get().data - let requestResult = decodeFromJsonBytes(T, reqBodyData) - if requestResult.isErr(): + let requestResult = decodeFromJsonBytes(T, reqBodyData).valueOr: return err( - RestApiResponse.badRequest( - "Invalid content body, could not decode. " & $requestResult.error - ) + RestApiResponse.badRequest("Invalid content body, could not decode: " & $error) ) - return ok(requestResult.get()) + return ok(requestResult) proc decodeBytes*( t: typedesc[string], value: openarray[byte], contentType: Opt[ContentTypeData] diff --git a/waku/waku_api/rest/serdes.nim b/waku/rest_api/endpoint/serdes.nim similarity index 96% rename from waku/waku_api/rest/serdes.nim rename to waku/rest_api/endpoint/serdes.nim index 147184602..ab7ed8d25 100644 --- a/waku/waku_api/rest/serdes.nim +++ b/waku/rest_api/endpoint/serdes.nim @@ -117,8 +117,4 @@ proc encodeString*(value: SomeUnsignedInt): SerdesResult[string] = ok(Base10.toString(value)) proc decodeString*(T: typedesc[SomeUnsignedInt], value: string): SerdesResult[T] = - let v = Base10.decode(T, value) - if v.isErr(): - return err(v.error()) - else: - return ok(v.get()) + return Base10.decode(T, value) diff --git a/waku/waku_api/rest/server.nim b/waku/rest_api/endpoint/server.nim similarity index 98% rename from waku/waku_api/rest/server.nim rename to waku/rest_api/endpoint/server.nim index e5db5ee5e..44a02ccb2 100644 --- a/waku/waku_api/rest/server.nim +++ b/waku/rest_api/endpoint/server.nim @@ -91,7 +91,7 @@ proc new*( ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} = discard - server.httpServer = HttpServerRef.new( + server.httpServer = ?HttpServerRef.new( address, defaultProcessCallback, serverFlags, @@ -106,8 +106,7 @@ proc new*( maxRequestBodySize, dualstack = dualstack, middlewares = middlewares, - ).valueOr: - return err(error) + ) return ok(server) proc getRouter(): RestRouter = diff --git a/waku/waku_api/rest/store/client.nim b/waku/rest_api/endpoint/store/client.nim similarity index 97% rename from waku/waku_api/rest/store/client.nim rename to waku/rest_api/endpoint/store/client.nim index 80939ee25..71ba7610d 100644 --- a/waku/waku_api/rest/store/client.nim +++ b/waku/rest_api/endpoint/store/client.nim @@ -57,7 +57,7 @@ proc getStoreMessagesV3*( # Optional cursor fields cursor: string = "", # base64-encoded hash ascending: string = "", - pageSize: string = "", + pageSize: string = "20", # default value is 20 ): RestResponse[StoreQueryResponseHex] {. rest, endpoint: "/store/v3/messages", meth: HttpMethod.MethodGet .} diff --git a/waku/waku_api/rest/store/handlers.nim b/waku/rest_api/endpoint/store/handlers.nim similarity index 93% rename from waku/waku_api/rest/store/handlers.nim rename to waku/rest_api/endpoint/store/handlers.nim index cf0e96710..7d37191fb 100644 --- a/waku/waku_api/rest/store/handlers.nim +++ b/waku/rest_api/endpoint/store/handlers.nim @@ -1,6 +1,7 @@ {.push raises: [].} -import std/strformat, results, chronicles, uri, json_serialization, presto/route +import + std/[strformat, sugar], results, chronicles, uri, json_serialization, presto/route import ../../../waku_core, ../../../waku_store/common, @@ -35,14 +36,10 @@ proc performStoreQuery( error msg return RestApiResponse.internalServerError(msg) - let futRes = queryFut.read() - - if futRes.isErr(): - const msg = "Error occurred in queryFut.read()" - error msg, error = futRes.error - return RestApiResponse.internalServerError(fmt("{msg} [{futRes.error}]")) - - let res = futRes.get().toHex() + let res = queryFut.read().map(val => val.toHex()).valueOr: + const msg = "Error occurred in queryFut.read()" + error msg, error = error + return RestApiResponse.internalServerError(fmt("{msg} [{error}]")) if res.statusCode == uint32(ErrorCode.TOO_MANY_REQUESTS): info "Request rate limit reached on peer ", storePeer @@ -132,6 +129,14 @@ proc createStoreQuery( except CatchableError: return err("page size parsing error: " & getCurrentExceptionMsg()) + # Enforce default value of page_size to 20 + if parsedPagedSize.isNone(): + parsedPagedSize = some(20.uint64) + + # Enforce max value of page_size to 100 + if parsedPagedSize.get() > 100: + parsedPagedSize = some(100.uint64) + return ok( StoreQueryRequest( includeData: parsedIncludeData, diff --git a/waku/waku_api/rest/store/types.nim b/waku/rest_api/endpoint/store/types.nim similarity index 100% rename from waku/waku_api/rest/store/types.nim rename to waku/rest_api/endpoint/store/types.nim diff --git a/waku/waku_api/handlers.nim b/waku/rest_api/handlers.nim similarity index 100% rename from waku/waku_api/handlers.nim rename to waku/rest_api/handlers.nim diff --git a/waku/waku_api/message_cache.nim b/waku/rest_api/message_cache.nim similarity index 100% rename from waku/waku_api/message_cache.nim rename to waku/rest_api/message_cache.nim diff --git a/waku/utils/requests.nim b/waku/utils/requests.nim index 5e5b9d960..d9afd2887 100644 --- a/waku/utils/requests.nim +++ b/waku/utils/requests.nim @@ -7,4 +7,4 @@ import bearssl/rand, stew/byteutils proc generateRequestId*(rng: ref HmacDrbgContext): string = var bytes: array[10, byte] hmacDrbgGenerate(rng[], bytes) - return toHex(bytes) + return byteutils.toHex(bytes) diff --git a/waku/waku_api.nim b/waku/waku_api.nim deleted file mode 100644 index b584bfa2f..000000000 --- a/waku/waku_api.nim +++ /dev/null @@ -1,3 +0,0 @@ -import ./waku_api/message_cache, ./waku_api/rest, ./waku_api/json_rpc - -export message_cache, rest diff --git a/waku/waku_api/rest/legacy_store/client.nim b/waku/waku_api/rest/legacy_store/client.nim deleted file mode 100644 index 24ad38d9a..000000000 --- a/waku/waku_api/rest/legacy_store/client.nim +++ /dev/null @@ -1,75 +0,0 @@ -{.push raises: [].} - -import - chronicles, json_serialization, json_serialization/std/options, presto/[route, client] -import ../../../waku_store_legacy/common, ../serdes, ../responses, ./types - -export types - -logScope: - topics = "waku node rest legacy store_api" - -proc decodeBytes*( - t: typedesc[StoreResponseRest], - data: openArray[byte], - contentType: Opt[ContentTypeData], -): RestResult[StoreResponseRest] = - if MediaType.init($contentType) == MIMETYPE_JSON: - let decoded = ?decodeFromJsonBytes(StoreResponseRest, data) - return ok(decoded) - - if MediaType.init($contentType) == MIMETYPE_TEXT: - var res: string - if len(data) > 0: - res = newString(len(data)) - copyMem(addr res[0], unsafeAddr data[0], len(data)) - - return ok( - StoreResponseRest( - messages: newSeq[StoreWakuMessage](0), - cursor: none(HistoryCursorRest), - # field that contain error information - errorMessage: some(res), - ) - ) - - # If everything goes wrong - return err(cstring("Unsupported contentType " & $contentType)) - -proc getStoreMessagesV1*( - # URL-encoded reference to the store-node - peerAddr: string = "", - pubsubTopic: string = "", - # URL-encoded comma-separated list of content topics - contentTopics: string = "", - startTime: string = "", - endTime: string = "", - - # Optional cursor fields - senderTime: string = "", - storeTime: string = "", - digest: string = "", # base64-encoded digest - pageSize: string = "", - ascending: string = "", -): RestResponse[StoreResponseRest] {. - rest, endpoint: "/store/v1/messages", meth: HttpMethod.MethodGet -.} - -proc getStoreMessagesV1*( - # URL-encoded reference to the store-node - peerAddr: Option[string], - pubsubTopic: string = "", - # URL-encoded comma-separated list of content topics - contentTopics: string = "", - startTime: string = "", - endTime: string = "", - - # Optional cursor fields - senderTime: string = "", - storeTime: string = "", - digest: string = "", # base64-encoded digest - pageSize: string = "", - ascending: string = "", -): RestResponse[StoreResponseRest] {. - rest, endpoint: "/store/v1/messages", meth: HttpMethod.MethodGet -.} diff --git a/waku/waku_api/rest/legacy_store/handlers.nim b/waku/waku_api/rest/legacy_store/handlers.nim deleted file mode 100644 index 96e1da780..000000000 --- a/waku/waku_api/rest/legacy_store/handlers.nim +++ /dev/null @@ -1,251 +0,0 @@ -{.push raises: [].} - -import std/strformat, results, chronicles, uri, json_serialization, presto/route -import - ../../../waku_core, - ../../../waku_store_legacy/common, - ../../../waku_store_legacy/self_req_handler, - ../../../waku_node, - ../../../node/peer_manager, - ../../../common/paging, - ../../handlers, - ../responses, - ../serdes, - ./types - -export types - -logScope: - topics = "waku node rest legacy store_api" - -const futTimeout* = 5.seconds # Max time to wait for futures - -const NoPeerNoDiscError* = - RestApiResponse.preconditionFailed("No suitable service peer & no discovery method") - -# Queries the store-node with the query parameters and -# returns a RestApiResponse that is sent back to the api client. -proc performHistoryQuery( - selfNode: WakuNode, histQuery: HistoryQuery, storePeer: RemotePeerInfo -): Future[RestApiResponse] {.async.} = - let queryFut = selfNode.query(histQuery, storePeer) - if not await queryFut.withTimeout(futTimeout): - const msg = "No history response received (timeout)" - error msg - return RestApiResponse.internalServerError(msg) - - let res = queryFut.read() - if res.isErr(): - const msg = "Error occurred in queryFut.read()" - error msg, error = res.error - return RestApiResponse.internalServerError(fmt("{msg} [{res.error}]")) - - let storeResp = res.value.toStoreResponseRest() - let resp = RestApiResponse.jsonResponse(storeResp, status = Http200) - if resp.isErr(): - const msg = "Error building the json respose" - error msg, error = resp.error - return RestApiResponse.internalServerError(fmt("{msg} [{resp.error}]")) - - return resp.get() - -# Converts a string time representation into an Option[Timestamp]. -# Only positive time is considered a valid Timestamp in the request -proc parseTime(input: Option[string]): Result[Option[Timestamp], string] = - if input.isSome() and input.get() != "": - try: - let time = parseInt(input.get()) - if time > 0: - return ok(some(Timestamp(time))) - except ValueError: - return err("Problem parsing time [" & getCurrentExceptionMsg() & "]") - - return ok(none(Timestamp)) - -# Generates a history query cursor as per the given params -proc parseCursor( - parsedPubsubTopic: Option[string], - senderTime: Option[string], - storeTime: Option[string], - digest: Option[string], -): Result[Option[HistoryCursor], string] = - # Parse sender time - let parsedSenderTime = parseTime(senderTime).valueOr: - return err(error) - - # Parse store time - let parsedStoreTime = parseTime(storeTime).valueOr: - return err(error) - - # Parse message digest - let parsedMsgDigest = parseMsgDigest(digest).valueOr: - return err(error) - - # Parse cursor information - if parsedPubsubTopic.isSome() and parsedSenderTime.isSome() and - parsedStoreTime.isSome() and parsedMsgDigest.isSome(): - return ok( - some( - HistoryCursor( - pubsubTopic: parsedPubsubTopic.get(), - senderTime: parsedSenderTime.get(), - storeTime: parsedStoreTime.get(), - digest: parsedMsgDigest.get(), - ) - ) - ) - else: - return ok(none(HistoryCursor)) - -# Creates a HistoryQuery from the given params -proc createHistoryQuery( - pubsubTopic: Option[string], - contentTopics: Option[string], - senderTime: Option[string], - storeTime: Option[string], - digest: Option[string], - startTime: Option[string], - endTime: Option[string], - pageSize: Option[string], - direction: Option[string], -): Result[HistoryQuery, string] = - # Parse pubsubTopic parameter - var parsedPubsubTopic = none(string) - if pubsubTopic.isSome(): - let decodedPubsubTopic = decodeUrl(pubsubTopic.get()) - if decodedPubsubTopic != "": - parsedPubsubTopic = some(decodedPubsubTopic) - - # Parse the content topics - var parsedContentTopics = newSeq[ContentTopic](0) - if contentTopics.isSome(): - let ctList = decodeUrl(contentTopics.get()) - if ctList != "": - for ct in ctList.split(','): - parsedContentTopics.add(ct) - - # Parse cursor information - let parsedCursor = ?parseCursor(parsedPubsubTopic, senderTime, storeTime, digest) - - # Parse page size field - var parsedPagedSize = DefaultPageSize - if pageSize.isSome() and pageSize.get() != "": - try: - parsedPagedSize = uint64(parseInt(pageSize.get())) - except CatchableError: - return err("Problem parsing page size [" & getCurrentExceptionMsg() & "]") - - # Parse start time - let parsedStartTime = ?parseTime(startTime) - - # Parse end time - let parsedEndTime = ?parseTime(endTime) - - # Parse ascending field - var parsedDirection = default() - if direction.isSome() and direction.get() != "": - parsedDirection = direction.get().into() - - return ok( - HistoryQuery( - pubsubTopic: parsedPubsubTopic, - contentTopics: parsedContentTopics, - startTime: parsedStartTime, - endTime: parsedEndTime, - direction: parsedDirection, - pageSize: parsedPagedSize, - cursor: parsedCursor, - ) - ) - -# Simple type conversion. The "Option[Result[string, cstring]]" -# type is used by the nim-presto library. -proc toOpt(self: Option[Result[string, cstring]]): Option[string] = - if not self.isSome() or self.get().value == "": - return none(string) - if self.isSome() and self.get().value != "": - return some(self.get().value) - -proc retrieveMsgsFromSelfNode( - self: WakuNode, histQuery: HistoryQuery -): Future[RestApiResponse] {.async.} = - ## Performs a "store" request to the local node (self node.) - ## Notice that this doesn't follow the regular store libp2p channel because a node - ## it is not allowed to libp2p-dial a node to itself, by default. - ## - - let selfResp = (await self.wakuLegacyStore.handleSelfStoreRequest(histQuery)).valueOr: - return RestApiResponse.internalServerError($error) - - let storeResp = selfResp.toStoreResponseRest() - let resp = RestApiResponse.jsonResponse(storeResp, status = Http200).valueOr: - const msg = "Error building the json respose" - let e = $error - error msg, error = e - return RestApiResponse.internalServerError(fmt("{msg} [{e}]")) - - return resp - -# Subscribes the rest handler to attend "/store/v1/messages" requests -proc installStoreApiHandlers*( - router: var RestRouter, - node: WakuNode, - discHandler: Option[DiscoveryHandler] = none(DiscoveryHandler), -) = - # Handles the store-query request according to the passed parameters - router.api(MethodGet, "/store/v1/messages") do( - peerAddr: Option[string], - pubsubTopic: Option[string], - contentTopics: Option[string], - senderTime: Option[string], - storeTime: Option[string], - digest: Option[string], - startTime: Option[string], - endTime: Option[string], - pageSize: Option[string], - ascending: Option[string] - ) -> RestApiResponse: - info "REST-GET /store/v1/messages ", peer_addr = $peerAddr - - # All the GET parameters are URL-encoded (https://en.wikipedia.org/wiki/URL_encoding) - # Example: - # /store/v1/messages?peerAddr=%2Fip4%2F127.0.0.1%2Ftcp%2F60001%2Fp2p%2F16Uiu2HAmVFXtAfSj4EiR7mL2KvL4EE2wztuQgUSBoj2Jx2KeXFLN\&pubsubTopic=my-waku-topic - - # Parse the rest of the parameters and create a HistoryQuery - let histQuery = createHistoryQuery( - pubsubTopic.toOpt(), - contentTopics.toOpt(), - senderTime.toOpt(), - storeTime.toOpt(), - digest.toOpt(), - startTime.toOpt(), - endTime.toOpt(), - pageSize.toOpt(), - ascending.toOpt(), - ).valueOr: - return RestApiResponse.badRequest(error) - - if peerAddr.isNone() and not node.wakuLegacyStore.isNil(): - ## The user didn't specify a peer address and self-node is configured as a store node. - ## In this case we assume that the user is willing to retrieve the messages stored by - ## the local/self store node. - return await node.retrieveMsgsFromSelfNode(histQuery) - - # Parse the peer address parameter - let parsedPeerAddr = parseUrlPeerAddr(peerAddr.toOpt()).valueOr: - return RestApiResponse.badRequest(error) - - let peerAddr = parsedPeerAddr.valueOr: - node.peerManager.selectPeer(WakuLegacyStoreCodec).valueOr: - let handler = discHandler.valueOr: - return NoPeerNoDiscError - - let peerOp = (await handler()).valueOr: - return RestApiResponse.internalServerError($error) - - peerOp.valueOr: - return RestApiResponse.preconditionFailed( - "No suitable service peer & none discovered" - ) - - return await node.performHistoryQuery(histQuery, peerAddr) diff --git a/waku/waku_api/rest/legacy_store/types.nim b/waku/waku_api/rest/legacy_store/types.nim deleted file mode 100644 index 53a96bd69..000000000 --- a/waku/waku_api/rest/legacy_store/types.nim +++ /dev/null @@ -1,376 +0,0 @@ -{.push raises: [].} - -import - std/[sets, strformat, uri], - stew/byteutils, - chronicles, - json_serialization, - json_serialization/std/options, - presto/[route, client, common] -import - ../../../waku_store_legacy/common as waku_store_common, - ../../../common/base64, - ../../../waku_core, - ../serdes - -#### Types - -type - HistoryCursorRest* = object - pubsubTopic*: PubsubTopic - senderTime*: Timestamp - storeTime*: Timestamp - digest*: waku_store_common.MessageDigest - - StoreRequestRest* = object - # inspired by https://github.com/waku-org/nwaku/blob/f95147f5b7edfd45f914586f2d41cd18fb0e0d18/waku/v2//waku_store/common.nim#L52 - pubsubTopic*: Option[PubsubTopic] - contentTopics*: seq[ContentTopic] - cursor*: Option[HistoryCursorRest] - startTime*: Option[Timestamp] - endTime*: Option[Timestamp] - pageSize*: uint64 - ascending*: bool - - StoreWakuMessage* = object - payload*: Base64String - contentTopic*: Option[ContentTopic] - version*: Option[uint32] - timestamp*: Option[Timestamp] - ephemeral*: Option[bool] - meta*: Option[Base64String] - - StoreResponseRest* = object # inspired by https://rfc.vac.dev/spec/16/#storeresponse - messages*: seq[StoreWakuMessage] - cursor*: Option[HistoryCursorRest] - # field that contains error information - errorMessage*: Option[string] - -createJsonFlavor RestJson - -Json.setWriter JsonWriter, PreferredOutput = string - -#### Type conversion - -# Converts a URL-encoded-base64 string into a 'MessageDigest' -proc parseMsgDigest*( - input: Option[string] -): Result[Option[waku_store_common.MessageDigest], string] = - if not input.isSome() or input.get() == "": - return ok(none(waku_store_common.MessageDigest)) - - let decodedUrl = decodeUrl(input.get()) - let base64DecodedArr = base64.decode(Base64String(decodedUrl)).valueOr: - return err(error) - - var messageDigest = waku_store_common.MessageDigest() - - # Next snippet inspired by "nwaku/waku/waku_archive/archive.nim" - # TODO: Improve coherence of MessageDigest type - messageDigest = block: - var data: array[32, byte] - for i in 0 ..< min(base64DecodedArr.len, 32): - data[i] = base64DecodedArr[i] - - waku_store_common.MessageDigest(data: data) - - return ok(some(messageDigest)) - -# Converts a given MessageDigest object into a suitable -# Base64-URL-encoded string suitable to be transmitted in a Rest -# request-response. The MessageDigest is first base64 encoded -# and this result is URL-encoded. -proc toRestStringMessageDigest*(self: waku_store_common.MessageDigest): string = - let base64Encoded = base64.encode(self.data) - encodeUrl($base64Encoded) - -proc toWakuMessage*(message: StoreWakuMessage): WakuMessage = - WakuMessage( - payload: base64.decode(message.payload).get(), - contentTopic: message.contentTopic.get(), - version: message.version.get(), - timestamp: message.timestamp.get(), - ephemeral: message.ephemeral.get(), - meta: message.meta.get(Base64String("")).decode().get(), - ) - -# Converts a 'HistoryResponse' object to an 'StoreResponseRest' -# that can be serialized to a json object. -proc toStoreResponseRest*(histResp: HistoryResponse): StoreResponseRest = - proc toStoreWakuMessage(message: WakuMessage): StoreWakuMessage = - StoreWakuMessage( - payload: base64.encode(message.payload), - contentTopic: some(message.contentTopic), - version: some(message.version), - timestamp: some(message.timestamp), - ephemeral: some(message.ephemeral), - meta: - if message.meta.len > 0: - some(base64.encode(message.meta)) - else: - none(Base64String), - ) - - var storeWakuMsgs: seq[StoreWakuMessage] - for m in histResp.messages: - storeWakuMsgs.add(m.toStoreWakuMessage()) - - var cursor = none(HistoryCursorRest) - if histResp.cursor.isSome: - cursor = some( - HistoryCursorRest( - pubsubTopic: histResp.cursor.get().pubsubTopic, - senderTime: histResp.cursor.get().senderTime, - storeTime: histResp.cursor.get().storeTime, - digest: histResp.cursor.get().digest, - ) - ) - - StoreResponseRest(messages: storeWakuMsgs, cursor: cursor) - -## Beginning of StoreWakuMessage serde - -proc writeValue*( - writer: var JsonWriter, value: StoreWakuMessage -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - writer.writeField("payload", $value.payload) - if value.contentTopic.isSome(): - writer.writeField("contentTopic", value.contentTopic.get()) - if value.version.isSome(): - writer.writeField("version", value.version.get()) - if value.timestamp.isSome(): - writer.writeField("timestamp", value.timestamp.get()) - if value.ephemeral.isSome(): - writer.writeField("ephemeral", value.ephemeral.get()) - if value.meta.isSome(): - writer.writeField("meta", value.meta.get()) - writer.endRecord() - -proc readValue*( - reader: var JsonReader, value: var StoreWakuMessage -) {.gcsafe, raises: [SerializationError, IOError].} = - var - payload = none(Base64String) - contentTopic = none(ContentTopic) - version = none(uint32) - timestamp = none(Timestamp) - ephemeral = none(bool) - meta = none(Base64String) - - var keys = initHashSet[string]() - for fieldName in readObjectFields(reader): - # Check for reapeated keys - if keys.containsOrIncl(fieldName): - let err = - try: - fmt"Multiple `{fieldName}` fields found" - except CatchableError: - "Multiple fields with the same name found" - reader.raiseUnexpectedField(err, "StoreWakuMessage") - - case fieldName - of "payload": - payload = some(reader.readValue(Base64String)) - of "contentTopic": - contentTopic = some(reader.readValue(ContentTopic)) - of "version": - version = some(reader.readValue(uint32)) - of "timestamp": - timestamp = some(reader.readValue(Timestamp)) - of "ephemeral": - ephemeral = some(reader.readValue(bool)) - of "meta": - meta = some(reader.readValue(Base64String)) - else: - reader.raiseUnexpectedField("Unrecognided field", cstring(fieldName)) - - if payload.isNone(): - reader.raiseUnexpectedValue("Field `payload` is missing") - - value = StoreWakuMessage( - payload: payload.get(), - contentTopic: contentTopic, - version: version, - timestamp: timestamp, - ephemeral: ephemeral, - meta: meta, - ) - -## End of StoreWakuMessage serde - -## Beginning of MessageDigest serde - -proc writeValue*( - writer: var JsonWriter, value: waku_store_common.MessageDigest -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - writer.writeField("data", base64.encode(value.data)) - writer.endRecord() - -proc readValue*( - reader: var JsonReader, value: var waku_store_common.MessageDigest -) {.gcsafe, raises: [SerializationError, IOError].} = - var data = none(seq[byte]) - - for fieldName in readObjectFields(reader): - case fieldName - of "data": - if data.isSome(): - reader.raiseUnexpectedField("Multiple `data` fields found", "MessageDigest") - let decoded = base64.decode(reader.readValue(Base64String)).valueOr: - reader.raiseUnexpectedField("Failed decoding data", "MessageDigest") - data = some(decoded) - else: - reader.raiseUnexpectedField("Unrecognided field", cstring(fieldName)) - - if data.isNone(): - reader.raiseUnexpectedValue("Field `data` is missing") - - for i in 0 ..< 32: - value.data[i] = data.get()[i] - -## End of MessageDigest serde - -## Beginning of HistoryCursorRest serde - -proc writeValue*( - writer: var JsonWriter, value: HistoryCursorRest -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - writer.writeField("pubsubTopic", value.pubsubTopic) - writer.writeField("senderTime", value.senderTime) - writer.writeField("storeTime", value.storeTime) - writer.writeField("digest", value.digest) - writer.endRecord() - -proc readValue*( - reader: var JsonReader, value: var HistoryCursorRest -) {.gcsafe, raises: [SerializationError, IOError].} = - var - pubsubTopic = none(PubsubTopic) - senderTime = none(Timestamp) - storeTime = none(Timestamp) - digest = none(waku_store_common.MessageDigest) - - for fieldName in readObjectFields(reader): - case fieldName - of "pubsubTopic": - if pubsubTopic.isSome(): - reader.raiseUnexpectedField( - "Multiple `pubsubTopic` fields found", "HistoryCursorRest" - ) - pubsubTopic = some(reader.readValue(PubsubTopic)) - of "senderTime": - if senderTime.isSome(): - reader.raiseUnexpectedField( - "Multiple `senderTime` fields found", "HistoryCursorRest" - ) - senderTime = some(reader.readValue(Timestamp)) - of "storeTime": - if storeTime.isSome(): - reader.raiseUnexpectedField( - "Multiple `storeTime` fields found", "HistoryCursorRest" - ) - storeTime = some(reader.readValue(Timestamp)) - of "digest": - if digest.isSome(): - reader.raiseUnexpectedField( - "Multiple `digest` fields found", "HistoryCursorRest" - ) - digest = some(reader.readValue(waku_store_common.MessageDigest)) - else: - reader.raiseUnexpectedField("Unrecognided field", cstring(fieldName)) - - if pubsubTopic.isNone(): - reader.raiseUnexpectedValue("Field `pubsubTopic` is missing") - - if senderTime.isNone(): - reader.raiseUnexpectedValue("Field `senderTime` is missing") - - if storeTime.isNone(): - reader.raiseUnexpectedValue("Field `storeTime` is missing") - - if digest.isNone(): - reader.raiseUnexpectedValue("Field `digest` is missing") - - value = HistoryCursorRest( - pubsubTopic: pubsubTopic.get(), - senderTime: senderTime.get(), - storeTime: storeTime.get(), - digest: digest.get(), - ) - -## End of HistoryCursorRest serde - -## Beginning of StoreResponseRest serde - -proc writeValue*( - writer: var JsonWriter, value: StoreResponseRest -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - writer.writeField("messages", value.messages) - if value.cursor.isSome(): - writer.writeField("cursor", value.cursor.get()) - if value.errorMessage.isSome(): - writer.writeField("errorMessage", value.errorMessage.get()) - writer.endRecord() - -proc readValue*( - reader: var JsonReader, value: var StoreResponseRest -) {.gcsafe, raises: [SerializationError, IOError].} = - var - messages = none(seq[StoreWakuMessage]) - cursor = none(HistoryCursorRest) - errorMessage = none(string) - - for fieldName in readObjectFields(reader): - case fieldName - of "messages": - if messages.isSome(): - reader.raiseUnexpectedField( - "Multiple `messages` fields found", "StoreResponseRest" - ) - messages = some(reader.readValue(seq[StoreWakuMessage])) - of "cursor": - if cursor.isSome(): - reader.raiseUnexpectedField( - "Multiple `cursor` fields found", "StoreResponseRest" - ) - cursor = some(reader.readValue(HistoryCursorRest)) - of "errorMessage": - if errorMessage.isSome(): - reader.raiseUnexpectedField( - "Multiple `errorMessage` fields found", "StoreResponseRest" - ) - errorMessage = some(reader.readValue(string)) - else: - reader.raiseUnexpectedField("Unrecognided field", cstring(fieldName)) - - if messages.isNone(): - reader.raiseUnexpectedValue("Field `messages` is missing") - - value = StoreResponseRest( - messages: messages.get(), cursor: cursor, errorMessage: errorMessage - ) - -## End of StoreResponseRest serde - -## Beginning of StoreRequestRest serde - -proc writeValue*( - writer: var JsonWriter, value: StoreRequestRest -) {.gcsafe, raises: [IOError].} = - writer.beginRecord() - if value.pubsubTopic.isSome(): - writer.writeField("pubsubTopic", value.pubsubTopic.get()) - writer.writeField("contentTopics", value.contentTopics) - if value.startTime.isSome(): - writer.writeField("startTime", value.startTime.get()) - if value.endTime.isSome(): - writer.writeField("endTime", value.endTime.get()) - writer.writeField("pageSize", value.pageSize) - writer.writeField("ascending", value.ascending) - writer.endRecord() - -## End of StoreRequestRest serde diff --git a/waku/waku_archive/archive.nim b/waku/waku_archive/archive.nim index 707c757a3..976d7d035 100644 --- a/waku/waku_archive/archive.nim +++ b/waku/waku_archive/archive.nim @@ -14,7 +14,8 @@ import ../waku_core, ../waku_core/message/digest, ./common, - ./archive_metrics + ./archive_metrics, + waku/waku_archive/retention_policy/retention_policy_time logScope: topics = "waku archive" @@ -45,7 +46,7 @@ type WakuArchive* = ref object validator: MessageValidator - retentionPolicy: Option[RetentionPolicy] + retentionPolicies: seq[RetentionPolicy] retentionPolicyHandle: Future[void] metricsHandle: Future[void] @@ -61,9 +62,19 @@ proc validate*(msg: WakuMessage): Result[void, string] = upperBound = now + MaxMessageTimestampVariance if msg.timestamp < lowerBound: + warn "rejecting message with old timestamp", + msgTimestamp = msg.timestamp, + lowerBound = lowerBound, + now = now, + drift = (now - msg.timestamp) div 1_000_000_000 return err(invalidMessageOld) if upperBound < msg.timestamp: + warn "rejecting message with future timestamp", + msgTimestamp = msg.timestamp, + upperBound = upperBound, + now = now, + drift = (msg.timestamp - now) div 1_000_000_000 return err(invalidMessageFuture) return ok() @@ -72,13 +83,14 @@ proc new*( T: type WakuArchive, driver: ArchiveDriver, validator: MessageValidator = validate, - retentionPolicy = none(RetentionPolicy), + retentionPolicies = @[RetentionPolicy(TimeRetentionPolicy.new(2.days.seconds))], ): Result[T, string] = if driver.isNil(): return err("archive driver is Nil") - let archive = - WakuArchive(driver: driver, validator: validator, retentionPolicy: retentionPolicy) + let archive = WakuArchive( + driver: driver, validator: validator, retentionPolicies: retentionPolicies + ) return ok(archive) @@ -253,16 +265,15 @@ proc findMessages*( ) proc periodicRetentionPolicy(self: WakuArchive) {.async.} = - let policy = self.retentionPolicy.get() - while true: - info "executing message retention policy" - (await policy.execute(self.driver)).isOkOr: - waku_archive_errors.inc(labelValues = [retPolicyFailure]) - error "failed execution of retention policy", error = error - await sleepAsync(WakuArchiveDefaultRetentionPolicyIntervalWhenError) - ## in case of error, let's try again faster - continue + for policy in self.retentionPolicies: + info "executing message retention policy", policy = $policy + (await policy.execute(self.driver)).isOkOr: + waku_archive_errors.inc(labelValues = [retPolicyFailure]) + error "failed execution of retention policy", policy = $policy, error = error + await sleepAsync(WakuArchiveDefaultRetentionPolicyIntervalWhenError) + ## in case of error, let's try again faster + continue await sleepAsync(WakuArchiveDefaultRetentionPolicyInterval) @@ -279,7 +290,7 @@ proc periodicMetricReport(self: WakuArchive) {.async.} = await sleepAsync(WakuArchiveDefaultMetricsReportInterval) proc start*(self: WakuArchive) = - if self.retentionPolicy.isSome(): + if self.retentionPolicies.len > 0: self.retentionPolicyHandle = self.periodicRetentionPolicy() self.metricsHandle = self.periodicMetricReport() @@ -287,7 +298,7 @@ proc start*(self: WakuArchive) = proc stopWait*(self: WakuArchive) {.async.} = var futures: seq[Future[void]] - if self.retentionPolicy.isSome() and not self.retentionPolicyHandle.isNil(): + if not self.retentionPolicyHandle.isNil(): futures.add(self.retentionPolicyHandle.cancelAndWait()) if not self.metricsHandle.isNil: diff --git a/waku/waku_archive/driver/builder.nim b/waku/waku_archive/driver/builder.nim index cc46afb4c..811b16999 100644 --- a/waku/waku_archive/driver/builder.nim +++ b/waku/waku_archive/driver/builder.nim @@ -32,71 +32,54 @@ proc new*( ## maxNumConn - defines the maximum number of connections to handle simultaneously (Postgres) ## onFatalErrorAction - called if, e.g., the connection with db got lost - let dbUrlValidationRes = dburl.validateDbUrl(url) - if dbUrlValidationRes.isErr(): - return err("DbUrl failure in ArchiveDriver.new: " & dbUrlValidationRes.error) + dburl.validateDbUrl(url).isOkOr: + return err("DbUrl failure in ArchiveDriver.new: " & error) - let engineRes = dburl.getDbEngine(url) - if engineRes.isErr(): - return err("error getting db engine in setupWakuArchiveDriver: " & engineRes.error) - - let engine = engineRes.get() + let engine = dburl.getDbEngine(url).valueOr: + return err("error getting db engine in setupWakuArchiveDriver: " & error) case engine of "sqlite": - let pathRes = dburl.getDbPath(url) - if pathRes.isErr(): - return err("error get path in setupWakuArchiveDriver: " & pathRes.error) + let path = dburl.getDbPath(url).valueOr: + return err("error get path in setupWakuArchiveDriver: " & error) - let dbRes = SqliteDatabase.new(pathRes.get()) - if dbRes.isErr(): - return err("error in setupWakuArchiveDriver: " & dbRes.error) - - let db = dbRes.get() + let db = SqliteDatabase.new(path).valueOr: + return err("error in setupWakuArchiveDriver: " & error) # SQLite vacuum - let sqliteStatsRes = db.gatherSqlitePageStats() - if sqliteStatsRes.isErr(): - return err("error while gathering sqlite stats: " & $sqliteStatsRes.error) + let (pageSize, pageCount, freelistCount) = db.gatherSqlitePageStats().valueOr: + return err("error while gathering sqlite stats: " & $error) - let (pageSize, pageCount, freelistCount) = sqliteStatsRes.get() info "sqlite database page stats", pageSize = pageSize, pages = pageCount, freePages = freelistCount if vacuum and (pageCount > 0 and freelistCount > 0): - let vacuumRes = db.performSqliteVacuum() - if vacuumRes.isErr(): - return err("error in vacuum sqlite: " & $vacuumRes.error) + db.performSqliteVacuum().isOkOr: + return err("error in vacuum sqlite: " & $error) # Database migration if migrate: - let migrateRes = archive_driver_sqlite_migrations.migrate(db) - if migrateRes.isErr(): - return err("error in migrate sqlite: " & $migrateRes.error) + archive_driver_sqlite_migrations.migrate(db).isOkOr: + return err("error in migrate sqlite: " & $error) info "setting up sqlite waku archive driver" - let res = SqliteDriver.new(db) - if res.isErr(): - return err("failed to init sqlite archive driver: " & res.error) + let res = SqliteDriver.new(db).valueOr: + return err("failed to init sqlite archive driver: " & error) - return ok(res.get()) + return ok(res) of "postgres": when defined(postgres): - let res = PostgresDriver.new( + let driver = PostgresDriver.new( dbUrl = url, maxConnections = maxNumConn, onFatalErrorAction = onFatalErrorAction, - ) - if res.isErr(): - return err("failed to init postgres archive driver: " & res.error) - - let driver = res.get() + ).valueOr: + return err("failed to init postgres archive driver: " & error) # Database migration if migrate: - let migrateRes = await archive_postgres_driver_migrations.migrate(driver) - if migrateRes.isErr(): - return err("ArchiveDriver build failed in migration: " & $migrateRes.error) + (await archive_postgres_driver_migrations.migrate(driver)).isOkOr: + return err("ArchiveDriver build failed in migration: " & $error) ## This should be started once we make sure the 'messages' table exists ## Hence, this should be run after the migration is completed. diff --git a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim index d5eba9a5c..82c8ec2ae 100644 --- a/waku/waku_archive/driver/postgres_driver/postgres_driver.nim +++ b/waku/waku_archive/driver/postgres_driver/postgres_driver.nim @@ -5,6 +5,7 @@ import stew/[byteutils, arrayops], results, chronos, + metrics, db_connector/[postgres, db_common], chronicles import @@ -16,6 +17,15 @@ import ./postgres_healthcheck, ./partitions_manager +logScope: + topics = "postgres driver" + +declarePublicGauge postgres_payload_size_bytes, + "Payload size in bytes of correctly stored messages" + +logScope: + topics = "postgres driver" + type PostgresDriver* = ref object of ArchiveDriver ## Establish a separate pools for read/write operations writeConnPool: PgAsyncPool @@ -41,8 +51,7 @@ const SelectClause = const SelectNoCursorAscStmtName = "SelectWithoutCursorAsc" const SelectNoCursorAscStmtDef = - SelectClause & - """WHERE contentTopic IN ($1) AND + SelectClause & """WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND timestamp >= $4 AND @@ -50,8 +59,7 @@ const SelectNoCursorAscStmtDef = ORDER BY timestamp ASC, messageHash ASC LIMIT $6;""" const SelectNoCursorNoDataAscStmtName = "SelectWithoutCursorAndDataAsc" -const SelectNoCursorNoDataAscStmtDef = - """SELECT messageHash FROM messages +const SelectNoCursorNoDataAscStmtDef = """SELECT messageHash FROM messages WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND @@ -61,8 +69,7 @@ const SelectNoCursorNoDataAscStmtDef = const SelectNoCursorDescStmtName = "SelectWithoutCursorDesc" const SelectNoCursorDescStmtDef = - SelectClause & - """WHERE contentTopic IN ($1) AND + SelectClause & """WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND timestamp >= $4 AND @@ -70,8 +77,7 @@ const SelectNoCursorDescStmtDef = ORDER BY timestamp DESC, messageHash DESC LIMIT $6;""" const SelectNoCursorNoDataDescStmtName = "SelectWithoutCursorAndDataDesc" -const SelectNoCursorNoDataDescStmtDef = - """SELECT messageHash FROM messages +const SelectNoCursorNoDataDescStmtDef = """SELECT messageHash FROM messages WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND @@ -81,8 +87,7 @@ const SelectNoCursorNoDataDescStmtDef = const SelectWithCursorDescStmtName = "SelectWithCursorDesc" const SelectWithCursorDescStmtDef = - SelectClause & - """WHERE contentTopic IN ($1) AND + SelectClause & """WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND (timestamp, messageHash) < ($4,$5) AND @@ -91,8 +96,7 @@ const SelectWithCursorDescStmtDef = ORDER BY timestamp DESC, messageHash DESC LIMIT $8;""" const SelectWithCursorNoDataDescStmtName = "SelectWithCursorNoDataDesc" -const SelectWithCursorNoDataDescStmtDef = - """SELECT messageHash FROM messages +const SelectWithCursorNoDataDescStmtDef = """SELECT messageHash FROM messages WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND @@ -103,8 +107,7 @@ const SelectWithCursorNoDataDescStmtDef = const SelectWithCursorAscStmtName = "SelectWithCursorAsc" const SelectWithCursorAscStmtDef = - SelectClause & - """WHERE contentTopic IN ($1) AND + SelectClause & """WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND (timestamp, messageHash) > ($4,$5) AND @@ -113,8 +116,7 @@ const SelectWithCursorAscStmtDef = ORDER BY timestamp ASC, messageHash ASC LIMIT $8;""" const SelectWithCursorNoDataAscStmtName = "SelectWithCursorNoDataAsc" -const SelectWithCursorNoDataAscStmtDef = - """SELECT messageHash FROM messages +const SelectWithCursorNoDataAscStmtDef = """SELECT messageHash FROM messages WHERE contentTopic IN ($1) AND messageHash IN ($2) AND pubsubTopic = $3 AND @@ -124,8 +126,7 @@ const SelectWithCursorNoDataAscStmtDef = ORDER BY timestamp ASC, messageHash ASC LIMIT $8;""" const SelectCursorByHashName = "SelectMessageByHashInMessagesLookup" -const SelectCursorByHashDef = - """SELECT timestamp FROM messages_lookup +const SelectCursorByHashDef = """SELECT timestamp FROM messages_lookup WHERE messageHash = $1""" const @@ -186,11 +187,11 @@ proc timeCursorCallbackImpl(pqResult: ptr PGresult, timeCursor: var Option[Times let catchable = catch: parseBiggestInt(rawTimestamp) - if catchable.isErr(): - error "could not parse correctly", error = catchable.error.msg + let time = catchable.valueOr: + error "could not parse correctly", error = error.msg return - timeCursor = some(catchable.get()) + timeCursor = some(time) proc hashCallbackImpl( pqResult: ptr PGresult, rows: var seq[(WakuMessageHash, PubsubTopic, WakuMessage)] @@ -214,11 +215,10 @@ proc hashCallbackImpl( let catchable = catch: parseHexStr(rawHash) - if catchable.isErr(): - error "could not parse correctly", error = catchable.error.msg + let hashHex = catchable.valueOr: + error "could not parse correctly", error = error.msg return - let hashHex = catchable.get() let msgHash = fromBytes(hashHex.toOpenArrayByte(0, 31)) rows.add((msgHash, "", WakuMessage())) @@ -294,13 +294,13 @@ method put*( pubsubTopic: PubsubTopic, message: WakuMessage, ): Future[ArchiveDriverResult[void]] {.async.} = - let messageHash = toHex(messageHash) + let messageHash = byteutils.toHex(messageHash) let contentTopic = message.contentTopic - let payload = toHex(message.payload) + let payload = byteutils.toHex(message.payload) let version = $message.version let timestamp = $message.timestamp - let meta = toHex(message.meta) + let meta = byteutils.toHex(message.meta) trace "put PostgresDriver", messageHash, contentTopic, payload, version, timestamp, meta @@ -334,7 +334,7 @@ method put*( return err("could not put msg in messages table: " & $error) ## Now add the row to messages_lookup - return await s.writeConnPool.runStmt( + let ret = await s.writeConnPool.runStmt( InsertRowInMessagesLookupStmtName, InsertRowInMessagesLookupStmtDefinition, @[messageHash, timestamp], @@ -342,6 +342,10 @@ method put*( @[int32(0), int32(0)], ) + if ret.isOk(): + postgres_payload_size_bytes.set(message.payload.len) + return ret + method getAllMessages*( s: PostgresDriver ): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = @@ -368,6 +372,7 @@ proc getPartitionsList( ): Future[ArchiveDriverResult[seq[string]]] {.async.} = ## Retrieves the seq of partition table names. ## e.g: @["messages_1708534333_1708534393", "messages_1708534273_1708534333"] + ## This returns the partitions that are attached to the main messages table. var partitions: seq[string] proc rowCallback(pqResult: ptr PGresult) = for iRow in 0 ..< pqResult.pqNtuples(): @@ -394,6 +399,49 @@ proc getPartitionsList( return ok(partitions) +## fwd declaration. The implementation is below. +proc dropPartition( + self: PostgresDriver, partitionName: string +): Future[ArchiveDriverResult[void]] {.async.} + +proc dropOrphanPartitions( + s: PostgresDriver +): Future[ArchiveDriverResult[void]] {.async.} = + ## Tries to remove partitions that weren't correctly removed during retention policy execution. + ## Orphan partition is a partition that is not attached to the main messages table. + ## Therefore, it is not used for queries and can be safely removed. + var partitions: seq[string] + proc rowCallback(pqResult: ptr PGresult) = + for iRow in 0 ..< pqResult.pqNtuples(): + let partitionName = $(pqgetvalue(pqResult, iRow, 0)) + partitions.add(partitionName) + + ( + await s.readConnPool.pgQuery( + """ + SELECT c.relname AS partition_name + FROM pg_class c + LEFT JOIN pg_inherits i ON i.inhrelid = c.oid + WHERE c.relname LIKE 'messages_%' + AND c.relname != 'messages_lookup' + AND c.relkind = 'r' -- only regular tables + AND i.inhrelid IS NULL -- detached partition + ORDER BY partition_name + """, + newSeq[string](0), + rowCallback, + ) + ).isOkOr: + return err("dropOrphanPartitions failed in query: " & $error) + + for partition in partitions: + info "orphan partition found", partitionName = partition + (await s.dropPartition(partition)).isOkOr: + error "failed to drop orphan partition", partitionName = partition, error = $error + continue + + return ok() + proc getTimeCursor( s: PostgresDriver, hashHex: string ): Future[ArchiveDriverResult[Option[Timestamp]]] {.async.} = @@ -432,7 +480,7 @@ proc getMessagesArbitraryQuery( var args: seq[string] if cursor.isSome(): - let hashHex = toHex(cursor.get()) + let hashHex = byteutils.toHex(cursor.get()) let timeCursor = ?await s.getTimeCursor(hashHex) @@ -513,7 +561,7 @@ proc getMessageHashesArbitraryQuery( var args: seq[string] if cursor.isSome(): - let hashHex = toHex(cursor.get()) + let hashHex = byteutils.toHex(cursor.get()) let timeCursor = ?await s.getTimeCursor(hashHex) @@ -623,7 +671,7 @@ proc getMessagesPreparedStmt( return ok(rows) - let hashHex = toHex(cursor.get()) + let hashHex = byteutils.toHex(cursor.get()) let timeCursor = ?await s.getTimeCursor(hashHex) @@ -716,7 +764,7 @@ proc getMessageHashesPreparedStmt( return ok(rows) - let hashHex = toHex(cursor.get()) + let hashHex = byteutils.toHex(cursor.get()) let timeCursor = ?await s.getTimeCursor(hashHex) @@ -889,11 +937,10 @@ method getMessages*( let splittedHashes = hashes[i ..< stop] - let subRows = - ?await s.getMessagesWithinLimits( - includeData, contentTopics, pubsubTopic, cursor, startTime, endTime, - splittedHashes, maxPageSize, ascendingOrder, requestId, - ) + let subRows = ?await s.getMessagesWithinLimits( + includeData, contentTopics, pubsubTopic, cursor, startTime, endTime, + splittedHashes, maxPageSize, ascendingOrder, requestId, + ) for row in subRows: row @@ -953,11 +1000,10 @@ method getDatabaseSize*( method getMessagesCount*( s: PostgresDriver ): Future[ArchiveDriverResult[int64]] {.async.} = - let intRes = await s.getInt("SELECT COUNT(1) FROM messages") - if intRes.isErr(): - return err("error in getMessagesCount: " & intRes.error) + let intRes = (await s.getInt("SELECT COUNT(1) FROM messages")).valueOr: + return err("error in getMessagesCount: " & error) - return ok(intRes.get()) + return ok(intRes) method getOldestMessageTimestamp*( s: PostgresDriver @@ -970,47 +1016,44 @@ method getOldestMessageTimestamp*( let oldestPartitionTimeNanoSec = oldestPartition.getPartitionStartTimeInNanosec() - let intRes = await s.getInt("SELECT MIN(timestamp) FROM messages") - if intRes.isErr(): + let intRes = (await s.getInt("SELECT MIN(timestamp) FROM messages")).valueOr: ## Just return the oldest partition time considering the partitions set return ok(Timestamp(oldestPartitionTimeNanoSec)) - return ok(Timestamp(min(intRes.get(), oldestPartitionTimeNanoSec))) + return ok(Timestamp(min(intRes, oldestPartitionTimeNanoSec))) method getNewestMessageTimestamp*( s: PostgresDriver ): Future[ArchiveDriverResult[Timestamp]] {.async.} = - let intRes = await s.getInt("SELECT MAX(timestamp) FROM messages") + let intRes = (await s.getInt("SELECT MAX(timestamp) FROM messages")).valueOr: + return err("error in getNewestMessageTimestamp: " & error) - if intRes.isErr(): - return err("error in getNewestMessageTimestamp: " & intRes.error) - - return ok(Timestamp(intRes.get())) + return ok(Timestamp(intRes)) method deleteOldestMessagesNotWithinLimit*( s: PostgresDriver, limit: int ): Future[ArchiveDriverResult[void]] {.async.} = - var execRes = await s.writeConnPool.pgQuery( - """DELETE FROM messages WHERE messageHash NOT IN + ( + await s.writeConnPool.pgQuery( + """DELETE FROM messages WHERE messageHash NOT IN ( SELECT messageHash FROM messages ORDER BY timestamp DESC LIMIT ? );""", - @[$limit], - ) - if execRes.isErr(): - return err("error in deleteOldestMessagesNotWithinLimit: " & execRes.error) - - execRes = await s.writeConnPool.pgQuery( - """DELETE FROM messages_lookup WHERE messageHash NOT IN - ( - SELECT messageHash FROM messages ORDER BY timestamp DESC LIMIT ? - );""", - @[$limit], - ) - if execRes.isErr(): - return err( - "error in deleteOldestMessagesNotWithinLimit messages_lookup: " & execRes.error + @[$limit], ) + ).isOkOr: + return err("error in deleteOldestMessagesNotWithinLimit: " & error) + + ( + await s.writeConnPool.pgQuery( + """DELETE FROM messages_lookup WHERE messageHash NOT IN + ( + SELECT messageHash FROM messages ORDER BY timestamp DESC LIMIT ? + );""", + @[$limit], + ) + ).isOkOr: + return err("error in deleteOldestMessagesNotWithinLimit messages_lookup: " & error) return ok() @@ -1264,11 +1307,18 @@ proc loopPartitionFactory( self: PostgresDriver, onFatalError: OnFatalErrorHandler ) {.async.} = ## Loop proc that continuously checks whether we need to create a new partition. - ## Notice that the deletion of partitions is handled by the retention policy modules. + ## Notice that the deletion of partitions is mostly handled by the retention policy modules. + ## This loop only removes orphan partitions which were detached but not properly removed by the + ## retention policy module due to some error. However, the main task of this loop is to create + ## new partitions when needed. info "starting loopPartitionFactory" while true: + trace "loopPartitionFactory iteration started" + (await self.dropOrphanPartitions()).isOkOr: + onFatalError("error when dropping orphan partitions: " & $error) + trace "Check if a new partition is needed" ## Let's make the 'partition_manager' aware of the current partitions @@ -1326,14 +1376,24 @@ proc getTableSize*( return ok(tableSize) -proc removePartition( +proc dropPartition( + self: PostgresDriver, partitionName: string +): Future[ArchiveDriverResult[void]] {.async.} = + let dropPartitionQuery = "DROP TABLE " & partitionName + info "drop partition", query = dropPartitionQuery + (await self.performWriteQuery(dropPartitionQuery)).isOkOr: + return err(fmt"error in dropPartition: {dropPartitionQuery}: " & $error) + + return ok() + +proc detachAndDropPartition( self: PostgresDriver, partition: Partition ): Future[ArchiveDriverResult[void]] {.async.} = - ## Removes the desired partition and also removes the rows from messages_lookup table + ## Detaches and drops the desired partition and also removes the rows from messages_lookup table ## whose rows belong to the partition time range let partitionName = partition.getName() - info "beginning of removePartition", partitionName + info "beginning of detachAndDropPartition", partitionName let partSize = (await self.getTableSize(partitionName)).valueOr("") @@ -1344,8 +1404,10 @@ proc removePartition( (await self.performWriteQuery(detachPartitionQuery)).isOkOr: info "detected error when trying to detach partition", error - if ($error).contains("FINALIZE") or - ($error).contains("already pending detach in part"): + if ($error).contains("FINALIZE") or ($error).contains("already pending"): + ## We assume "already pending detach in partitioned table ..." as possible error + debug "enforce detach with FINALIZE because of detected error", error + ## We assume the database is suggesting to use FINALIZE when detaching a partition let detachPartitionFinalizeQuery = "ALTER TABLE messages DETACH PARTITION " & partitionName & " FINALIZE;" @@ -1356,11 +1418,8 @@ proc removePartition( else: return err(fmt"error in {detachPartitionQuery}: " & $error) - ## Drop the partition - let dropPartitionQuery = "DROP TABLE " & partitionName - info "removeOldestPartition drop partition", query = dropPartitionQuery - (await self.performWriteQuery(dropPartitionQuery)).isOkOr: - return err(fmt"error in {dropPartitionQuery}: " & $error) + ## Drop partition + ?(await self.dropPartition(partitionName)) info "removed partition", partition_name = partitionName, partition_size = partSize self.partitionMngr.removeOldestPartitionName() @@ -1385,8 +1444,18 @@ proc removePartitionsOlderThan( var oldestPartition = self.partitionMngr.getOldestPartition().valueOr: return err("could not get oldest partition in removePartitionOlderThan: " & $error) - while not oldestPartition.containsMoment(tsInSec): - (await self.removePartition(oldestPartition)).isOkOr: + debug "oldest partition info", + partitionName = oldestPartition.getName(), + partitionLastMoment = oldestPartition.getLastMoment(), + tsInSec + + while oldestPartition.getLastMoment() < tsInSec: + info "start removing partition whose first record is older than the specified timestamp", + partitionName = oldestPartition.getName(), + partitionFirstMoment = oldestPartition.getLastMoment(), + tsInSec + + (await self.detachAndDropPartition(oldestPartition)).isOkOr: return err("issue in removePartitionsOlderThan: " & $error) oldestPartition = self.partitionMngr.getOldestPartition().valueOr: @@ -1414,7 +1483,7 @@ proc removeOldestPartition( info "Skipping to remove the current partition" return ok() - return await self.removePartition(oldestPartition) + return await self.detachAndDropPartition(oldestPartition) proc containsAnyPartition*(self: PostgresDriver): bool = return not self.partitionMngr.isEmpty() diff --git a/waku/waku_archive/driver/queue_driver/queue_driver.nim b/waku/waku_archive/driver/queue_driver/queue_driver.nim index 9dbf3c112..2ffc9ab00 100644 --- a/waku/waku_archive/driver/queue_driver/queue_driver.nim +++ b/waku/waku_archive/driver/queue_driver/queue_driver.nim @@ -97,8 +97,7 @@ proc getPage( # Find starting entry if cursor.isSome(): - let cursorEntry = w.walkToCursor(cursor.get(), forward) - if cursorEntry.isErr(): + w.walkToCursor(cursor.get(), forward).isOkOr: return err(QueueDriverErrorKind.INVALID_CURSOR) # Advance walker once more @@ -177,7 +176,7 @@ proc first*(driver: QueueDriver): ArchiveDriverResult[Index] = res = w.first() w.destroy() - if res.isErr(): + res.isOkOr: return err("Not found") return ok(res.value.key) @@ -188,7 +187,7 @@ proc last*(driver: QueueDriver): ArchiveDriverResult[Index] = res = w.last() w.destroy() - if res.isErr(): + res.isOkOr: return err("Not found") return ok(res.value.key) @@ -285,14 +284,11 @@ method getMessages*( let catchable = catch: driver.getPage(maxPageSize, ascendingOrder, index, matchesQuery) - let pageRes: QueueDriverGetPageResult = - if catchable.isErr(): - return err(catchable.error.msg) - else: - catchable.get() + let pageRes: QueueDriverGetPageResult = catchable.valueOr: + return err(catchable.error.msg) - if pageRes.isErr(): - return err($pageRes.error) + pageRes.isOkOr: + return err($error) return ok(pageRes.value) diff --git a/waku/waku_archive/driver/sqlite_driver/migrations.nim b/waku/waku_archive/driver/sqlite_driver/migrations.nim index 33de5fec3..b077de19a 100644 --- a/waku/waku_archive/driver/sqlite_driver/migrations.nim +++ b/waku/waku_archive/driver/sqlite_driver/migrations.nim @@ -36,9 +36,8 @@ proc isSchemaVersion7*(db: SqliteDatabase): DatabaseResult[bool] = let query = """SELECT l.name FROM pragma_table_info("Message") as l WHERE l.pk != 0;""" - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err("failed to determine the current SchemaVersion: " & $res.error) + db.query(query, queryRowCallback).isOkOr: + return err("failed to determine the current SchemaVersion: " & $error) if pkColumns == @["pubsubTopic", "id", "storedAt"]: return ok(true) @@ -65,10 +64,8 @@ proc migrate*(db: SqliteDatabase, targetVersion = SchemaVersion): DatabaseResult ## Force the correct schema version ?db.setUserVersion(7) - let migrationRes = - migrate(db, targetVersion, migrationsScriptsDir = MessageStoreMigrationPath) - if migrationRes.isErr(): - return err("failed to execute migration scripts: " & migrationRes.error) + migrate(db, targetVersion, migrationsScriptsDir = MessageStoreMigrationPath).isOkOr: + return err("failed to execute migration scripts: " & error) info "finished message store's sqlite database migration" return ok() diff --git a/waku/waku_archive/driver/sqlite_driver/queries.nim b/waku/waku_archive/driver/sqlite_driver/queries.nim index 6fafc06eb..9ef6591c2 100644 --- a/waku/waku_archive/driver/sqlite_driver/queries.nim +++ b/waku/waku_archive/driver/sqlite_driver/queries.nim @@ -78,12 +78,11 @@ proc createTableQuery(table: string): SqlQueryStr = proc createTable*(db: SqliteDatabase): DatabaseResult[void] = let query = createTableQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Create indices @@ -93,12 +92,11 @@ proc createOldestMessageTimestampIndexQuery(table: string): SqlQueryStr = proc createOldestMessageTimestampIndex*(db: SqliteDatabase): DatabaseResult[void] = let query = createOldestMessageTimestampIndexQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Insert message @@ -129,8 +127,7 @@ proc getMessageCount*(db: SqliteDatabase): DatabaseResult[int64] = count = sqlite3_column_int64(s, 0) let query = countMessagesQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to count number of messages in the database") return ok(count) @@ -146,8 +143,7 @@ proc selectOldestTimestamp*(db: SqliteDatabase): DatabaseResult[Timestamp] {.inl timestamp = queryRowTimestampCallback(s, 0) let query = selectOldestMessageTimestampQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to get the oldest receiver timestamp from the database") return ok(timestamp) @@ -163,8 +159,7 @@ proc selectNewestTimestamp*(db: SqliteDatabase): DatabaseResult[Timestamp] {.inl timestamp = queryRowTimestampCallback(s, 0) let query = selectNewestMessageTimestampQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): + db.query(query, queryRowCallback).isOkOr: return err("failed to get the newest receiver timestamp from the database") return ok(timestamp) @@ -178,12 +173,11 @@ proc deleteMessagesOlderThanTimestamp*( db: SqliteDatabase, ts: int64 ): DatabaseResult[void] = let query = deleteMessagesOlderThanTimestampQuery(DbTable, ts) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Delete oldest messages not within limit @@ -199,12 +193,11 @@ proc deleteOldestMessagesNotWithinLimit*( ): DatabaseResult[void] = # NOTE: The word `limit` here refers the store capacity/maximum number-of-messages allowed limit let query = deleteOldestMessagesNotWithinLimitQuery(DbTable, limit = limit) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) + discard ?db.query( + query, + proc(s: ptr sqlite3_stmt) = + discard, + ) return ok() ## Select all messages diff --git a/waku/waku_archive/driver/sqlite_driver/sqlite_driver.nim b/waku/waku_archive/driver/sqlite_driver/sqlite_driver.nim index 173dd3e81..ff7b0e7d3 100644 --- a/waku/waku_archive/driver/sqlite_driver/sqlite_driver.nim +++ b/waku/waku_archive/driver/sqlite_driver/sqlite_driver.nim @@ -20,14 +20,12 @@ proc init(db: SqliteDatabase): ArchiveDriverResult[void] = return err("db not initialized") # Create table, if doesn't exist - let resCreate = createTable(db) - if resCreate.isErr(): - return err("failed to create table: " & resCreate.error()) + createTable(db).isOkOr: + return err("failed to create table: " & error) # Create indices, if don't exist - let resRtIndex = createOldestMessageTimestampIndex(db) - if resRtIndex.isErr(): - return err("failed to create i_ts index: " & resRtIndex.error()) + createOldestMessageTimestampIndex(db).isOkOr: + return err("failed to create i_ts index: " & error) return ok() @@ -37,9 +35,7 @@ type SqliteDriver* = ref object of ArchiveDriver proc new*(T: type SqliteDriver, db: SqliteDatabase): ArchiveDriverResult[T] = # Database initialization - let resInit = init(db) - if resInit.isErr(): - return err(resInit.error()) + ?init(db) # General initialization let insertStmt = db.prepareInsertMessageStmt() diff --git a/waku/waku_archive/retention_policy.nim b/waku/waku_archive/retention_policy.nim index d4b75ee1f..c2663fb66 100644 --- a/waku/waku_archive/retention_policy.nim +++ b/waku/waku_archive/retention_policy.nim @@ -11,3 +11,6 @@ method execute*( p: RetentionPolicy, store: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.base, async.} = discard + +method `$`*(p: RetentionPolicy): string {.base, gcsafe.} = + "unknown retention policy" diff --git a/waku/waku_archive/retention_policy/builder.nim b/waku/waku_archive/retention_policy/builder.nim index 6cb131bbc..7e777f4a0 100644 --- a/waku/waku_archive/retention_policy/builder.nim +++ b/waku/waku_archive/retention_policy/builder.nim @@ -7,7 +7,7 @@ import ./retention_policy_capacity, ./retention_policy_size -proc new*( +proc new( T: type RetentionPolicy, retPolicy: string ): RetentionPolicyResult[Option[RetentionPolicy]] = let retPolicy = retPolicy.toLower @@ -83,3 +83,14 @@ proc new*( return ok(some(retPolicy)) else: return err("unknown retention policy") + +proc new*( + T: typedesc[RetentionPolicy], retPolicies: seq[string] +): RetentionPolicyResult[seq[RetentionPolicy]] = + var policies: seq[RetentionPolicy] + for retPolicy in retPolicies: + let policy = RetentionPolicy.new(retPolicy).valueOr: + return err(error) + if policy.isSome(): + policies.add(policy.get()) + return ok(policies) diff --git a/waku/waku_archive/retention_policy/retention_policy_capacity.nim b/waku/waku_archive/retention_policy/retention_policy_capacity.nim index ed4dd2339..ff4da6861 100644 --- a/waku/waku_archive/retention_policy/retention_policy_capacity.nim +++ b/waku/waku_archive/retention_policy/retention_policy_capacity.nim @@ -50,6 +50,9 @@ proc new*(T: type CapacityRetentionPolicy, capacity = DefaultCapacity): T = capacity: capacity, totalCapacity: totalCapacity, deleteWindow: deleteWindow ) +method `$`*(p: CapacityRetentionPolicy): string = + "capacity:" & $p.capacity + method execute*( p: CapacityRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = diff --git a/waku/waku_archive/retention_policy/retention_policy_size.nim b/waku/waku_archive/retention_policy/retention_policy_size.nim index e60aba303..416d95ec0 100644 --- a/waku/waku_archive/retention_policy/retention_policy_size.nim +++ b/waku/waku_archive/retention_policy/retention_policy_size.nim @@ -15,6 +15,9 @@ type SizeRetentionPolicy* = ref object of RetentionPolicy proc new*(T: type SizeRetentionPolicy, size = DefaultRetentionSize): T = SizeRetentionPolicy(sizeLimit: size) +method `$`*(p: SizeRetentionPolicy): string = + "size:" & $p.sizeLimit & "b" + method execute*( p: SizeRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = diff --git a/waku/waku_archive/retention_policy/retention_policy_time.nim b/waku/waku_archive/retention_policy/retention_policy_time.nim index b0a548d2e..12f056c7b 100644 --- a/waku/waku_archive/retention_policy/retention_policy_time.nim +++ b/waku/waku_archive/retention_policy/retention_policy_time.nim @@ -6,34 +6,26 @@ import ../../waku_core, ../driver, ../retention_policy logScope: topics = "waku archive retention_policy" -const DefaultRetentionTime*: int64 = 30.days.seconds - type TimeRetentionPolicy* = ref object of RetentionPolicy retentionTime: chronos.Duration -proc new*(T: type TimeRetentionPolicy, retentionTime = DefaultRetentionTime): T = +proc new*(T: type TimeRetentionPolicy, retentionTime: int64): T = TimeRetentionPolicy(retentionTime: retentionTime.seconds) +method `$`*(p: TimeRetentionPolicy): string = + "time:" & $p.retentionTime.seconds + method execute*( p: TimeRetentionPolicy, driver: ArchiveDriver ): Future[RetentionPolicyResult[void]] {.async.} = - ## Delete messages that exceed the retention time by 10% and more (batch delete for efficiency) + ## Delete messages that exceed the retention time info "beginning of executing message retention policy - time" - let omtRes = await driver.getOldestMessageTimestamp() - if omtRes.isErr(): - return err("failed to get oldest message timestamp: " & omtRes.error) - let now = getNanosecondTime(getTime().toUnixFloat()) let retentionTimestamp = now - p.retentionTime.nanoseconds - let thresholdTimestamp = retentionTimestamp - p.retentionTime.nanoseconds div 10 - if thresholdTimestamp <= omtRes.value: - return ok() - - let res = await driver.deleteMessagesOlderThanTimestamp(ts = retentionTimestamp) - if res.isErr(): - return err("failed to delete oldest messages: " & res.error) + (await driver.deleteMessagesOlderThanTimestamp(ts = retentionTimestamp)).isOkOr: + return err("failed to delete oldest messages: " & error) info "end of executing message retention policy - time" return ok() diff --git a/waku/waku_archive_legacy.nim b/waku/waku_archive_legacy.nim deleted file mode 100644 index bcb6b6a54..000000000 --- a/waku/waku_archive_legacy.nim +++ /dev/null @@ -1,6 +0,0 @@ -import - ./waku_archive_legacy/common, - ./waku_archive_legacy/archive, - ./waku_archive_legacy/driver - -export common, archive, driver diff --git a/waku/waku_archive_legacy/archive.nim b/waku/waku_archive_legacy/archive.nim deleted file mode 100644 index 7bf5685a5..000000000 --- a/waku/waku_archive_legacy/archive.nim +++ /dev/null @@ -1,285 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import - std/[times, options, sequtils, algorithm], - stew/byteutils, - chronicles, - chronos, - metrics, - results -import - ../common/paging, - ./driver, - ../waku_core, - ../waku_core/message/digest, - ./common, - ./archive_metrics - -logScope: - topics = "waku archive" - -const - DefaultPageSize*: uint = 20 - MaxPageSize*: uint = 100 - - # Retention policy - WakuArchiveDefaultRetentionPolicyInterval* = chronos.minutes(30) - - # Metrics reporting - WakuArchiveDefaultMetricsReportInterval* = chronos.minutes(30) - - # Message validation - # 20 seconds maximum allowable sender timestamp "drift" - MaxMessageTimestampVariance* = getNanoSecondTime(20) - -type MessageValidator* = - proc(msg: WakuMessage): Result[void, string] {.closure, gcsafe, raises: [].} - -## Archive - -type WakuArchive* = ref object - driver: ArchiveDriver - - validator: MessageValidator - -proc validate*(msg: WakuMessage): Result[void, string] = - if msg.ephemeral: - # Ephemeral message, do not store - return - - if msg.timestamp == 0: - return ok() - - let - now = getNanosecondTime(getTime().toUnixFloat()) - lowerBound = now - MaxMessageTimestampVariance - upperBound = now + MaxMessageTimestampVariance - - if msg.timestamp < lowerBound: - return err(invalidMessageOld) - - if upperBound < msg.timestamp: - return err(invalidMessageFuture) - - return ok() - -proc new*( - T: type WakuArchive, driver: ArchiveDriver, validator: MessageValidator = validate -): Result[T, string] = - if driver.isNil(): - return err("archive driver is Nil") - - let archive = WakuArchive(driver: driver, validator: validator) - - return ok(archive) - -proc handleMessage*( - self: WakuArchive, pubsubTopic: PubsubTopic, msg: WakuMessage -) {.async.} = - let - msgDigest = computeDigest(msg) - msgDigestHex = msgDigest.data.to0xHex() - msgHash = computeMessageHash(pubsubTopic, msg) - msgHashHex = msgHash.to0xHex() - msgTimestamp = - if msg.timestamp > 0: - msg.timestamp - else: - getNanosecondTime(getTime().toUnixFloat()) - - trace "handling message", - msg_hash = msgHashHex, - pubsubTopic = pubsubTopic, - contentTopic = msg.contentTopic, - msgTimestamp = msg.timestamp, - digest = msgDigestHex - - self.validator(msg).isOkOr: - waku_legacy_archive_errors.inc(labelValues = [error]) - trace "invalid message", - msg_hash = msgHashHex, - pubsubTopic = pubsubTopic, - contentTopic = msg.contentTopic, - timestamp = msg.timestamp, - error = error - return - - let insertStartTime = getTime().toUnixFloat() - - (await self.driver.put(pubsubTopic, msg, msgDigest, msgHash, msgTimestamp)).isOkOr: - waku_legacy_archive_errors.inc(labelValues = [insertFailure]) - error "failed to insert message", - msg_hash = msgHashHex, - pubsubTopic = pubsubTopic, - contentTopic = msg.contentTopic, - timestamp = msg.timestamp, - error = error - return - - let insertDuration = getTime().toUnixFloat() - insertStartTime - waku_legacy_archive_insert_duration_seconds.observe(insertDuration) - - info "message archived", - msg_hash = msgHashHex, - pubsubTopic = pubsubTopic, - contentTopic = msg.contentTopic, - msgTimestamp = msg.timestamp, - digest = msgDigestHex, - insertDuration = insertDuration - -proc findMessages*( - self: WakuArchive, query: ArchiveQuery -): Future[ArchiveResult] {.async, gcsafe.} = - ## Search the archive to return a single page of messages matching the query criteria - - let maxPageSize = - if query.pageSize <= 0: - DefaultPageSize - else: - min(query.pageSize, MaxPageSize) - - let isAscendingOrder = query.direction.into() - - if query.contentTopics.len > 10: - return err(ArchiveError.invalidQuery("too many content topics")) - - if query.cursor.isSome() and query.cursor.get().hash.len != 32: - return err(ArchiveError.invalidQuery("invalid cursor hash length")) - - let queryStartTime = getTime().toUnixFloat() - - let rows = ( - await self.driver.getMessages( - includeData = query.includeData, - contentTopic = query.contentTopics, - pubsubTopic = query.pubsubTopic, - cursor = query.cursor, - startTime = query.startTime, - endTime = query.endTime, - hashes = query.hashes, - maxPageSize = maxPageSize + 1, - ascendingOrder = isAscendingOrder, - requestId = query.requestId, - ) - ).valueOr: - return err(ArchiveError(kind: ArchiveErrorKind.DRIVER_ERROR, cause: error)) - - let queryDuration = getTime().toUnixFloat() - queryStartTime - waku_legacy_archive_query_duration_seconds.observe(queryDuration) - - var hashes = newSeq[WakuMessageHash]() - var messages = newSeq[WakuMessage]() - var topics = newSeq[PubsubTopic]() - var cursor = none(ArchiveCursor) - - if rows.len == 0: - return ok(ArchiveResponse(hashes: hashes, messages: messages, cursor: cursor)) - - ## Messages - let pageSize = min(rows.len, int(maxPageSize)) - - if query.includeData: - topics = rows[0 ..< pageSize].mapIt(it[0]) - messages = rows[0 ..< pageSize].mapIt(it[1]) - - hashes = rows[0 ..< pageSize].mapIt(it[4]) - - ## Cursor - if rows.len > int(maxPageSize): - ## Build last message cursor - ## The cursor is built from the last message INCLUDED in the response - ## (i.e. the second last message in the rows list) - - let (pubsubTopic, message, digest, storeTimestamp, hash) = rows[^2] - - cursor = some( - ArchiveCursor( - digest: MessageDigest.fromBytes(digest), - storeTime: storeTimestamp, - sendertime: message.timestamp, - pubsubTopic: pubsubTopic, - hash: hash, - ) - ) - - # All messages MUST be returned in chronological order - if not isAscendingOrder: - reverse(hashes) - reverse(messages) - reverse(topics) - - return ok( - ArchiveResponse(hashes: hashes, messages: messages, topics: topics, cursor: cursor) - ) - -proc findMessagesV2*( - self: WakuArchive, query: ArchiveQuery -): Future[ArchiveResult] {.async, deprecated, gcsafe.} = - ## Search the archive to return a single page of messages matching the query criteria - - let maxPageSize = - if query.pageSize <= 0: - DefaultPageSize - else: - min(query.pageSize, MaxPageSize) - - let isAscendingOrder = query.direction.into() - - if query.contentTopics.len > 10: - return err(ArchiveError.invalidQuery("too many content topics")) - - let queryStartTime = getTime().toUnixFloat() - - let rows = ( - await self.driver.getMessagesV2( - contentTopic = query.contentTopics, - pubsubTopic = query.pubsubTopic, - cursor = query.cursor, - startTime = query.startTime, - endTime = query.endTime, - maxPageSize = maxPageSize + 1, - ascendingOrder = isAscendingOrder, - requestId = query.requestId, - ) - ).valueOr: - return err(ArchiveError(kind: ArchiveErrorKind.DRIVER_ERROR, cause: error)) - - let queryDuration = getTime().toUnixFloat() - queryStartTime - waku_legacy_archive_query_duration_seconds.observe(queryDuration) - - var messages = newSeq[WakuMessage]() - var cursor = none(ArchiveCursor) - - if rows.len == 0: - return ok(ArchiveResponse(messages: messages, cursor: cursor)) - - ## Messages - let pageSize = min(rows.len, int(maxPageSize)) - - messages = rows[0 ..< pageSize].mapIt(it[1]) - - ## Cursor - if rows.len > int(maxPageSize): - ## Build last message cursor - ## The cursor is built from the last message INCLUDED in the response - ## (i.e. the second last message in the rows list) - - let (pubsubTopic, message, digest, storeTimestamp, _) = rows[^2] - - cursor = some( - ArchiveCursor( - digest: MessageDigest.fromBytes(digest), - storeTime: storeTimestamp, - sendertime: message.timestamp, - pubsubTopic: pubsubTopic, - ) - ) - - # All messages MUST be returned in chronological order - if not isAscendingOrder: - reverse(messages) - - return ok(ArchiveResponse(messages: messages, cursor: cursor)) diff --git a/waku/waku_archive_legacy/archive_metrics.nim b/waku/waku_archive_legacy/archive_metrics.nim deleted file mode 100644 index c3569a1ea..000000000 --- a/waku/waku_archive_legacy/archive_metrics.nim +++ /dev/null @@ -1,22 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import metrics - -declarePublicGauge waku_legacy_archive_messages, - "number of historical messages", ["type"] -declarePublicCounter waku_legacy_archive_errors, - "number of store protocol errors", ["type"] -declarePublicHistogram waku_legacy_archive_insert_duration_seconds, - "message insertion duration" -declarePublicHistogram waku_legacy_archive_query_duration_seconds, - "history query duration" - -# Error types (metric label values) -const - invalidMessageOld* = "invalid_message_too_old" - invalidMessageFuture* = "invalid_message_future_timestamp" - insertFailure* = "insert_failure" - retPolicyFailure* = "retpolicy_failure" diff --git a/waku/waku_archive_legacy/common.nim b/waku/waku_archive_legacy/common.nim deleted file mode 100644 index ee45181cb..000000000 --- a/waku/waku_archive_legacy/common.nim +++ /dev/null @@ -1,88 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/options, results, stew/byteutils, stew/arrayops, nimcrypto/sha2 -import ../waku_core, ../common/paging - -## Waku message digest - -type MessageDigest* = MDigest[256] - -proc fromBytes*(T: type MessageDigest, src: seq[byte]): T = - var data: array[32, byte] - - let byteCount = copyFrom[byte](data, src) - - assert byteCount == 32 - - return MessageDigest(data: data) - -proc computeDigest*(msg: WakuMessage): MessageDigest = - var ctx: sha256 - ctx.init() - defer: - ctx.clear() - - ctx.update(msg.contentTopic.toBytes()) - ctx.update(msg.payload) - - # Computes the hash - return ctx.finish() - -## Public API types - -type - #TODO Once Store v2 is removed, the cursor becomes the hash of the last message - ArchiveCursor* = object - digest*: MessageDigest - storeTime*: Timestamp - senderTime*: Timestamp - pubsubTopic*: PubsubTopic - hash*: WakuMessageHash - - ArchiveQuery* = object - includeData*: bool # indicate if messages should be returned in addition to hashes. - pubsubTopic*: Option[PubsubTopic] - contentTopics*: seq[ContentTopic] - cursor*: Option[ArchiveCursor] - startTime*: Option[Timestamp] - endTime*: Option[Timestamp] - hashes*: seq[WakuMessageHash] - pageSize*: uint - direction*: PagingDirection - requestId*: string - - ArchiveResponse* = object - hashes*: seq[WakuMessageHash] - messages*: seq[WakuMessage] - topics*: seq[PubsubTopic] - cursor*: Option[ArchiveCursor] - - ArchiveErrorKind* {.pure.} = enum - UNKNOWN = uint32(0) - DRIVER_ERROR = uint32(1) - INVALID_QUERY = uint32(2) - - ArchiveError* = object - case kind*: ArchiveErrorKind - of DRIVER_ERROR, INVALID_QUERY: - # TODO: Add an enum to be able to distinguish between error causes - cause*: string - else: - discard - - ArchiveResult* = Result[ArchiveResponse, ArchiveError] - -proc `$`*(err: ArchiveError): string = - case err.kind - of ArchiveErrorKind.DRIVER_ERROR: - "DRIVER_ERROR: " & err.cause - of ArchiveErrorKind.INVALID_QUERY: - "INVALID_QUERY: " & err.cause - of ArchiveErrorKind.UNKNOWN: - "UNKNOWN" - -proc invalidQuery*(T: type ArchiveError, cause: string): T = - ArchiveError(kind: ArchiveErrorKind.INVALID_QUERY, cause: cause) diff --git a/waku/waku_archive_legacy/driver.nim b/waku/waku_archive_legacy/driver.nim deleted file mode 100644 index 8ff8df029..000000000 --- a/waku/waku_archive_legacy/driver.nim +++ /dev/null @@ -1,121 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/options, results, chronos -import ../waku_core, ./common - -const DefaultPageSize*: uint = 25 - -type - ArchiveDriverResult*[T] = Result[T, string] - ArchiveDriver* = ref object of RootObj - -#TODO Once Store v2 is removed keep only messages and hashes -type ArchiveRow* = (PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash) - -# ArchiveDriver interface - -method put*( - driver: ArchiveDriver, - pubsubTopic: PubsubTopic, - message: WakuMessage, - digest: MessageDigest, - messageHash: WakuMessageHash, - receivedTime: Timestamp, -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method getAllMessages*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.base, async.} = - discard - -method getMessagesV2*( - driver: ArchiveDriver, - contentTopic = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.base, deprecated, async.} = - discard - -method getMessages*( - driver: ArchiveDriver, - includeData = true, - contentTopic = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = newSeq[WakuMessageHash](0), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId = "", -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.base, async.} = - discard - -method getMessagesCount*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[int64]] {.base, async.} = - discard - -method getPagesCount*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[int64]] {.base, async.} = - discard - -method getPagesSize*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[int64]] {.base, async.} = - discard - -method getDatabaseSize*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[int64]] {.base, async.} = - discard - -method performVacuum*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method getOldestMessageTimestamp*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[Timestamp]] {.base, async.} = - discard - -method getNewestMessageTimestamp*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[Timestamp]] {.base, async.} = - discard - -method deleteMessagesOlderThanTimestamp*( - driver: ArchiveDriver, ts: Timestamp -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method deleteOldestMessagesNotWithinLimit*( - driver: ArchiveDriver, limit: int -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method decreaseDatabaseSize*( - driver: ArchiveDriver, targetSizeInBytes: int64, forceRemoval: bool = false -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method close*( - driver: ArchiveDriver -): Future[ArchiveDriverResult[void]] {.base, async.} = - discard - -method existsTable*( - driver: ArchiveDriver, tableName: string -): Future[ArchiveDriverResult[bool]] {.base, async.} = - discard diff --git a/waku/waku_archive_legacy/driver/builder.nim b/waku/waku_archive_legacy/driver/builder.nim deleted file mode 100644 index d73803b81..000000000 --- a/waku/waku_archive_legacy/driver/builder.nim +++ /dev/null @@ -1,104 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import results, chronicles, chronos -import - ../driver, - ../../common/databases/dburl, - ../../common/databases/db_sqlite, - ../../common/error_handling, - ./sqlite_driver, - ./sqlite_driver/migrations as archive_driver_sqlite_migrations, - ./queue_driver - -export sqlite_driver, queue_driver - -when defined(postgres): - import ## These imports add dependency with an external libpq library - ./postgres_driver - export postgres_driver - -proc new*( - T: type ArchiveDriver, - url: string, - vacuum: bool, - migrate: bool, - maxNumConn: int, - onFatalErrorAction: OnFatalErrorHandler, -): Future[Result[T, string]] {.async.} = - ## url - string that defines the database - ## vacuum - if true, a cleanup operation will be applied to the database - ## migrate - if true, the database schema will be updated - ## maxNumConn - defines the maximum number of connections to handle simultaneously (Postgres) - ## onFatalErrorAction - called if, e.g., the connection with db got lost - - let dbUrlValidationRes = dburl.validateDbUrl(url) - if dbUrlValidationRes.isErr(): - return err("DbUrl failure in ArchiveDriver.new: " & dbUrlValidationRes.error) - - let engineRes = dburl.getDbEngine(url) - if engineRes.isErr(): - return err("error getting db engine in setupWakuArchiveDriver: " & engineRes.error) - - let engine = engineRes.get() - - case engine - of "sqlite": - let pathRes = dburl.getDbPath(url) - if pathRes.isErr(): - return err("error get path in setupWakuArchiveDriver: " & pathRes.error) - - let dbRes = SqliteDatabase.new(pathRes.get()) - if dbRes.isErr(): - return err("error in setupWakuArchiveDriver: " & dbRes.error) - - let db = dbRes.get() - - # SQLite vacuum - let sqliteStatsRes = db.gatherSqlitePageStats() - if sqliteStatsRes.isErr(): - return err("error while gathering sqlite stats: " & $sqliteStatsRes.error) - - let (pageSize, pageCount, freelistCount) = sqliteStatsRes.get() - info "sqlite database page stats", - pageSize = pageSize, pages = pageCount, freePages = freelistCount - - if vacuum and (pageCount > 0 and freelistCount > 0): - let vacuumRes = db.performSqliteVacuum() - if vacuumRes.isErr(): - return err("error in vacuum sqlite: " & $vacuumRes.error) - - # Database migration - if migrate: - let migrateRes = archive_driver_sqlite_migrations.migrate(db) - if migrateRes.isErr(): - return err("error in migrate sqlite: " & $migrateRes.error) - - info "setting up sqlite waku archive driver" - let res = SqliteDriver.new(db) - if res.isErr(): - return err("failed to init sqlite archive driver: " & res.error) - - return ok(res.get()) - of "postgres": - when defined(postgres): - let res = PostgresDriver.new( - dbUrl = url, - maxConnections = maxNumConn, - onFatalErrorAction = onFatalErrorAction, - ) - if res.isErr(): - return err("failed to init postgres archive driver: " & res.error) - - let driver = res.get() - return ok(driver) - else: - return err( - "Postgres has been configured but not been compiled. Check compiler definitions." - ) - else: - info "setting up in-memory waku archive driver" - let driver = QueueDriver.new() # Defaults to a capacity of 25.000 messages - return ok(driver) diff --git a/waku/waku_archive_legacy/driver/postgres_driver.nim b/waku/waku_archive_legacy/driver/postgres_driver.nim deleted file mode 100644 index 496005cbe..000000000 --- a/waku/waku_archive_legacy/driver/postgres_driver.nim +++ /dev/null @@ -1,8 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import ./postgres_driver/postgres_driver - -export postgres_driver diff --git a/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim b/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim deleted file mode 100644 index 56d388b6d..000000000 --- a/waku/waku_archive_legacy/driver/postgres_driver/postgres_driver.nim +++ /dev/null @@ -1,978 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import - std/[options, sequtils, strutils, strformat, times], - stew/[byteutils, arrayops], - results, - chronos, - db_connector/[postgres, db_common], - chronicles -import - ../../../common/error_handling, - ../../../waku_core, - ../../common, - ../../driver, - ./postgres_healthcheck, - ../../../common/databases/db_postgres as waku_postgres - -type PostgresDriver* = ref object of ArchiveDriver - ## Establish a separate pools for read/write operations - writeConnPool: PgAsyncPool - readConnPool: PgAsyncPool - -const InsertRowStmtName = "InsertRow" -const InsertRowStmtDefinition = # TODO: get the sql queries from a file - """INSERT INTO messages (id, messageHash, contentTopic, payload, pubsubTopic, - version, timestamp, meta) VALUES ($1, $2, $3, $4, $5, $6, $7, CASE WHEN $8 = '' THEN NULL ELSE $8 END) ON CONFLICT DO NOTHING;""" - -const InsertRowInMessagesLookupStmtName = "InsertRowMessagesLookup" -const InsertRowInMessagesLookupStmtDefinition = - """INSERT INTO messages_lookup (messageHash, timestamp) VALUES ($1, $2) ON CONFLICT DO NOTHING;""" - -const SelectNoCursorAscStmtName = "SelectWithoutCursorAsc" -const SelectNoCursorAscStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - messageHash IN ($2) AND - pubsubTopic = $3 AND - timestamp >= $4 AND - timestamp <= $5 - ORDER BY timestamp ASC, messageHash ASC LIMIT $6;""" - -const SelectNoCursorDescStmtName = "SelectWithoutCursorDesc" -const SelectNoCursorDescStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - messageHash IN ($2) AND - pubsubTopic = $3 AND - timestamp >= $4 AND - timestamp <= $5 - ORDER BY timestamp DESC, messageHash DESC LIMIT $6;""" - -const SelectWithCursorDescStmtName = "SelectWithCursorDesc" -const SelectWithCursorDescStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - messageHash IN ($2) AND - pubsubTopic = $3 AND - (timestamp, messageHash) < ($4,$5) AND - timestamp >= $6 AND - timestamp <= $7 - ORDER BY timestamp DESC, messageHash DESC LIMIT $8;""" - -const SelectWithCursorAscStmtName = "SelectWithCursorAsc" -const SelectWithCursorAscStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - messageHash IN ($2) AND - pubsubTopic = $3 AND - (timestamp, messageHash) > ($4,$5) AND - timestamp >= $6 AND - timestamp <= $7 - ORDER BY timestamp ASC, messageHash ASC LIMIT $8;""" - -const SelectMessageByHashName = "SelectMessageByHash" -const SelectMessageByHashDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages WHERE messageHash = $1""" - -const SelectNoCursorV2AscStmtName = "SelectWithoutCursorV2Asc" -const SelectNoCursorV2AscStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - pubsubTopic = $2 AND - timestamp >= $3 AND - timestamp <= $4 - ORDER BY timestamp ASC LIMIT $5;""" - -const SelectNoCursorV2DescStmtName = "SelectWithoutCursorV2Desc" -const SelectNoCursorV2DescStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - pubsubTopic = $2 AND - timestamp >= $3 AND - timestamp <= $4 - ORDER BY timestamp DESC LIMIT $5;""" - -const SelectWithCursorV2DescStmtName = "SelectWithCursorV2Desc" -const SelectWithCursorV2DescStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - pubsubTopic = $2 AND - (timestamp, id) < ($3,$4) AND - timestamp >= $5 AND - timestamp <= $6 - ORDER BY timestamp DESC LIMIT $7;""" - -const SelectWithCursorV2AscStmtName = "SelectWithCursorV2Asc" -const SelectWithCursorV2AscStmtDef = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages - WHERE contentTopic IN ($1) AND - pubsubTopic = $2 AND - (timestamp, id) > ($3,$4) AND - timestamp >= $5 AND - timestamp <= $6 - ORDER BY timestamp ASC LIMIT $7;""" - -const DefaultMaxNumConns = 50 - -proc new*( - T: type PostgresDriver, - dbUrl: string, - maxConnections = DefaultMaxNumConns, - onFatalErrorAction: OnFatalErrorHandler = nil, -): ArchiveDriverResult[T] = - ## Very simplistic split of max connections - let maxNumConnOnEachPool = int(maxConnections / 2) - - let readConnPool = PgAsyncPool.new(dbUrl, maxNumConnOnEachPool).valueOr: - return err("error creating read conn pool PgAsyncPool") - - let writeConnPool = PgAsyncPool.new(dbUrl, maxNumConnOnEachPool).valueOr: - return err("error creating write conn pool PgAsyncPool") - - if not isNil(onFatalErrorAction): - asyncSpawn checkConnectivity(readConnPool, onFatalErrorAction) - - if not isNil(onFatalErrorAction): - asyncSpawn checkConnectivity(writeConnPool, onFatalErrorAction) - - let driver = PostgresDriver(writeConnPool: writeConnPool, readConnPool: readConnPool) - return ok(driver) - -proc reset*(s: PostgresDriver): Future[ArchiveDriverResult[void]] {.async.} = - ## Clear the database partitions - let targetSize = 0 - let forceRemoval = true - let ret = await s.decreaseDatabaseSize(targetSize, forceRemoval) - return ret - -proc rowCallbackImpl( - pqResult: ptr PGresult, - outRows: var seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)], -) = - ## Proc aimed to contain the logic of the callback passed to the `psasyncpool`. - ## That callback is used in "SELECT" queries. - ## - ## pqResult - contains the query results - ## outRows - seq of Store-rows. This is populated from the info contained in pqResult - - let numFields = pqResult.pqnfields() - if numFields != 8: - error "Wrong number of fields, expected 8", numFields - return - - for iRow in 0 ..< pqResult.pqNtuples(): - var wakuMessage: WakuMessage - var timestamp: Timestamp - var version: uint - var pubSubTopic: string - var contentTopic: string - var digest: string - var payload: string - var hashHex: string - var msgHash: WakuMessageHash - var meta: string - - try: - contentTopic = $(pqgetvalue(pqResult, iRow, 0)) - payload = parseHexStr($(pqgetvalue(pqResult, iRow, 1))) - pubSubTopic = $(pqgetvalue(pqResult, iRow, 2)) - version = parseUInt($(pqgetvalue(pqResult, iRow, 3))) - timestamp = parseInt($(pqgetvalue(pqResult, iRow, 4))) - digest = parseHexStr($(pqgetvalue(pqResult, iRow, 5))) - hashHex = parseHexStr($(pqgetvalue(pqResult, iRow, 6))) - meta = parseHexStr($(pqgetvalue(pqResult, iRow, 7))) - msgHash = fromBytes(hashHex.toOpenArrayByte(0, 31)) - except ValueError: - error "could not parse correctly", error = getCurrentExceptionMsg() - - wakuMessage.timestamp = timestamp - wakuMessage.version = uint32(version) - wakuMessage.contentTopic = contentTopic - wakuMessage.payload = @(payload.toOpenArrayByte(0, payload.high)) - wakuMessage.meta = @(meta.toOpenArrayByte(0, meta.high)) - - outRows.add( - ( - pubSubTopic, - wakuMessage, - @(digest.toOpenArrayByte(0, digest.high)), - timestamp, - msgHash, - ) - ) - -method put*( - s: PostgresDriver, - pubsubTopic: PubsubTopic, - message: WakuMessage, - digest: MessageDigest, - messageHash: WakuMessageHash, - receivedTime: Timestamp, -): Future[ArchiveDriverResult[void]] {.async.} = - let digest = toHex(digest.data) - let messageHash = toHex(messageHash) - let contentTopic = message.contentTopic - let payload = toHex(message.payload) - let version = $message.version - let timestamp = $message.timestamp - let meta = toHex(message.meta) - - trace "put PostgresDriver", timestamp = timestamp - - ( - await s.writeConnPool.runStmt( - InsertRowStmtName, - InsertRowStmtDefinition, - @[ - digest, messageHash, contentTopic, payload, pubsubTopic, version, timestamp, - meta, - ], - @[ - int32(digest.len), - int32(messageHash.len), - int32(contentTopic.len), - int32(payload.len), - int32(pubsubTopic.len), - int32(version.len), - int32(timestamp.len), - int32(meta.len), - ], - @[int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0)], - ) - ).isOkOr: - return err("could not put msg in messages table: " & $error) - - ## Now add the row to messages_lookup - return await s.writeConnPool.runStmt( - InsertRowInMessagesLookupStmtName, - InsertRowInMessagesLookupStmtDefinition, - @[messageHash, timestamp], - @[int32(messageHash.len), int32(timestamp.len)], - @[int32(0), int32(0)], - ) - -method getAllMessages*( - s: PostgresDriver -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## Retrieve all messages from the store. - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - ( - await s.readConnPool.pgQuery( - """SELECT contentTopic, - payload, pubsubTopic, version, timestamp, - id, messageHash, meta FROM messages ORDER BY timestamp ASC""", - newSeq[string](0), - rowCallback, - ) - ).isOkOr: - return err("failed in query: " & $error) - - return ok(rows) - -proc getMessagesArbitraryQuery( - s: PostgresDriver, - contentTopic: seq[ContentTopic] = @[], - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hexHashes: seq[string] = @[], - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## This proc allows to handle atypical queries. We don't use prepared statements for those. - - var query = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages""" - var statements: seq[string] - var args: seq[string] - - if contentTopic.len > 0: - let cstmt = "contentTopic IN (" & "?".repeat(contentTopic.len).join(",") & ")" - statements.add(cstmt) - for t in contentTopic: - args.add(t) - - if hexHashes.len > 0: - let cstmt = "messageHash IN (" & "?".repeat(hexHashes.len).join(",") & ")" - statements.add(cstmt) - for t in hexHashes: - args.add(t) - - if pubsubTopic.isSome(): - statements.add("pubsubTopic = ?") - args.add(pubsubTopic.get()) - - if cursor.isSome(): - let hashHex = toHex(cursor.get().hash) - - var entree: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc entreeCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, entree) - - ( - await s.readConnPool.runStmt( - SelectMessageByHashName, - SelectMessageByHashDef, - @[hashHex], - @[int32(hashHex.len)], - @[int32(0)], - entreeCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query with cursor: " & $error) - - if entree.len == 0: - return ok(entree) - - let storetime = entree[0][3] - - let comp = if ascendingOrder: ">" else: "<" - statements.add("(timestamp, messageHash) " & comp & " (?,?)") - args.add($storetime) - args.add(hashHex) - - if startTime.isSome(): - statements.add("timestamp >= ?") - args.add($startTime.get()) - - if endTime.isSome(): - statements.add("timestamp <= ?") - args.add($endTime.get()) - - if statements.len > 0: - query &= " WHERE " & statements.join(" AND ") - - var direction: string - if ascendingOrder: - direction = "ASC" - else: - direction = "DESC" - - query &= " ORDER BY timestamp " & direction & ", messageHash " & direction - - query &= " LIMIT ?" - args.add($maxPageSize) - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - (await s.readConnPool.pgQuery(query, args, rowCallback, requestId)).isOkOr: - return err("failed to run query: " & $error) - - return ok(rows) - -proc getMessagesV2ArbitraryQuery( - s: PostgresDriver, - contentTopic: seq[ContentTopic] = @[], - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async, deprecated.} = - ## This proc allows to handle atypical queries. We don't use prepared statements for those. - - var query = - """SELECT contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta FROM messages""" - var statements: seq[string] - var args: seq[string] - - if contentTopic.len > 0: - let cstmt = "contentTopic IN (" & "?".repeat(contentTopic.len).join(",") & ")" - statements.add(cstmt) - for t in contentTopic: - args.add(t) - - if pubsubTopic.isSome(): - statements.add("pubsubTopic = ?") - args.add(pubsubTopic.get()) - - if cursor.isSome(): - let comp = if ascendingOrder: ">" else: "<" - statements.add("(timestamp, id) " & comp & " (?,?)") - args.add($cursor.get().storeTime) - args.add(toHex(cursor.get().digest.data)) - - if startTime.isSome(): - statements.add("timestamp >= ?") - args.add($startTime.get()) - - if endTime.isSome(): - statements.add("timestamp <= ?") - args.add($endTime.get()) - - if statements.len > 0: - query &= " WHERE " & statements.join(" AND ") - - var direction: string - if ascendingOrder: - direction = "ASC" - else: - direction = "DESC" - - query &= " ORDER BY timestamp " & direction & ", id " & direction - - query &= " LIMIT ?" - args.add($maxPageSize) - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - (await s.readConnPool.pgQuery(query, args, rowCallback, requestId)).isOkOr: - return err("failed to run query: " & $error) - - return ok(rows) - -proc getMessagesPreparedStmt( - s: PostgresDriver, - contentTopic: string, - pubsubTopic: PubsubTopic, - cursor = none(ArchiveCursor), - startTime: Timestamp, - endTime: Timestamp, - hashes: string, - maxPageSize = DefaultPageSize, - ascOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## This proc aims to run the most typical queries in a more performant way, i.e. by means of - ## prepared statements. - ## - ## contentTopic - string with list of conten topics. e.g: "'ctopic1','ctopic2','ctopic3'" - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - let startTimeStr = $startTime - let endTimeStr = $endTime - let limit = $maxPageSize - - if cursor.isSome(): - let hash = toHex(cursor.get().hash) - - var entree: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - - proc entreeCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, entree) - - ( - await s.readConnPool.runStmt( - SelectMessageByHashName, - SelectMessageByHashDef, - @[hash], - @[int32(hash.len)], - @[int32(0)], - entreeCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query with cursor: " & $error) - - if entree.len == 0: - return ok(entree) - - let timestamp = $entree[0][3] - - var stmtName = - if ascOrder: SelectWithCursorAscStmtName else: SelectWithCursorDescStmtName - var stmtDef = - if ascOrder: SelectWithCursorAscStmtDef else: SelectWithCursorDescStmtDef - - ( - await s.readConnPool.runStmt( - stmtName, - stmtDef, - @[ - contentTopic, hashes, pubsubTopic, timestamp, hash, startTimeStr, endTimeStr, - limit, - ], - @[ - int32(contentTopic.len), - int32(hashes.len), - int32(pubsubTopic.len), - int32(timestamp.len), - int32(hash.len), - int32(startTimeStr.len), - int32(endTimeStr.len), - int32(limit.len), - ], - @[ - int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0) - ], - rowCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query with cursor: " & $error) - else: - var stmtName = - if ascOrder: SelectNoCursorAscStmtName else: SelectNoCursorDescStmtName - var stmtDef = if ascOrder: SelectNoCursorAscStmtDef else: SelectNoCursorDescStmtDef - - ( - await s.readConnPool.runStmt( - stmtName, - stmtDef, - @[contentTopic, hashes, pubsubTopic, startTimeStr, endTimeStr, limit], - @[ - int32(contentTopic.len), - int32(hashes.len), - int32(pubsubTopic.len), - int32(startTimeStr.len), - int32(endTimeStr.len), - int32(limit.len), - ], - @[int32(0), int32(0), int32(0), int32(0), int32(0), int32(0)], - rowCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query without cursor: " & $error) - - return ok(rows) - -proc getMessagesV2PreparedStmt( - s: PostgresDriver, - contentTopic: string, - pubsubTopic: PubsubTopic, - cursor = none(ArchiveCursor), - startTime: Timestamp, - endTime: Timestamp, - maxPageSize = DefaultPageSize, - ascOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async, deprecated.} = - ## This proc aims to run the most typical queries in a more performant way, i.e. by means of - ## prepared statements. - ## - ## contentTopic - string with list of conten topics. e.g: "'ctopic1','ctopic2','ctopic3'" - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - let startTimeStr = $startTime - let endTimeStr = $endTime - let limit = $maxPageSize - - if cursor.isSome(): - var stmtName = - if ascOrder: SelectWithCursorV2AscStmtName else: SelectWithCursorV2DescStmtName - var stmtDef = - if ascOrder: SelectWithCursorV2AscStmtDef else: SelectWithCursorV2DescStmtDef - - let digest = toHex(cursor.get().digest.data) - let timestamp = $cursor.get().storeTime - - ( - await s.readConnPool.runStmt( - stmtName, - stmtDef, - @[contentTopic, pubsubTopic, timestamp, digest, startTimeStr, endTimeStr, limit], - @[ - int32(contentTopic.len), - int32(pubsubTopic.len), - int32(timestamp.len), - int32(digest.len), - int32(startTimeStr.len), - int32(endTimeStr.len), - int32(limit.len), - ], - @[int32(0), int32(0), int32(0), int32(0), int32(0), int32(0), int32(0)], - rowCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query with cursor: " & $error) - else: - var stmtName = - if ascOrder: SelectNoCursorV2AscStmtName else: SelectNoCursorV2DescStmtName - var stmtDef = - if ascOrder: SelectNoCursorV2AscStmtDef else: SelectNoCursorV2DescStmtDef - - ( - await s.readConnPool.runStmt( - stmtName, - stmtDef, - @[contentTopic, pubsubTopic, startTimeStr, endTimeStr, limit], - @[ - int32(contentTopic.len), - int32(pubsubTopic.len), - int32(startTimeStr.len), - int32(endTimeStr.len), - int32(limit.len), - ], - @[int32(0), int32(0), int32(0), int32(0), int32(0)], - rowCallback, - requestId, - ) - ).isOkOr: - return err("failed to run query without cursor: " & $error) - - return ok(rows) - -proc getMessagesByMessageHashes( - s: PostgresDriver, hashes: string, maxPageSize: uint, requestId: string -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## Retrieves information only filtering by a given messageHashes list. - ## This proc levarages on the messages_lookup table to have better query performance - ## and only query the desired partitions in the partitioned messages table - var query = - fmt""" - WITH min_timestamp AS ( - SELECT MIN(timestamp) AS min_ts - FROM messages_lookup - WHERE messagehash IN ( - {hashes} - ) - ) - SELECT contentTopic, payload, pubsubTopic, version, m.timestamp, id, m.messageHash, meta - FROM messages m - INNER JOIN - messages_lookup l - ON - m.timestamp = l.timestamp - AND m.messagehash = l.messagehash - WHERE - l.timestamp >= (SELECT min_ts FROM min_timestamp) - AND l.messagehash IN ( - {hashes} - ) - ORDER BY - m.timestamp DESC, - m.messagehash DESC - LIMIT {maxPageSize}; - """ - - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc rowCallback(pqResult: ptr PGresult) = - rowCallbackImpl(pqResult, rows) - - ( - await s.readConnPool.pgQuery( - query = query, rowCallback = rowCallback, requestId = requestId - ) - ).isOkOr: - return err("failed to run query: " & $error) - - return ok(rows) - -method getMessages*( - s: PostgresDriver, - includeData = true, - contentTopicSeq = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = newSeq[WakuMessageHash](0), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId = "", -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - let hexHashes = hashes.mapIt(toHex(it)) - - if cursor.isNone() and pubsubTopic.isNone() and contentTopicSeq.len == 0 and - startTime.isNone() and endTime.isNone() and hexHashes.len > 0: - return await s.getMessagesByMessageHashes( - "'" & hexHashes.join("','") & "'", maxPageSize, requestId - ) - - if contentTopicSeq.len == 1 and hexHashes.len == 1 and pubsubTopic.isSome() and - startTime.isSome() and endTime.isSome(): - ## Considered the most common query. Therefore, we use prepared statements to optimize it. - return await s.getMessagesPreparedStmt( - contentTopicSeq.join(","), - PubsubTopic(pubsubTopic.get()), - cursor, - startTime.get(), - endTime.get(), - hexHashes.join(","), - maxPageSize, - ascendingOrder, - requestId, - ) - else: - ## We will run atypical query. In this case we don't use prepared statemets - return await s.getMessagesArbitraryQuery( - contentTopicSeq, pubsubTopic, cursor, startTime, endTime, hexHashes, maxPageSize, - ascendingOrder, requestId, - ) - -method getMessagesV2*( - s: PostgresDriver, - contentTopicSeq = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async, deprecated.} = - if contentTopicSeq.len == 1 and pubsubTopic.isSome() and startTime.isSome() and - endTime.isSome(): - ## Considered the most common query. Therefore, we use prepared statements to optimize it. - return await s.getMessagesV2PreparedStmt( - contentTopicSeq.join(","), - PubsubTopic(pubsubTopic.get()), - cursor, - startTime.get(), - endTime.get(), - maxPageSize, - ascendingOrder, - requestId, - ) - else: - ## We will run atypical query. In this case we don't use prepared statemets - return await s.getMessagesV2ArbitraryQuery( - contentTopicSeq, pubsubTopic, cursor, startTime, endTime, maxPageSize, - ascendingOrder, requestId, - ) - -proc getStr( - s: PostgresDriver, query: string -): Future[ArchiveDriverResult[string]] {.async.} = - # Performs a query that is expected to return a single string - - var ret: string - proc rowCallback(pqResult: ptr PGresult) = - if pqResult.pqnfields() != 1: - error "Wrong number of fields in getStr" - return - - if pqResult.pqNtuples() != 1: - error "Wrong number of rows in getStr" - return - - ret = $(pqgetvalue(pqResult, 0, 0)) - - (await s.readConnPool.pgQuery(query, newSeq[string](0), rowCallback)).isOkOr: - return err("failed in getRow: " & $error) - - return ok(ret) - -proc getInt( - s: PostgresDriver, query: string -): Future[ArchiveDriverResult[int64]] {.async.} = - # Performs a query that is expected to return a single numeric value (int64) - - var retInt = 0'i64 - let str = (await s.getStr(query)).valueOr: - return err("could not get str in getInt: " & $error) - - try: - retInt = parseInt(str) - except ValueError: - return err( - "exception in getInt, parseInt, str: " & str & " query: " & query & " exception: " & - getCurrentExceptionMsg() - ) - - return ok(retInt) - -method getDatabaseSize*( - s: PostgresDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - let intRes = (await s.getInt("SELECT pg_database_size(current_database())")).valueOr: - return err("error in getDatabaseSize: " & error) - - let databaseSize: int64 = int64(intRes) - return ok(databaseSize) - -method getMessagesCount*( - s: PostgresDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - let intRes = await s.getInt("SELECT COUNT(1) FROM messages") - if intRes.isErr(): - return err("error in getMessagesCount: " & intRes.error) - - return ok(intRes.get()) - -method getOldestMessageTimestamp*( - s: PostgresDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return err("not implemented because legacy will get deprecated") - -method getNewestMessageTimestamp*( - s: PostgresDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - let intRes = await s.getInt("SELECT MAX(timestamp) FROM messages") - if intRes.isErr(): - return err("error in getNewestMessageTimestamp: " & intRes.error) - - return ok(Timestamp(intRes.get())) - -method deleteOldestMessagesNotWithinLimit*( - s: PostgresDriver, limit: int -): Future[ArchiveDriverResult[void]] {.async.} = - ## Will be completely removed when deprecating store legacy - # let execRes = await s.writeConnPool.pgQuery( - # """DELETE FROM messages WHERE id NOT IN - # ( - # SELECT id FROM messages ORDER BY timestamp DESC LIMIT ? - # );""", - # @[$limit], - # ) - # if execRes.isErr(): - # return err("error in deleteOldestMessagesNotWithinLimit: " & execRes.error) - - return ok() - -method close*(s: PostgresDriver): Future[ArchiveDriverResult[void]] {.async.} = - ## Close the database connection - let writeCloseRes = await s.writeConnPool.close() - let readCloseRes = await s.readConnPool.close() - - writeCloseRes.isOkOr: - return err("error closing write pool: " & $error) - - readCloseRes.isOkOr: - return err("error closing read pool: " & $error) - - return ok() - -proc sleep*( - s: PostgresDriver, seconds: int -): Future[ArchiveDriverResult[void]] {.async.} = - # This is for testing purposes only. It is aimed to test the proper - # implementation of asynchronous requests. It merely triggers a sleep in the - # database for the amount of seconds given as a parameter. - - proc rowCallback(result: ptr PGresult) = - ## We are not interested in any value in this case - discard - - try: - let params = @[$seconds] - (await s.writeConnPool.pgQuery("SELECT pg_sleep(?)", params, rowCallback)).isOkOr: - return err("error in postgres_driver sleep: " & $error) - except DbError: - # This always raises an exception although the sleep works - return err("exception sleeping: " & getCurrentExceptionMsg()) - - return ok() - -proc performWriteQuery*( - s: PostgresDriver, query: string -): Future[ArchiveDriverResult[void]] {.async.} = - ## Performs a query that somehow changes the state of the database - - (await s.writeConnPool.pgQuery(query)).isOkOr: - return err("error in performWriteQuery: " & $error) - - return ok() - -method decreaseDatabaseSize*( - driver: PostgresDriver, targetSizeInBytes: int64, forceRemoval: bool = false -): Future[ArchiveDriverResult[void]] {.async.} = - ## This is completely disabled and only the non-legacy driver - ## will take care of that - # var dbSize = (await driver.getDatabaseSize()).valueOr: - # return err("decreaseDatabaseSize failed to get database size: " & $error) - - # ## database size in bytes - # var totalSizeOfDB: int64 = int64(dbSize) - - # if totalSizeOfDB <= targetSizeInBytes: - # return ok() - - # info "start reducing database size", - # targetSize = $targetSizeInBytes, currentSize = $totalSizeOfDB - - # while totalSizeOfDB > targetSizeInBytes and driver.containsAnyPartition(): - # (await driver.removeOldestPartition(forceRemoval)).isOkOr: - # return err( - # "decreaseDatabaseSize inside loop failed to remove oldest partition: " & $error - # ) - - # dbSize = (await driver.getDatabaseSize()).valueOr: - # return - # err("decreaseDatabaseSize inside loop failed to get database size: " & $error) - - # let newCurrentSize = int64(dbSize) - # if newCurrentSize == totalSizeOfDB: - # return err("the previous partition removal didn't clear database size") - - # totalSizeOfDB = newCurrentSize - - # info "reducing database size", - # targetSize = $targetSizeInBytes, newCurrentSize = $totalSizeOfDB - - return ok() - -method existsTable*( - s: PostgresDriver, tableName: string -): Future[ArchiveDriverResult[bool]] {.async.} = - let query: string = - fmt""" - SELECT EXISTS ( - SELECT FROM - pg_tables - WHERE - tablename = '{tableName}' - ); - """ - - var exists: string - proc rowCallback(pqResult: ptr PGresult) = - if pqResult.pqnfields() != 1: - error "Wrong number of fields in existsTable" - return - - if pqResult.pqNtuples() != 1: - error "Wrong number of rows in existsTable" - return - - exists = $(pqgetvalue(pqResult, 0, 0)) - - (await s.readConnPool.pgQuery(query, newSeq[string](0), rowCallback)).isOkOr: - return err("existsTable failed in getRow: " & $error) - - return ok(exists == "t") - -proc getCurrentVersion*( - s: PostgresDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - let existsVersionTable = (await s.existsTable("version")).valueOr: - return err("error in getCurrentVersion-existsTable: " & $error) - - if not existsVersionTable: - return ok(0) - - let res = (await s.getInt(fmt"SELECT version FROM version")).valueOr: - return err("error in getMessagesCount: " & $error) - - return ok(res) - -method deleteMessagesOlderThanTimestamp*( - s: PostgresDriver, tsNanoSec: Timestamp -): Future[ArchiveDriverResult[void]] {.async.} = - ## First of all, let's remove the older partitions so that we can reduce - ## the database size. - # (await s.removePartitionsOlderThan(tsNanoSec)).isOkOr: - # return err("error while removing older partitions: " & $error) - - # ( - # await s.writeConnPool.pgQuery( - # "DELETE FROM messages WHERE timestamp < " & $tsNanoSec - # ) - # ).isOkOr: - # return err("error in deleteMessagesOlderThanTimestamp: " & $error) - - return ok() diff --git a/waku/waku_archive_legacy/driver/postgres_driver/postgres_healthcheck.nim b/waku/waku_archive_legacy/driver/postgres_driver/postgres_healthcheck.nim deleted file mode 100644 index 23678538e..000000000 --- a/waku/waku_archive_legacy/driver/postgres_driver/postgres_healthcheck.nim +++ /dev/null @@ -1,37 +0,0 @@ -{.push raises: [].} - -import chronos, chronicles, results -import ../../../common/databases/db_postgres, ../../../common/error_handling - -## Simple query to validate that the postgres is working and attending requests -const HealthCheckQuery = "SELECT version();" -const CheckConnectivityInterval = 60.seconds -const MaxNumTrials = 20 -const TrialInterval = 1.seconds - -proc checkConnectivity*( - connPool: PgAsyncPool, onFatalErrorAction: OnFatalErrorHandler -) {.async.} = - while true: - (await connPool.pgQuery(HealthCheckQuery)).isOkOr: - ## The connection failed once. Let's try reconnecting for a while. - ## Notice that the 'pgQuery' proc tries to establish a new connection. - - block errorBlock: - ## Force close all the opened connections. No need to close gracefully. - (await connPool.resetConnPool()).isOkOr: - onFatalErrorAction("checkConnectivity legacy resetConnPool error: " & error) - - var numTrial = 0 - while numTrial < MaxNumTrials: - (await connPool.pgQuery(HealthCheckQuery)).isErrOr: - ## Connection resumed. Let's go back to the normal healthcheck. - break errorBlock - - await sleepAsync(TrialInterval) - numTrial.inc() - - ## The connection couldn't be resumed. Let's inform the upper layers. - onFatalErrorAction("postgres legacy health check error: " & error) - - await sleepAsync(CheckConnectivityInterval) diff --git a/waku/waku_archive_legacy/driver/queue_driver.nim b/waku/waku_archive_legacy/driver/queue_driver.nim deleted file mode 100644 index 1ea8a29d3..000000000 --- a/waku/waku_archive_legacy/driver/queue_driver.nim +++ /dev/null @@ -1,8 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import ./queue_driver/queue_driver, ./queue_driver/index - -export queue_driver, index diff --git a/waku/waku_archive_legacy/driver/queue_driver/index.nim b/waku/waku_archive_legacy/driver/queue_driver/index.nim deleted file mode 100644 index 2328870d0..000000000 --- a/waku/waku_archive_legacy/driver/queue_driver/index.nim +++ /dev/null @@ -1,91 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import nimcrypto/sha2 -import ../../../waku_core, ../../common - -type Index* = object - ## This type contains the description of an Index used in the pagination of WakuMessages - pubsubTopic*: string - senderTime*: Timestamp # the time at which the message is generated - receiverTime*: Timestamp - digest*: MessageDigest # calculated over payload and content topic - hash*: WakuMessageHash - -proc compute*( - T: type Index, msg: WakuMessage, receivedTime: Timestamp, pubsubTopic: PubsubTopic -): T = - ## Takes a WakuMessage with received timestamp and returns its Index. - let - digest = computeDigest(msg) - senderTime = msg.timestamp - hash = computeMessageHash(pubsubTopic, msg) - - return Index( - pubsubTopic: pubsubTopic, - senderTime: senderTime, - receiverTime: receivedTime, - digest: digest, - hash: hash, - ) - -proc tohistoryCursor*(index: Index): ArchiveCursor = - return ArchiveCursor( - pubsubTopic: index.pubsubTopic, - senderTime: index.senderTime, - storeTime: index.receiverTime, - digest: index.digest, - hash: index.hash, - ) - -proc toIndex*(index: ArchiveCursor): Index = - return Index( - pubsubTopic: index.pubsubTopic, - senderTime: index.senderTime, - receiverTime: index.storeTime, - digest: index.digest, - hash: index.hash, - ) - -proc `==`*(x, y: Index): bool = - ## receiverTime plays no role in index equality - return - ( - (x.senderTime == y.senderTime) and (x.digest == y.digest) and - (x.pubsubTopic == y.pubsubTopic) - ) or (x.hash == y.hash) # this applies to store v3 queries only - -proc cmp*(x, y: Index): int = - ## compares x and y - ## returns 0 if they are equal - ## returns -1 if x < y - ## returns 1 if x > y - ## - ## Default sorting order priority is: - ## 1. senderTimestamp - ## 2. receiverTimestamp (a fallback only if senderTimestamp unset on either side, and all other fields unequal) - ## 3. message digest - ## 4. pubsubTopic - - if x == y: - # Quick exit ensures receiver time does not affect index equality - return 0 - - # Timestamp has a higher priority for comparison - let - # Use receiverTime where senderTime is unset - xTimestamp = if x.senderTime == 0: x.receiverTime else: x.senderTime - yTimestamp = if y.senderTime == 0: y.receiverTime else: y.senderTime - - let timecmp = cmp(xTimestamp, yTimestamp) - if timecmp != 0: - return timecmp - - # Continue only when timestamps are equal - let digestcmp = cmp(x.digest.data, y.digest.data) - if digestcmp != 0: - return digestcmp - - return cmp(x.pubsubTopic, y.pubsubTopic) diff --git a/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim b/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim deleted file mode 100644 index 942a720df..000000000 --- a/waku/waku_archive_legacy/driver/queue_driver/queue_driver.nim +++ /dev/null @@ -1,364 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/options, results, stew/sorted_set, chronicles, chronos -import ../../../waku_core, ../../common, ../../driver, ./index - -logScope: - topics = "waku archive queue_store" - -const QueueDriverDefaultMaxCapacity* = 25_000 - -type - QueryFilterMatcher = - proc(index: Index, msg: WakuMessage): bool {.gcsafe, raises: [], closure.} - - QueueDriver* = ref object of ArchiveDriver - ## Bounded repository for indexed messages - ## - ## The store queue will keep messages up to its - ## configured capacity. As soon as this capacity - ## is reached and a new message is added, the oldest - ## item will be removed to make space for the new one. - ## This implies both a `delete` and `add` operation - ## for new items. - - # TODO: a circular/ring buffer may be a more efficient implementation - items: SortedSet[Index, WakuMessage] # sorted set of stored messages - capacity: int # Maximum amount of messages to keep - - QueueDriverErrorKind {.pure.} = enum - INVALID_CURSOR - - QueueDriverGetPageResult = Result[seq[ArchiveRow], QueueDriverErrorKind] - -proc `$`(error: QueueDriverErrorKind): string = - case error - of INVALID_CURSOR: "invalid_cursor" - -### Helpers - -proc walkToCursor( - w: SortedSetWalkRef[Index, WakuMessage], startCursor: Index, forward: bool -): SortedSetResult[Index, WakuMessage] = - ## Walk to util we find the cursor - ## TODO: Improve performance here with a binary/tree search - - var nextItem = - if forward: - w.first() - else: - w.last() - - ## Fast forward until we reach the startCursor - while nextItem.isOk(): - if nextItem.value.key == startCursor: - break - - # Not yet at cursor. Continue advancing - nextItem = - if forward: - w.next() - else: - w.prev() - - return nextItem - -#### API - -proc new*(T: type QueueDriver, capacity: int = QueueDriverDefaultMaxCapacity): T = - var items = SortedSet[Index, WakuMessage].init() - return QueueDriver(items: items, capacity: capacity) - -proc contains*(driver: QueueDriver, index: Index): bool = - ## Return `true` if the store queue already contains the `index`, `false` otherwise. - return driver.items.eq(index).isOk() - -proc len*(driver: QueueDriver): int {.noSideEffect.} = - return driver.items.len - -proc getPage( - driver: QueueDriver, - pageSize: uint = 0, - forward: bool = true, - cursor: Option[Index] = none(Index), - predicate: QueryFilterMatcher = nil, -): QueueDriverGetPageResult {.raises: [].} = - ## Populate a single page in forward direction - ## Start at the `startCursor` (exclusive), or first entry (inclusive) if not defined. - ## Page size must not exceed `maxPageSize` - ## Each entry must match the `pred` - var outSeq: seq[ArchiveRow] - - var w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - defer: - w.destroy() - - var currentEntry: SortedSetResult[Index, WakuMessage] - - # Find starting entry - if cursor.isSome(): - let cursorEntry = w.walkToCursor(cursor.get(), forward) - if cursorEntry.isErr(): - return err(QueueDriverErrorKind.INVALID_CURSOR) - - # Advance walker once more - currentEntry = - if forward: - w.next() - else: - w.prev() - else: - # Start from the beginning of the queue - currentEntry = - if forward: - w.first() - else: - w.last() - - trace "Starting page query", currentEntry = currentEntry - - ## This loop walks forward over the queue: - ## 1. from the given cursor (or first/last entry, if not provided) - ## 2. adds entries matching the predicate function to output page - ## 3. until either the end of the queue or maxPageSize is reached - var numberOfItems: uint = 0 - while currentEntry.isOk() and numberOfItems < pageSize: - trace "Continuing page query", - currentEntry = currentEntry, numberOfItems = numberOfItems - - let - key = currentEntry.value.key - data = currentEntry.value.data - - if predicate.isNil() or predicate(key, data): - numberOfItems += 1 - - outSeq.add( - (key.pubsubTopic, data, @(key.digest.data), key.receiverTime, key.hash) - ) - - currentEntry = - if forward: - w.next() - else: - w.prev() - - trace "Successfully retrieved page", len = outSeq.len - - return ok(outSeq) - -## --- SortedSet accessors --- - -iterator fwdIterator*(driver: QueueDriver): (Index, WakuMessage) = - ## Forward iterator over the entire store queue - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - res = w.first() - - while res.isOk(): - yield (res.value.key, res.value.data) - res = w.next() - - w.destroy() - -iterator bwdIterator*(driver: QueueDriver): (Index, WakuMessage) = - ## Backwards iterator over the entire store queue - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - res = w.last() - - while res.isOk(): - yield (res.value.key, res.value.data) - res = w.prev() - - w.destroy() - -proc first*(driver: QueueDriver): ArchiveDriverResult[Index] = - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - res = w.first() - w.destroy() - - if res.isErr(): - return err("Not found") - - return ok(res.value.key) - -proc last*(driver: QueueDriver): ArchiveDriverResult[Index] = - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - res = w.last() - w.destroy() - - if res.isErr(): - return err("Not found") - - return ok(res.value.key) - -## --- Queue API --- - -proc add*( - driver: QueueDriver, index: Index, msg: WakuMessage -): ArchiveDriverResult[void] = - ## Add a message to the queue - ## - ## If we're at capacity, we will be removing, the oldest (first) item - if driver.contains(index): - trace "could not add item to store queue. Index already exists", index = index - return err("duplicate") - - # TODO: the below delete block can be removed if we convert to circular buffer - if driver.items.len >= driver.capacity: - var - w = SortedSetWalkRef[Index, WakuMessage].init(driver.items) - firstItem = w.first - - if cmp(index, firstItem.value.key) < 0: - # When at capacity, we won't add if message index is smaller (older) than our oldest item - w.destroy # Clean up walker - return err("too_old") - - discard driver.items.delete(firstItem.value.key) - w.destroy # better to destroy walker after a delete operation - - driver.items.insert(index).value.data = msg - - return ok() - -method put*( - driver: QueueDriver, - pubsubTopic: PubsubTopic, - message: WakuMessage, - digest: MessageDigest, - messageHash: WakuMessageHash, - receivedTime: Timestamp, -): Future[ArchiveDriverResult[void]] {.async.} = - let index = Index( - pubsubTopic: pubsubTopic, - senderTime: message.timestamp, - receiverTime: receivedTime, - digest: digest, - hash: messageHash, - ) - - return driver.add(index, message) - -method getAllMessages*( - driver: QueueDriver -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - # TODO: Implement this message_store method - return err("interface method not implemented") - -method existsTable*( - driver: QueueDriver, tableName: string -): Future[ArchiveDriverResult[bool]] {.async.} = - return err("interface method not implemented") - -method getMessages*( - driver: QueueDriver, - includeData = true, - contentTopic: seq[ContentTopic] = @[], - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes: seq[WakuMessageHash] = @[], - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId = "", -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - let cursor = cursor.map(toIndex) - - let matchesQuery: QueryFilterMatcher = - func (index: Index, msg: WakuMessage): bool = - if pubsubTopic.isSome() and index.pubsubTopic != pubsubTopic.get(): - return false - - if contentTopic.len > 0 and msg.contentTopic notin contentTopic: - return false - - if startTime.isSome() and msg.timestamp < startTime.get(): - return false - - if endTime.isSome() and msg.timestamp > endTime.get(): - return false - - if hashes.len > 0 and index.hash notin hashes: - return false - - return true - - var pageRes: QueueDriverGetPageResult - try: - pageRes = driver.getPage(maxPageSize, ascendingOrder, cursor, matchesQuery) - except CatchableError, Exception: - return err(getCurrentExceptionMsg()) - - if pageRes.isErr(): - return err($pageRes.error) - - return ok(pageRes.value) - -method getMessagesCount*( - driver: QueueDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return ok(int64(driver.len())) - -method getPagesCount*( - driver: QueueDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return ok(int64(driver.len())) - -method getPagesSize*( - driver: QueueDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return ok(int64(driver.len())) - -method getDatabaseSize*( - driver: QueueDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return ok(int64(driver.len())) - -method performVacuum*( - driver: QueueDriver -): Future[ArchiveDriverResult[void]] {.async.} = - return err("interface method not implemented") - -method getOldestMessageTimestamp*( - driver: QueueDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return driver.first().map( - proc(index: Index): Timestamp = - index.receiverTime - ) - -method getNewestMessageTimestamp*( - driver: QueueDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return driver.last().map( - proc(index: Index): Timestamp = - index.receiverTime - ) - -method deleteMessagesOlderThanTimestamp*( - driver: QueueDriver, ts: Timestamp -): Future[ArchiveDriverResult[void]] {.async.} = - # TODO: Implement this message_store method - return err("interface method not implemented") - -method deleteOldestMessagesNotWithinLimit*( - driver: QueueDriver, limit: int -): Future[ArchiveDriverResult[void]] {.async.} = - # TODO: Implement this message_store method - return err("interface method not implemented") - -method decreaseDatabaseSize*( - driver: QueueDriver, targetSizeInBytes: int64, forceRemoval: bool = false -): Future[ArchiveDriverResult[void]] {.async.} = - return err("interface method not implemented") - -method close*(driver: QueueDriver): Future[ArchiveDriverResult[void]] {.async.} = - return ok() diff --git a/waku/waku_archive_legacy/driver/sqlite_driver.nim b/waku/waku_archive_legacy/driver/sqlite_driver.nim deleted file mode 100644 index 027e00488..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver.nim +++ /dev/null @@ -1,8 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import ./sqlite_driver/sqlite_driver - -export sqlite_driver diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/cursor.nim b/waku/waku_archive_legacy/driver/sqlite_driver/cursor.nim deleted file mode 100644 index 9729f0ff7..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver/cursor.nim +++ /dev/null @@ -1,11 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import ../../../waku_core, ../../common - -type DbCursor* = (Timestamp, seq[byte], PubsubTopic) - -proc toDbCursor*(c: ArchiveCursor): DbCursor = - (c.storeTime, @(c.digest.data), c.pubsubTopic) diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim b/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim deleted file mode 100644 index 5fccf8f3d..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver/migrations.nim +++ /dev/null @@ -1,74 +0,0 @@ -{.push raises: [].} - -import - std/[tables, strutils, os], results, chronicles, sqlite3_abi # sqlite3_column_int64 -import ../../../common/databases/db_sqlite, ../../../common/databases/common - -logScope: - topics = "waku archive migration" - -const SchemaVersion* = 9 # increase this when there is an update in the database schema - -template projectRoot(): string = - currentSourcePath.rsplit(DirSep, 1)[0] / ".." / ".." / ".." / ".." - -const MessageStoreMigrationPath: string = projectRoot / "migrations" / "message_store" - -proc isSchemaVersion7*(db: SqliteDatabase): DatabaseResult[bool] = - ## Temporary proc created to analyse when the table actually belongs to the SchemaVersion 7. - ## - ## During many nwaku versions, 0.14.0 until 0.18.0, the SchemaVersion wasn't set or checked. - ## Docker `nwaku` nodes that start working from these versions, 0.14.0 until 0.18.0, they started - ## with this discrepancy: `user_version`== 0 (not set) but Message table with SchemaVersion 7. - ## - ## We found issues where `user_version` (SchemaVersion) was set to 0 in the database even though - ## its scheme structure reflected SchemaVersion 7. In those cases, when `nwaku` re-started to - ## apply the migration scripts (in 0.19.0) the node didn't start properly because it tried to - ## migrate a database that already had the Schema structure #7, so it failed when changing the PK. - ## - ## TODO: This was added in version 0.20.0. We might remove this in version 0.30.0, as we - ## could consider that many users use +0.20.0. - - var pkColumns = newSeq[string]() - proc queryRowCallback(s: ptr sqlite3_stmt) = - let colName = cstring sqlite3_column_text(s, 0) - pkColumns.add($colName) - - let query = - """SELECT l.name FROM pragma_table_info("Message") as l WHERE l.pk != 0;""" - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err("failed to determine the current SchemaVersion: " & $res.error) - - if pkColumns == @["pubsubTopic", "id", "storedAt"]: - return ok(true) - else: - info "Not considered schema version 7" - return ok(false) - -proc migrate*(db: SqliteDatabase, targetVersion = SchemaVersion): DatabaseResult[void] = - ## Compares the `user_version` of the sqlite database with the provided `targetVersion`, then - ## it runs migration scripts if the `user_version` is outdated. The `migrationScriptsDir` path - ## points to the directory holding the migrations scripts once the db is updated, it sets the - ## `user_version` to the `tragetVersion`. - ## - ## If not `targetVersion` is provided, it defaults to `SchemaVersion`. - ## - ## NOTE: Down migration it is not currently supported - info "starting message store's sqlite database migration" - - let userVersion = ?db.getUserVersion() - let isSchemaVersion7 = ?db.isSchemaVersion7() - - if userVersion == 0'i64 and isSchemaVersion7: - info "We found user_version 0 but the database schema reflects the user_version 7" - ## Force the correct schema version - ?db.setUserVersion(7) - - let migrationRes = - migrate(db, targetVersion, migrationsScriptsDir = MessageStoreMigrationPath) - if migrationRes.isErr(): - return err("failed to execute migration scripts: " & migrationRes.error) - - info "finished message store's sqlite database migration" - return ok() diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim b/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim deleted file mode 100644 index 47f1d86ae..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver/queries.nim +++ /dev/null @@ -1,739 +0,0 @@ -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/[options, sequtils], stew/byteutils, sqlite3_abi, results -import - ../../../common/databases/db_sqlite, - ../../../common/databases/common, - ../../../waku_core, - ./cursor - -const DbTable = "Message" - -type SqlQueryStr = string - -### SQLite column helper methods - -proc queryRowWakuMessageCallback( - s: ptr sqlite3_stmt, - contentTopicCol, payloadCol, versionCol, senderTimestampCol, metaCol: cint, -): WakuMessage = - let - topic = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, contentTopicCol)) - topicLength = sqlite3_column_bytes(s, contentTopicCol) - contentTopic = string.fromBytes(@(toOpenArray(topic, 0, topicLength - 1))) - - p = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, payloadCol)) - m = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, metaCol)) - - payloadLength = sqlite3_column_bytes(s, payloadCol) - metaLength = sqlite3_column_bytes(s, metaCol) - payload = @(toOpenArray(p, 0, payloadLength - 1)) - version = sqlite3_column_int64(s, versionCol) - senderTimestamp = sqlite3_column_int64(s, senderTimestampCol) - meta = @(toOpenArray(m, 0, metaLength - 1)) - - return WakuMessage( - contentTopic: ContentTopic(contentTopic), - payload: payload, - version: uint32(version), - timestamp: Timestamp(senderTimestamp), - meta: meta, - ) - -proc queryRowReceiverTimestampCallback( - s: ptr sqlite3_stmt, storedAtCol: cint -): Timestamp = - let storedAt = sqlite3_column_int64(s, storedAtCol) - return Timestamp(storedAt) - -proc queryRowPubsubTopicCallback( - s: ptr sqlite3_stmt, pubsubTopicCol: cint -): PubsubTopic = - let - pubsubTopicPointer = - cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, pubsubTopicCol)) - pubsubTopicLength = sqlite3_column_bytes(s, pubsubTopicCol) - pubsubTopic = - string.fromBytes(@(toOpenArray(pubsubTopicPointer, 0, pubsubTopicLength - 1))) - - return pubsubTopic - -proc queryRowDigestCallback(s: ptr sqlite3_stmt, digestCol: cint): seq[byte] = - let - digestPointer = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, digestCol)) - digestLength = sqlite3_column_bytes(s, digestCol) - digest = @(toOpenArray(digestPointer, 0, digestLength - 1)) - - return digest - -proc queryRowWakuMessageHashCallback( - s: ptr sqlite3_stmt, hashCol: cint -): WakuMessageHash = - let - hashPointer = cast[ptr UncheckedArray[byte]](sqlite3_column_blob(s, hashCol)) - hashLength = sqlite3_column_bytes(s, hashCol) - hash = fromBytes(toOpenArray(hashPointer, 0, hashLength - 1)) - - return hash - -### SQLite queries - -## Create table - -proc createTableQuery(table: string): SqlQueryStr = - "CREATE TABLE IF NOT EXISTS " & table & " (" & " pubsubTopic BLOB NOT NULL," & - " contentTopic BLOB NOT NULL," & " payload BLOB," & " version INTEGER NOT NULL," & - " timestamp INTEGER NOT NULL," & " id BLOB," & " messageHash BLOB," & - " storedAt INTEGER NOT NULL," & " meta BLOB," & - " CONSTRAINT messageIndex PRIMARY KEY (messageHash)" & ") WITHOUT ROWID;" - -proc createTable*(db: SqliteDatabase): DatabaseResult[void] = - let query = createTableQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -## Create indices - -proc createOldestMessageTimestampIndexQuery(table: string): SqlQueryStr = - "CREATE INDEX IF NOT EXISTS i_ts ON " & table & " (storedAt);" - -proc createOldestMessageTimestampIndex*(db: SqliteDatabase): DatabaseResult[void] = - let query = createOldestMessageTimestampIndexQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -proc createHistoryQueryIndexQuery(table: string): SqlQueryStr = - "CREATE INDEX IF NOT EXISTS i_query ON " & table & - " (contentTopic, pubsubTopic, storedAt, id);" - -proc createHistoryQueryIndex*(db: SqliteDatabase): DatabaseResult[void] = - let query = createHistoryQueryIndexQuery(DbTable) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -## Insert message -type InsertMessageParams* = ( - seq[byte], - seq[byte], - Timestamp, - seq[byte], - seq[byte], - seq[byte], - int64, - Timestamp, - seq[byte], -) - -proc insertMessageQuery(table: string): SqlQueryStr = - return - "INSERT INTO " & table & - "(id, messageHash, storedAt, contentTopic, payload, pubsubTopic, version, timestamp, meta)" & - " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);" - -proc prepareInsertMessageStmt*( - db: SqliteDatabase -): SqliteStmt[InsertMessageParams, void] = - let query = insertMessageQuery(DbTable) - return - db.prepareStmt(query, InsertMessageParams, void).expect("this is a valid statement") - -## Count table messages - -proc countMessagesQuery(table: string): SqlQueryStr = - return "SELECT COUNT(*) FROM " & table - -proc getMessageCount*(db: SqliteDatabase): DatabaseResult[int64] = - var count: int64 - proc queryRowCallback(s: ptr sqlite3_stmt) = - count = sqlite3_column_int64(s, 0) - - let query = countMessagesQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err("failed to count number of messages in the database") - - return ok(count) - -## Get oldest message receiver timestamp - -proc selectOldestMessageTimestampQuery(table: string): SqlQueryStr = - return "SELECT MIN(storedAt) FROM " & table - -proc selectOldestReceiverTimestamp*( - db: SqliteDatabase -): DatabaseResult[Timestamp] {.inline.} = - var timestamp: Timestamp - proc queryRowCallback(s: ptr sqlite3_stmt) = - timestamp = queryRowReceiverTimestampCallback(s, 0) - - let query = selectOldestMessageTimestampQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err("failed to get the oldest receiver timestamp from the database") - - return ok(timestamp) - -## Get newest message receiver timestamp - -proc selectNewestMessageTimestampQuery(table: string): SqlQueryStr = - return "SELECT MAX(storedAt) FROM " & table - -proc selectNewestReceiverTimestamp*( - db: SqliteDatabase -): DatabaseResult[Timestamp] {.inline.} = - var timestamp: Timestamp - proc queryRowCallback(s: ptr sqlite3_stmt) = - timestamp = queryRowReceiverTimestampCallback(s, 0) - - let query = selectNewestMessageTimestampQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err("failed to get the newest receiver timestamp from the database") - - return ok(timestamp) - -## Delete messages older than timestamp - -proc deleteMessagesOlderThanTimestampQuery(table: string, ts: Timestamp): SqlQueryStr = - return "DELETE FROM " & table & " WHERE storedAt < " & $ts - -proc deleteMessagesOlderThanTimestamp*( - db: SqliteDatabase, ts: int64 -): DatabaseResult[void] = - let query = deleteMessagesOlderThanTimestampQuery(DbTable, ts) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -## Delete oldest messages not within limit - -proc deleteOldestMessagesNotWithinLimitQuery(table: string, limit: int): SqlQueryStr = - return - "DELETE FROM " & table & " WHERE (storedAt, id, pubsubTopic) NOT IN (" & - " SELECT storedAt, id, pubsubTopic FROM " & table & - " ORDER BY storedAt DESC, id DESC" & " LIMIT " & $limit & ");" - -proc deleteOldestMessagesNotWithinLimit*( - db: SqliteDatabase, limit: int -): DatabaseResult[void] = - # NOTE: The word `limit` here refers the store capacity/maximum number-of-messages allowed limit - let query = deleteOldestMessagesNotWithinLimitQuery(DbTable, limit = limit) - discard - ?db.query( - query, - proc(s: ptr sqlite3_stmt) = - discard, - ) - return ok() - -## Select all messages - -proc selectAllMessagesQuery(table: string): SqlQueryStr = - return - "SELECT storedAt, contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta" & - " FROM " & table & " ORDER BY storedAt ASC" - -proc selectAllMessages*( - db: SqliteDatabase -): DatabaseResult[ - seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] -] {.gcsafe.} = - ## Retrieve all messages from the store. - var rows: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] - proc queryRowCallback(s: ptr sqlite3_stmt) = - let - pubsubTopic = queryRowPubsubTopicCallback(s, pubsubTopicCol = 3) - wakuMessage = queryRowWakuMessageCallback( - s, - contentTopicCol = 1, - payloadCol = 2, - versionCol = 4, - senderTimestampCol = 5, - metaCol = 8, - ) - digest = queryRowDigestCallback(s, digestCol = 6) - storedAt = queryRowReceiverTimestampCallback(s, storedAtCol = 0) - hash = queryRowWakuMessageHashCallback(s, hashCol = 7) - - rows.add((pubsubTopic, wakuMessage, digest, storedAt, hash)) - - let query = selectAllMessagesQuery(DbTable) - let res = db.query(query, queryRowCallback) - if res.isErr(): - return err(res.error()) - - return ok(rows) - -## Select messages by history query with limit - -proc combineClauses(clauses: varargs[Option[string]]): Option[string] = - let whereSeq = @clauses.filterIt(it.isSome()).mapIt(it.get()) - if whereSeq.len <= 0: - return none(string) - - var where: string = whereSeq[0] - for clause in whereSeq[1 ..^ 1]: - where &= " AND " & clause - return some(where) - -proc whereClausev2( - cursor: bool, - pubsubTopic: Option[PubsubTopic], - contentTopic: seq[ContentTopic], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - ascending: bool, -): Option[string] {.deprecated.} = - let cursorClause = - if cursor: - let comp = if ascending: ">" else: "<" - - some("(storedAt, id) " & comp & " (?, ?)") - else: - none(string) - - let pubsubTopicClause = - if pubsubTopic.isNone(): - none(string) - else: - some("pubsubTopic = (?)") - - let contentTopicClause = - if contentTopic.len <= 0: - none(string) - else: - var where = "contentTopic IN (" - where &= "?" - for _ in 1 ..< contentTopic.len: - where &= ", ?" - where &= ")" - some(where) - - let startTimeClause = - if startTime.isNone(): - none(string) - else: - some("storedAt >= (?)") - - let endTimeClause = - if endTime.isNone(): - none(string) - else: - some("storedAt <= (?)") - - return combineClauses( - cursorClause, pubsubTopicClause, contentTopicClause, startTimeClause, endTimeClause - ) - -proc selectMessagesWithLimitQueryv2( - table: string, where: Option[string], limit: uint, ascending = true, v3 = false -): SqlQueryStr {.deprecated.} = - let order = if ascending: "ASC" else: "DESC" - - var query: string - - query = - "SELECT storedAt, contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta" - query &= " FROM " & table - - if where.isSome(): - query &= " WHERE " & where.get() - - query &= " ORDER BY storedAt " & order & ", id " & order - - query &= " LIMIT " & $limit & ";" - - return query - -proc prepareStmt( - db: SqliteDatabase, stmt: string -): DatabaseResult[SqliteStmt[void, void]] = - var s: RawStmtPtr - checkErr sqlite3_prepare_v2(db.env, stmt, stmt.len.cint, addr s, nil) - return ok(SqliteStmt[void, void](s)) - -proc execSelectMessagesV2WithLimitStmt( - s: SqliteStmt, - cursor: Option[DbCursor], - pubsubTopic: Option[PubsubTopic], - contentTopic: seq[ContentTopic], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - onRowCallback: DataProc, -): DatabaseResult[void] {.deprecated.} = - let s = RawStmtPtr(s) - - # Bind params - var paramIndex = 1 - - if cursor.isSome(): - let (storedAt, id, _) = cursor.get() - checkErr bindParam(s, paramIndex, storedAt) - paramIndex += 1 - checkErr bindParam(s, paramIndex, id) - paramIndex += 1 - - if pubsubTopic.isSome(): - let pubsubTopic = toBytes(pubsubTopic.get()) - checkErr bindParam(s, paramIndex, pubsubTopic) - paramIndex += 1 - - for topic in contentTopic: - checkErr bindParam(s, paramIndex, topic.toBytes()) - paramIndex += 1 - - if startTime.isSome(): - let time = startTime.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - - if endTime.isSome(): - let time = endTime.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - - try: - while true: - let v = sqlite3_step(s) - case v - of SQLITE_ROW: - onRowCallback(s) - of SQLITE_DONE: - return ok() - else: - return err($sqlite3_errstr(v)) - except Exception, CatchableError: - # release implicit transaction - discard sqlite3_reset(s) # same return information as step - discard sqlite3_clear_bindings(s) # no errors possible - -proc selectMessagesByHistoryQueryWithLimit*( - db: SqliteDatabase, - contentTopic: seq[ContentTopic], - pubsubTopic: Option[PubsubTopic], - cursor: Option[DbCursor], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - limit: uint, - ascending: bool, -): DatabaseResult[ - seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] -] {.deprecated.} = - var messages: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] = - @[] - - proc queryRowCallback(s: ptr sqlite3_stmt) = - let - pubsubTopic = queryRowPubsubTopicCallback(s, pubsubTopicCol = 3) - message = queryRowWakuMessageCallback( - s, - contentTopicCol = 1, - payloadCol = 2, - versionCol = 4, - senderTimestampCol = 5, - metaCol = 8, - ) - digest = queryRowDigestCallback(s, digestCol = 6) - storedAt = queryRowReceiverTimestampCallback(s, storedAtCol = 0) - hash = queryRowWakuMessageHashCallback(s, hashCol = 7) - - messages.add((pubsubTopic, message, digest, storedAt, hash)) - - let query = block: - let where = whereClausev2( - cursor.isSome(), pubsubTopic, contentTopic, startTime, endTime, ascending - ) - - selectMessagesWithLimitQueryv2(DbTable, where, limit, ascending) - - let dbStmt = ?db.prepareStmt(query) - ?dbStmt.execSelectMessagesV2WithLimitStmt( - cursor, pubsubTopic, contentTopic, startTime, endTime, queryRowCallback - ) - dbStmt.dispose() - - return ok(messages) - -### Store v3 ### - -proc execSelectMessageByHash( - s: SqliteStmt, hash: WakuMessageHash, onRowCallback: DataProc -): DatabaseResult[void] = - let s = RawStmtPtr(s) - - checkErr bindParam(s, 1, toSeq(hash)) - - try: - while true: - let v = sqlite3_step(s) - case v - of SQLITE_ROW: - onRowCallback(s) - of SQLITE_DONE: - return ok() - else: - return err($sqlite3_errstr(v)) - except Exception, CatchableError: - # release implicit transaction - discard sqlite3_reset(s) # same return information as step - discard sqlite3_clear_bindings(s) # no errors possible - -proc selectMessageByHashQuery(): SqlQueryStr = - var query: string - - query = "SELECT contentTopic, payload, version, timestamp, meta, messageHash" - query &= " FROM " & DbTable - query &= " WHERE messageHash = (?)" - - return query - -proc whereClause( - cursor: bool, - pubsubTopic: Option[PubsubTopic], - contentTopic: seq[ContentTopic], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - hashes: seq[WakuMessageHash], - ascending: bool, -): Option[string] = - let cursorClause = - if cursor: - let comp = if ascending: ">" else: "<" - - some("(timestamp, messageHash) " & comp & " (?, ?)") - else: - none(string) - - let pubsubTopicClause = - if pubsubTopic.isNone(): - none(string) - else: - some("pubsubTopic = (?)") - - let contentTopicClause = - if contentTopic.len <= 0: - none(string) - else: - var where = "contentTopic IN (" - where &= "?" - for _ in 1 ..< contentTopic.len: - where &= ", ?" - where &= ")" - some(where) - - let startTimeClause = - if startTime.isNone(): - none(string) - else: - some("storedAt >= (?)") - - let endTimeClause = - if endTime.isNone(): - none(string) - else: - some("storedAt <= (?)") - - let hashesClause = - if hashes.len <= 0: - none(string) - else: - var where = "messageHash IN (" - where &= "?" - for _ in 1 ..< hashes.len: - where &= ", ?" - where &= ")" - some(where) - - return combineClauses( - cursorClause, pubsubTopicClause, contentTopicClause, startTimeClause, endTimeClause, - hashesClause, - ) - -proc execSelectMessagesWithLimitStmt( - s: SqliteStmt, - cursor: Option[(Timestamp, WakuMessageHash)], - pubsubTopic: Option[PubsubTopic], - contentTopic: seq[ContentTopic], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - hashes: seq[WakuMessageHash], - onRowCallback: DataProc, -): DatabaseResult[void] = - let s = RawStmtPtr(s) - - # Bind params - var paramIndex = 1 - - if cursor.isSome(): - let (time, hash) = cursor.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - checkErr bindParam(s, paramIndex, toSeq(hash)) - paramIndex += 1 - - if pubsubTopic.isSome(): - let pubsubTopic = toBytes(pubsubTopic.get()) - checkErr bindParam(s, paramIndex, pubsubTopic) - paramIndex += 1 - - for topic in contentTopic: - checkErr bindParam(s, paramIndex, topic.toBytes()) - paramIndex += 1 - - for hash in hashes: - checkErr bindParam(s, paramIndex, toSeq(hash)) - paramIndex += 1 - - if startTime.isSome(): - let time = startTime.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - - if endTime.isSome(): - let time = endTime.get() - checkErr bindParam(s, paramIndex, time) - paramIndex += 1 - - try: - while true: - let v = sqlite3_step(s) - case v - of SQLITE_ROW: - onRowCallback(s) - of SQLITE_DONE: - return ok() - else: - return err($sqlite3_errstr(v)) - except Exception, CatchableError: - # release implicit transaction - discard sqlite3_reset(s) # same return information as step - discard sqlite3_clear_bindings(s) # no errors possible - -proc selectMessagesWithLimitQuery( - table: string, where: Option[string], limit: uint, ascending = true, v3 = false -): SqlQueryStr = - let order = if ascending: "ASC" else: "DESC" - - var query: string - - query = - "SELECT storedAt, contentTopic, payload, pubsubTopic, version, timestamp, id, messageHash, meta" - query &= " FROM " & table - - if where.isSome(): - query &= " WHERE " & where.get() - - query &= " ORDER BY storedAt " & order & ", messageHash " & order - - query &= " LIMIT " & $limit & ";" - - return query - -proc selectMessagesByStoreQueryWithLimit*( - db: SqliteDatabase, - contentTopic: seq[ContentTopic], - pubsubTopic: Option[PubsubTopic], - cursor: Option[WakuMessageHash], - startTime: Option[Timestamp], - endTime: Option[Timestamp], - hashes: seq[WakuMessageHash], - limit: uint, - ascending: bool, -): DatabaseResult[ - seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] -] = - # Must first get the message timestamp before paginating by time - let newCursor = - if cursor.isSome() and cursor.get() != EmptyWakuMessageHash: - let hash: WakuMessageHash = cursor.get() - - var wakuMessage: Option[WakuMessage] - - proc queryRowCallback(s: ptr sqlite3_stmt) = - wakuMessage = some( - queryRowWakuMessageCallback( - s, - contentTopicCol = 0, - payloadCol = 1, - versionCol = 2, - senderTimestampCol = 3, - metaCol = 4, - ) - ) - - let query = selectMessageByHashQuery() - let dbStmt = ?db.prepareStmt(query) - ?dbStmt.execSelectMessageByHash(hash, queryRowCallback) - dbStmt.dispose() - - if wakuMessage.isSome(): - let time = wakuMessage.get().timestamp - - some((time, hash)) - else: - return err("cursor not found") - else: - none((Timestamp, WakuMessageHash)) - - var messages: seq[(PubsubTopic, WakuMessage, seq[byte], Timestamp, WakuMessageHash)] = - @[] - - proc queryRowCallback(s: ptr sqlite3_stmt) = - let - pubsubTopic = queryRowPubsubTopicCallback(s, pubsubTopicCol = 3) - message = queryRowWakuMessageCallback( - s, - contentTopicCol = 1, - payloadCol = 2, - versionCol = 4, - senderTimestampCol = 5, - metaCol = 8, - ) - digest = queryRowDigestCallback(s, digestCol = 6) - storedAt = queryRowReceiverTimestampCallback(s, storedAtCol = 0) - hash = queryRowWakuMessageHashCallback(s, hashCol = 7) - - messages.add((pubsubTopic, message, digest, storedAt, hash)) - - let query = block: - let where = whereClause( - newCursor.isSome(), - pubsubTopic, - contentTopic, - startTime, - endTime, - hashes, - ascending, - ) - - selectMessagesWithLimitQuery(DbTable, where, limit, ascending, true) - - let dbStmt = ?db.prepareStmt(query) - ?dbStmt.execSelectMessagesWithLimitStmt( - newCursor, pubsubTopic, contentTopic, startTime, endTime, hashes, queryRowCallback - ) - dbStmt.dispose() - - return ok(messages) diff --git a/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim b/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim deleted file mode 100644 index 5a6c12b05..000000000 --- a/waku/waku_archive_legacy/driver/sqlite_driver/sqlite_driver.nim +++ /dev/null @@ -1,225 +0,0 @@ -# The code in this file is an adaptation of the Sqlite KV Store found in nim-eth. -# https://github.com/status-im/nim-eth/blob/master/eth/db/kvstore_sqlite3.nim -when (NimMajor, NimMinor) < (1, 4): - {.push raises: [Defect].} -else: - {.push raises: [].} - -import std/options, stew/byteutils, chronicles, chronos, results -import - ../../../common/databases/db_sqlite, - ../../../waku_core, - ../../../waku_core/message/digest, - ../../common, - ../../driver, - ./cursor, - ./queries - -logScope: - topics = "waku archive sqlite" - -proc init(db: SqliteDatabase): ArchiveDriverResult[void] = - ## Misconfiguration can lead to nil DB - if db.isNil(): - return err("db not initialized") - - # Create table, if doesn't exist - let resCreate = createTable(db) - if resCreate.isErr(): - return err("failed to create table: " & resCreate.error()) - - # Create indices, if don't exist - let resRtIndex = createOldestMessageTimestampIndex(db) - if resRtIndex.isErr(): - return err("failed to create i_rt index: " & resRtIndex.error()) - - let resMsgIndex = createHistoryQueryIndex(db) - if resMsgIndex.isErr(): - return err("failed to create i_query index: " & resMsgIndex.error()) - - return ok() - -type SqliteDriver* = ref object of ArchiveDriver - db: SqliteDatabase - insertStmt: SqliteStmt[InsertMessageParams, void] - -proc new*(T: type SqliteDriver, db: SqliteDatabase): ArchiveDriverResult[T] = - # Database initialization - let resInit = init(db) - if resInit.isErr(): - return err(resInit.error()) - - # General initialization - let insertStmt = db.prepareInsertMessageStmt() - return ok(SqliteDriver(db: db, insertStmt: insertStmt)) - -method put*( - s: SqliteDriver, - pubsubTopic: PubsubTopic, - message: WakuMessage, - digest: MessageDigest, - messageHash: WakuMessageHash, - receivedTime: Timestamp, -): Future[ArchiveDriverResult[void]] {.async.} = - ## Inserts a message into the store - let res = s.insertStmt.exec( - ( - @(digest.data), # id - @(messageHash), # messageHash - receivedTime, # storedAt - toBytes(message.contentTopic), # contentTopic - message.payload, # payload - toBytes(pubsubTopic), # pubsubTopic - int64(message.version), # version - message.timestamp, # senderTimestamp - message.meta, # meta - ) - ) - - return res - -method getAllMessages*( - s: SqliteDriver -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - ## Retrieve all messages from the store. - return s.db.selectAllMessages() - -method getMessagesV2*( - s: SqliteDriver, - contentTopic = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId: string, -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async, deprecated.} = - let cursor = cursor.map(toDbCursor) - - let rowsRes = s.db.selectMessagesByHistoryQueryWithLimit( - contentTopic, - pubsubTopic, - cursor, - startTime, - endTime, - limit = maxPageSize, - ascending = ascendingOrder, - ) - - return rowsRes - -method getMessages*( - s: SqliteDriver, - includeData = true, - contentTopic = newSeq[ContentTopic](0), - pubsubTopic = none(PubsubTopic), - cursor = none(ArchiveCursor), - startTime = none(Timestamp), - endTime = none(Timestamp), - hashes = newSeq[WakuMessageHash](0), - maxPageSize = DefaultPageSize, - ascendingOrder = true, - requestId = "", -): Future[ArchiveDriverResult[seq[ArchiveRow]]] {.async.} = - let cursor = - if cursor.isSome(): - some(cursor.get().hash) - else: - none(WakuMessageHash) - - let rowsRes = s.db.selectMessagesByStoreQueryWithLimit( - contentTopic, - pubsubTopic, - cursor, - startTime, - endTime, - hashes, - limit = maxPageSize, - ascending = ascendingOrder, - ) - - return rowsRes - -method getMessagesCount*( - s: SqliteDriver -): Future[ArchiveDriverResult[int64]] {.async.} = - return s.db.getMessageCount() - -method getPagesCount*(s: SqliteDriver): Future[ArchiveDriverResult[int64]] {.async.} = - return s.db.getPageCount() - -method getPagesSize*(s: SqliteDriver): Future[ArchiveDriverResult[int64]] {.async.} = - return s.db.getPageSize() - -method getDatabaseSize*(s: SqliteDriver): Future[ArchiveDriverResult[int64]] {.async.} = - return s.db.getDatabaseSize() - -method performVacuum*(s: SqliteDriver): Future[ArchiveDriverResult[void]] {.async.} = - return s.db.performSqliteVacuum() - -method getOldestMessageTimestamp*( - s: SqliteDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return s.db.selectOldestReceiverTimestamp() - -method getNewestMessageTimestamp*( - s: SqliteDriver -): Future[ArchiveDriverResult[Timestamp]] {.async.} = - return s.db.selectnewestReceiverTimestamp() - -method deleteMessagesOlderThanTimestamp*( - s: SqliteDriver, ts: Timestamp -): Future[ArchiveDriverResult[void]] {.async.} = - return s.db.deleteMessagesOlderThanTimestamp(ts) - -method deleteOldestMessagesNotWithinLimit*( - s: SqliteDriver, limit: int -): Future[ArchiveDriverResult[void]] {.async.} = - return s.db.deleteOldestMessagesNotWithinLimit(limit) - -method decreaseDatabaseSize*( - driver: SqliteDriver, targetSizeInBytes: int64, forceRemoval: bool = false -): Future[ArchiveDriverResult[void]] {.async.} = - ## To remove 20% of the outdated data from database - const DeleteLimit = 0.80 - - ## when db size overshoots the database limit, shread 20% of outdated messages - ## get size of database - let dbSize = (await driver.getDatabaseSize()).valueOr: - return err("failed to get database size: " & $error) - - ## database size in bytes - let totalSizeOfDB: int64 = int64(dbSize) - - if totalSizeOfDB < targetSizeInBytes: - return ok() - - ## to shread/delete messsges, get the total row/message count - let numMessages = (await driver.getMessagesCount()).valueOr: - return err("failed to get messages count: " & error) - - ## NOTE: Using SQLite vacuuming is done manually, we delete a percentage of rows - ## if vacumming is done automatically then we aim to check DB size periodially for efficient - ## retention policy implementation. - - ## 80% of the total messages are to be kept, delete others - let pageDeleteWindow = int(float(numMessages) * DeleteLimit) - - (await driver.deleteOldestMessagesNotWithinLimit(limit = pageDeleteWindow)).isOkOr: - return err("deleting oldest messages failed: " & error) - - return ok() - -method close*(s: SqliteDriver): Future[ArchiveDriverResult[void]] {.async.} = - ## Close the database connection - # Dispose statements - s.insertStmt.dispose() - # Close connection - s.db.close() - return ok() - -method existsTable*( - s: SqliteDriver, tableName: string -): Future[ArchiveDriverResult[bool]] {.async.} = - return err("existsTable method not implemented in sqlite_driver") diff --git a/waku/waku_core/codecs.nim b/waku/waku_core/codecs.nim index 6dcdfe2f5..f0f0c977e 100644 --- a/waku/waku_core/codecs.nim +++ b/waku/waku_core/codecs.nim @@ -9,4 +9,4 @@ const WakuTransferCodec* = "/vac/waku/transfer/1.0.0" WakuMetadataCodec* = "/vac/waku/metadata/1.0.0" WakuPeerExchangeCodec* = "/vac/waku/peer-exchange/2.0.0-alpha1" - WakuLegacyStoreCodec* = "/vac/waku/store/2.0.0-beta4" + WakuRendezVousCodec* = "/vac/waku/rendezvous/1.0.0" diff --git a/waku/waku_core/message/digest.nim b/waku/waku_core/message/digest.nim index 8b99abd7e..3f82ce8f6 100644 --- a/waku/waku_core/message/digest.nim +++ b/waku/waku_core/message/digest.nim @@ -19,6 +19,11 @@ func shortLog*(hash: WakuMessageHash): string = func `$`*(hash: WakuMessageHash): string = shortLog(hash) +func to0xHex*(hash: WakuMessageHash): string = + var hexhash = newStringOfCap(64) + hexhash &= hash.toOpenArray(hash.low, hash.high).to0xHex() + hexhash + const EmptyWakuMessageHash*: WakuMessageHash = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, diff --git a/waku/waku_core/peers.nim b/waku/waku_core/peers.nim index 883f266bd..c4b8b593e 100644 --- a/waku/waku_core/peers.nim +++ b/waku/waku_core/peers.nim @@ -9,6 +9,7 @@ import eth/p2p/discoveryv5/enr, eth/net/utils, libp2p/crypto/crypto, + libp2p/crypto/curve25519, libp2p/crypto/secp, libp2p/errors, libp2p/multiaddress, @@ -37,6 +38,7 @@ type Static PeerExchange Dns + Kademlia PeerDirection* = enum UnknownDirection @@ -48,6 +50,8 @@ type RemotePeerInfo* = ref object addrs*: seq[MultiAddress] enr*: Option[enr.Record] protocols*: seq[string] + shards*: seq[uint16] + mixPubKey*: Option[Curve25519Key] agent*: string protoVersion*: string @@ -73,6 +77,7 @@ proc init*( addrs: seq[MultiAddress] = @[], enr: Option[enr.Record] = none(enr.Record), protocols: seq[string] = @[], + shards: seq[uint16] = @[], publicKey: crypto.PublicKey = crypto.PublicKey(), agent: string = "", protoVersion: string = "", @@ -82,12 +87,14 @@ proc init*( direction: PeerDirection = UnknownDirection, lastFailedConn: Moment = Moment.init(0, Second), numberFailedConn: int = 0, + mixPubKey: Option[Curve25519Key] = none(Curve25519Key), ): T = RemotePeerInfo( peerId: peerId, addrs: addrs, enr: enr, protocols: protocols, + shards: shards, publicKey: publicKey, agent: agent, protoVersion: protoVersion, @@ -97,6 +104,7 @@ proc init*( direction: direction, lastFailedConn: lastFailedConn, numberFailedConn: numberFailedConn, + mixPubKey: mixPubKey, ) proc init*( @@ -105,9 +113,12 @@ proc init*( addrs: seq[MultiAddress] = @[], enr: Option[enr.Record] = none(enr.Record), protocols: seq[string] = @[], + shards: seq[uint16] = @[], ): T {.raises: [Defect, ResultError[cstring], LPError].} = let peerId = PeerID.init(peerId).tryGet() - RemotePeerInfo(peerId: peerId, addrs: addrs, enr: enr, protocols: protocols) + RemotePeerInfo( + peerId: peerId, addrs: addrs, enr: enr, protocols: protocols, shards: shards + ) ## Parse @@ -165,17 +176,15 @@ proc parsePeerInfoFromRegularAddr(peer: MultiAddress): Result[RemotePeerInfo, st case addrPart[].protoName()[] # All protocols listed here: https://github.com/multiformats/multiaddr/blob/b746a7d014e825221cc3aea6e57a92d78419990f/protocols.csv of "p2p": - p2pPart = - ?addrPart.mapErr( - proc(err: string): string = - "Error getting p2pPart [" & err & "]" - ) + p2pPart = ?addrPart.mapErr( + proc(err: string): string = + "Error getting p2pPart [" & err & "]" + ) of "ip4", "ip6", "dns", "dnsaddr", "dns4", "dns6", "tcp", "ws", "wss": - let val = - ?addrPart.mapErr( - proc(err: string): string = - "Error getting addrPart [" & err & "]" - ) + let val = ?addrPart.mapErr( + proc(err: string): string = + "Error getting addrPart [" & err & "]" + ) ?wireAddr.append(val).mapErr( proc(err: string): string = "Error appending addrPart [" & err & "]" @@ -188,11 +197,10 @@ proc parsePeerInfoFromRegularAddr(peer: MultiAddress): Result[RemotePeerInfo, st "] [peer:" & $peer & "]" return err(msg) - let peerId = - ?PeerID.init(p2pPartStr.split("/")[^1]).mapErr( - proc(e: cstring): string = - $e - ) + let peerId = ?PeerID.init(p2pPartStr.split("/")[^1]).mapErr( + proc(e: cstring): string = + $e + ) if not wireAddr.validWireAddr(): return err("invalid multiaddress: no supported transport found") @@ -222,11 +230,10 @@ proc parsePeerInfo*(maddrs: varargs[string]): Result[RemotePeerInfo, string] = ## format `(ip4|ip6)/tcp/p2p`, into dialable PeerInfo var multiAddresses = newSeq[MultiAddress]() for maddr in maddrs: - let multiAddr = - ?MultiAddress.init(maddr).mapErr( - proc(err: string): string = - "MultiAddress.init [" & err & "]" - ) + let multiAddr = ?MultiAddress.init(maddr).mapErr( + proc(err: string): string = + "MultiAddress.init [" & err & "]" + ) multiAddresses.add(multiAddr) parsePeerInfo(multiAddresses) @@ -249,11 +256,10 @@ proc parseUrlPeerAddr*( return ok(none(RemotePeerInfo)) let parsedAddr = decodeUrl(peerAddr.get()) - let parsedPeerInfo = parsePeerInfo(parsedAddr) - if parsedPeerInfo.isErr(): - return err("Failed parsing remote peer info [" & parsedPeerInfo.error & "]") + let parsedPeerInfo = parsePeerInfo(parsedAddr).valueOr: + return err("Failed parsing remote peer info: " & error) - return ok(some(parsedPeerInfo.value)) + return ok(some(parsedPeerInfo)) proc toRemotePeerInfo*(enrRec: enr.Record): Result[RemotePeerInfo, cstring] = ## Converts an ENR to dialable RemotePeerInfo @@ -327,6 +333,7 @@ converter toRemotePeerInfo*(peerInfo: PeerInfo): RemotePeerInfo = addrs: peerInfo.listenAddrs, enr: none(enr.Record), protocols: peerInfo.protocols, + shards: @[], agent: peerInfo.agentVersion, protoVersion: peerInfo.protoVersion, publicKey: peerInfo.publicKey, @@ -339,11 +346,10 @@ proc hasProtocol*(ma: MultiAddress, proto: string): bool = ## Returns ``true`` if ``ma`` contains protocol ``proto``. let proto = MultiCodec.codec(proto) - let protos = ma.protocols() - if protos.isErr(): + let protos = ma.protocols().valueOr: return false - return protos.get().anyIt(it == proto) + return protos.anyIt(it == proto) func hasUdpPort*(peer: RemotePeerInfo): bool = if peer.enr.isNone(): @@ -363,6 +369,9 @@ proc getAgent*(peer: RemotePeerInfo): string = return peer.agent proc getShards*(peer: RemotePeerInfo): seq[uint16] = + if peer.shards.len > 0: + return peer.shards + if peer.enr.isNone(): return @[] diff --git a/waku/waku_core/subscription.nim b/waku/waku_core/subscription.nim index 19f3386ef..8694efa1b 100644 --- a/waku/waku_core/subscription.nim +++ b/waku/waku_core/subscription.nim @@ -1,3 +1,3 @@ -import ./subscription/subscription_manager, ./subscription/push_handler +import ./subscription/push_handler -export subscription_manager, push_handler +export push_handler diff --git a/waku/waku_core/subscription/subscription_manager.nim b/waku/waku_core/subscription/subscription_manager.nim deleted file mode 100644 index 1b950b3b4..000000000 --- a/waku/waku_core/subscription/subscription_manager.nim +++ /dev/null @@ -1,52 +0,0 @@ -{.push raises: [].} - -import std/tables, results, chronicles, chronos - -import ./push_handler, ../topics, ../message - -## Subscription manager -type SubscriptionManager* = object - subscriptions: TableRef[(string, ContentTopic), FilterPushHandler] - -proc init*(T: type SubscriptionManager): T = - SubscriptionManager( - subscriptions: newTable[(string, ContentTopic), FilterPushHandler]() - ) - -proc clear*(m: var SubscriptionManager) = - m.subscriptions.clear() - -proc registerSubscription*( - m: SubscriptionManager, - pubsubTopic: PubsubTopic, - contentTopic: ContentTopic, - handler: FilterPushHandler, -) = - try: - # TODO: Handle over subscription surprises - m.subscriptions[(pubsubTopic, contentTopic)] = handler - except CatchableError: - error "failed to register filter subscription", error = getCurrentExceptionMsg() - -proc removeSubscription*( - m: SubscriptionManager, pubsubTopic: PubsubTopic, contentTopic: ContentTopic -) = - m.subscriptions.del((pubsubTopic, contentTopic)) - -proc notifySubscriptionHandler*( - m: SubscriptionManager, - pubsubTopic: PubsubTopic, - contentTopic: ContentTopic, - message: WakuMessage, -) = - if not m.subscriptions.hasKey((pubsubTopic, contentTopic)): - return - - try: - let handler = m.subscriptions[(pubsubTopic, contentTopic)] - asyncSpawn handler(pubsubTopic, message) - except CatchableError: - discard - -proc getSubscriptionsCount*(m: SubscriptionManager): int = - m.subscriptions.len() diff --git a/waku/waku_core/topics/content_topic.nim b/waku/waku_core/topics/content_topic.nim index 5984a760b..3eeb35771 100644 --- a/waku/waku_core/topics/content_topic.nim +++ b/waku/waku_core/topics/content_topic.nim @@ -127,11 +127,10 @@ proc parse*( ): ParsingResult[seq[NsContentTopic]] = var res: seq[NsContentTopic] = @[] for contentTopic in topics: - let parseRes = NsContentTopic.parse(contentTopic) - if parseRes.isErr(): - let error: ParsingError = parseRes.error - return ParsingResult[seq[NsContentTopic]].err(error) - res.add(parseRes.value) + let parseRes = NsContentTopic.parse(contentTopic).valueOr: + let pError: ParsingError = error + return ParsingResult[seq[NsContentTopic]].err(pError) + res.add(parseRes) return ParsingResult[seq[NsContentTopic]].ok(res) # Content topic compatibility diff --git a/waku/waku_core/topics/pubsub_topic.nim b/waku/waku_core/topics/pubsub_topic.nim index 27ea27180..1e921d06d 100644 --- a/waku/waku_core/topics/pubsub_topic.nim +++ b/waku/waku_core/topics/pubsub_topic.nim @@ -54,20 +54,18 @@ proc parseStaticSharding*( let clusterPart = parts[0] if clusterPart.len == 0: return err(ParsingError.missingPart("cluster_id")) - let clusterId = - ?Base10.decode(uint16, clusterPart).mapErr( - proc(err: auto): auto = - ParsingError.invalidFormat($err) - ) + let clusterId = ?Base10.decode(uint16, clusterPart).mapErr( + proc(err: auto): auto = + ParsingError.invalidFormat($err) + ) let shardPart = parts[1] if shardPart.len == 0: return err(ParsingError.missingPart("shard_number")) - let shardId = - ?Base10.decode(uint16, shardPart).mapErr( - proc(err: auto): auto = - ParsingError.invalidFormat($err) - ) + let shardId = ?Base10.decode(uint16, shardPart).mapErr( + proc(err: auto): auto = + ParsingError.invalidFormat($err) + ) ok(RelayShard(clusterId: clusterId, shardId: shardId)) diff --git a/waku/waku_core/topics/sharding.nim b/waku/waku_core/topics/sharding.nim index 006850acf..1cb5b37b3 100644 --- a/waku/waku_core/topics/sharding.nim +++ b/waku/waku_core/topics/sharding.nim @@ -59,12 +59,8 @@ proc getShardsFromContentTopics*( else: @[contentTopics] - let parseRes = NsContentTopic.parse(topics) - let nsContentTopics = - if parseRes.isErr(): - return err("Cannot parse content topic: " & $parseRes.error) - else: - parseRes.get() + let nsContentTopics = NsContentTopic.parse(topics).valueOr: + return err("Cannot parse content topic: " & $error) var topicMap = initTable[RelayShard, seq[NsContentTopic]]() for content in nsContentTopics: diff --git a/waku/waku_enr/capabilities.nim b/waku/waku_enr/capabilities.nim index b4e2bf37a..26899fbb4 100644 --- a/waku/waku_enr/capabilities.nim +++ b/waku/waku_enr/capabilities.nim @@ -94,11 +94,10 @@ func waku2*(record: TypedRecord): Option[CapabilitiesBitfield] = some(CapabilitiesBitfield(field.get()[0])) proc supportsCapability*(r: Record, cap: Capabilities): bool = - let recordRes = r.toTyped() - if recordRes.isErr(): + let recordRes = r.toTyped().valueOr: return false - let bitfieldOpt = recordRes.value.waku2 + let bitfieldOpt = recordRes.waku2 if bitfieldOpt.isNone(): return false @@ -106,11 +105,10 @@ proc supportsCapability*(r: Record, cap: Capabilities): bool = bitfield.supportsCapability(cap) proc getCapabilities*(r: Record): seq[Capabilities] = - let recordRes = r.toTyped() - if recordRes.isErr(): + let recordRes = r.toTyped().valueOr: return @[] - let bitfieldOpt = recordRes.value.waku2 + let bitfieldOpt = recordRes.waku2 if bitfieldOpt.isNone(): return @[] diff --git a/waku/waku_enr/multiaddr.nim b/waku/waku_enr/multiaddr.nim index 83e3d1992..4d6e9baa7 100644 --- a/waku/waku_enr/multiaddr.nim +++ b/waku/waku_enr/multiaddr.nim @@ -74,7 +74,7 @@ func stripPeerId(multiaddr: MultiAddress): MultiAddress = return cleanAddr func withMultiaddrs*(builder: var EnrBuilder, multiaddrs: seq[MultiAddress]) = - let multiaddrs = multiaddrs.map(stripPeerId) + let multiaddrs = deduplicate(multiaddrs.map(stripPeerId)) let value = encodeMultiaddrs(multiaddrs) builder.addFieldPair(MultiaddrEnrField, value) @@ -88,8 +88,7 @@ func multiaddrs*(record: TypedRecord): Option[seq[MultiAddress]] = if field.isNone(): return none(seq[MultiAddress]) - let decodeRes = decodeMultiaddrs(field.get()) - if decodeRes.isErr(): + let decodeRes = decodeMultiaddrs(field.get()).valueOr: return none(seq[MultiAddress]) - some(decodeRes.value) + some(decodeRes) diff --git a/waku/waku_enr/sharding.nim b/waku/waku_enr/sharding.nim index d54464f94..2aeb96a9d 100644 --- a/waku/waku_enr/sharding.nim +++ b/waku/waku_enr/sharding.nim @@ -64,16 +64,15 @@ func topicsToRelayShards*(topics: seq[string]): Result[Option[RelayShards], stri let parsedTopicsRes = topics.mapIt(RelayShard.parse(it)) for res in parsedTopicsRes: - if res.isErr(): - return err("failed to parse topic: " & $res.error) + res.isOkOr: + return err("failed to parse topic: " & $error) if parsedTopicsRes.anyIt(it.get().clusterId != parsedTopicsRes[0].get().clusterId): return err("use shards with the same cluster Id.") - let relayShard = - ?RelayShards.init( - parsedTopicsRes[0].get().clusterId, parsedTopicsRes.mapIt(it.get().shardId) - ) + let relayShard = ?RelayShards.init( + parsedTopicsRes[0].get().clusterId, parsedTopicsRes.mapIt(it.get().shardId) + ) return ok(some(relayShard)) @@ -84,11 +83,10 @@ func contains*(rs: RelayShards, shard: RelayShard): bool = return rs.contains(shard.clusterId, shard.shardId) func contains*(rs: RelayShards, topic: PubsubTopic): bool = - let parseRes = RelayShard.parse(topic) - if parseRes.isErr(): + let parseRes = RelayShard.parse(topic).valueOr: return false - rs.contains(parseRes.value) + rs.contains(parseRes) # ENR builder extension @@ -239,12 +237,11 @@ proc containsShard*(r: Record, shard: RelayShard): bool = return containsShard(r, shard.clusterId, shard.shardId) proc containsShard*(r: Record, topic: PubsubTopic): bool = - let parseRes = RelayShard.parse(topic) - if parseRes.isErr(): - info "invalid static sharding topic", topic = topic, error = parseRes.error + let parseRes = RelayShard.parse(topic).valueOr: + info "invalid static sharding topic", topic = topic, error = error return false - containsShard(r, parseRes.value) + containsShard(r, parseRes) proc isClusterMismatched*(record: Record, clusterId: uint16): bool = ## Check the ENR sharding info for matching cluster id diff --git a/waku/waku_filter_v2/client.nim b/waku/waku_filter_v2/client.nim index 1dc018150..7798f41b7 100644 --- a/waku/waku_filter_v2/client.nim +++ b/waku/waku_filter_v2/client.nim @@ -8,11 +8,11 @@ import chronos, libp2p/protocols/protocol, bearssl/rand, - stew/byteutils + stew/byteutils, + brokers/broker_context + import - ../node/peer_manager, - ../node/delivery_monitor/subscriptions_observer, - ../waku_core, + waku/[node/peer_manager, waku_core, events/delivery_events], ./common, ./protocol_metrics, ./rpc_codec, @@ -22,18 +22,15 @@ logScope: topics = "waku filter client" type WakuFilterClient* = ref object of LPProtocol + brokerCtx: BrokerContext rng: ref HmacDrbgContext peerManager: PeerManager pushHandlers: seq[FilterPushHandler] - subscrObservers: seq[SubscriptionObserver] func generateRequestId(rng: ref HmacDrbgContext): string = var bytes: array[10, byte] hmacDrbgGenerate(rng[], bytes) - return toHex(bytes) - -proc addSubscrObserver*(wfc: WakuFilterClient, obs: SubscriptionObserver) = - wfc.subscrObservers.add(obs) + return byteutils.toHex(bytes) proc sendSubscribeRequest( wfc: WakuFilterClient, @@ -80,14 +77,11 @@ proc sendSubscribeRequest( waku_filter_errors.inc(labelValues = [errMsg]) return err(FilterSubscribeError.badResponse(errMsg)) - let respDecodeRes = FilterSubscribeResponse.decode(respBuf) - if respDecodeRes.isErr(): + let response = FilterSubscribeResponse.decode(respBuf).valueOr: trace "Failed to decode filter subscribe response", servicePeer waku_filter_errors.inc(labelValues = [decodeRpcFailure]) return err(FilterSubscribeError.badResponse(decodeRpcFailure)) - let response = respDecodeRes.get() - # DOS protection rate limit checks does not know about request id if response.statusCode != FilterSubscribeErrorKind.TOO_MANY_REQUESTS.uint32 and response.requestId != filterSubscribeRequest.requestId: @@ -108,12 +102,20 @@ proc sendSubscribeRequest( return ok() proc ping*( - wfc: WakuFilterClient, servicePeer: RemotePeerInfo + wfc: WakuFilterClient, servicePeer: RemotePeerInfo, timeout = chronos.seconds(0) ): Future[FilterSubscribeResult] {.async.} = info "sending ping", servicePeer = shortLog($servicePeer) let requestId = generateRequestId(wfc.rng) let filterSubscribeRequest = FilterSubscribeRequest.ping(requestId) + if timeout > chronos.seconds(0): + let fut = wfc.sendSubscribeRequest(servicePeer, filterSubscribeRequest) + if not await fut.withTimeout(timeout): + return err( + FilterSubscribeError.parse(uint32(FilterSubscribeErrorKind.PEER_DIAL_FAILURE)) + ) + return fut.read() + return await wfc.sendSubscribeRequest(servicePeer, filterSubscribeRequest) proc subscribe*( @@ -135,8 +137,7 @@ proc subscribe*( ?await wfc.sendSubscribeRequest(servicePeer, filterSubscribeRequest) - for obs in wfc.subscrObservers: - obs.onSubscribe(pubSubTopic, contentTopicSeq) + OnFilterSubscribeEvent.emit(wfc.brokerCtx, pubsubTopic, contentTopicSeq) return ok() @@ -159,8 +160,7 @@ proc unsubscribe*( ?await wfc.sendSubscribeRequest(servicePeer, filterSubscribeRequest) - for obs in wfc.subscrObservers: - obs.onUnsubscribe(pubSubTopic, contentTopicSeq) + OnFilterUnSubscribeEvent.emit(wfc.brokerCtx, pubsubTopic, contentTopicSeq) return ok() @@ -213,6 +213,9 @@ proc initProtocolHandler(wfc: WakuFilterClient) = proc new*( T: type WakuFilterClient, peerManager: PeerManager, rng: ref HmacDrbgContext ): T = - let wfc = WakuFilterClient(rng: rng, peerManager: peerManager, pushHandlers: @[]) + let brokerCtx = globalBrokerContext() + let wfc = WakuFilterClient( + brokerCtx: brokerCtx, rng: rng, peerManager: peerManager, pushHandlers: @[] + ) wfc.initProtocolHandler() wfc diff --git a/waku/waku_filter_v2/protocol.nim b/waku/waku_filter_v2/protocol.nim index 5e9b48496..35620b6cd 100644 --- a/waku/waku_filter_v2/protocol.nim +++ b/waku/waku_filter_v2/protocol.nim @@ -157,15 +157,14 @@ proc handleSubscribeRequest*( requestDurationSec, labelValues = [$request.filterSubscribeType] ) - if subscribeResult.isErr(): + subscribeResult.isOkOr: error "subscription request error", peerId = shortLog(peerId), request = request return FilterSubscribeResponse( requestId: request.requestId, - statusCode: subscribeResult.error.kind.uint32, - statusDesc: some($subscribeResult.error), + statusCode: error.kind.uint32, + statusDesc: some($error), ) - else: - return FilterSubscribeResponse.ok(request.requestId) + return FilterSubscribeResponse.ok(request.requestId) proc pushToPeer( wf: WakuFilter, peerId: PeerId, buffer: seq[byte] @@ -245,7 +244,8 @@ proc handleMessage*( ) {.async.} = let msgHash = computeMessageHash(pubsubTopic, message).to0xHex() - info "handling message", pubsubTopic = pubsubTopic, msg_hash = msgHash + info "handling message", + pubsubTopic = pubsubTopic, contentTopic = message.contentTopic, msg_hash = msgHash let handleMessageStartTime = Moment.now() @@ -309,15 +309,12 @@ proc initProtocolHandler(wf: WakuFilter) = amount = buf.len().int64, labelValues = [WakuFilterSubscribeCodec, "in"] ) - let decodeRes = FilterSubscribeRequest.decode(buf) - if decodeRes.isErr(): + let request = FilterSubscribeRequest.decode(buf).valueOr: error "failed to decode filter subscribe request", - peer_id = conn.peerId, err = decodeRes.error + peer_id = conn.peerId, err = error waku_filter_errors.inc(labelValues = [decodeRpcFailure]) return - let request = decodeRes.value #TODO: toAPI() split here - try: response = await wf.handleSubscribeRequest(conn.peerId, request) except CatchableError: diff --git a/waku/waku_keystore/keyfile.nim b/waku/waku_keystore/keyfile.nim index 488e241ab..c84a45dba 100644 --- a/waku/waku_keystore/keyfile.nim +++ b/waku/waku_keystore/keyfile.nim @@ -1,4 +1,4 @@ -# This implementation is originally taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile and adapted to +# This implementation is originally taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile and adapted to # - create keyfiles for arbitrary-long input byte data (rather than fixed-size private keys) # - allow storage of multiple keyfiles (encrypted with different passwords) in same file and iteration among successful decryptions # - enable/disable at compilation time the keyfile id and version fields @@ -517,26 +517,15 @@ func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] = proc decodeKeyFileJson*(j: JsonNode, password: string): KfResult[seq[byte]] = ## Decode secret from keyfile json object ``j`` using ## password string ``password``. - let res = decodeCrypto(j) - if res.isErr: - return err(res.error) - let crypto = res.get() + let crypto = ?decodeCrypto(j) case crypto.kind of PBKDF2: - let res = decodePbkdf2Params(crypto.kdfParams) - if res.isErr: - return err(res.error) - - let params = res.get() + let params = ?decodePbkdf2Params(crypto.kdfParams) let dkey = ?deriveKey(password, params.salt, PBKDF2, params.prf, params.c) return decryptSecret(crypto, dkey) of SCRYPT: - let res = decodeScryptParams(crypto.kdfParams) - if res.isErr: - return err(res.error) - - let params = res.get() + let params = ?decodeScryptParams(crypto.kdfParams) let dkey = ?deriveKey(password, params.salt, params.n, params.r, params.p) return decryptSecret(crypto, dkey) diff --git a/waku/waku_keystore/keystore.nim b/waku/waku_keystore/keystore.nim index 6cc4ef701..158f1a98e 100644 --- a/waku/waku_keystore/keystore.nim +++ b/waku/waku_keystore/keystore.nim @@ -50,9 +50,7 @@ proc loadAppKeystore*( # If no keystore exists at path we create a new empty one with passed keystore parameters if fileExists(path) == false: - let newKeystoreRes = createAppKeystore(path, appInfo, separator) - if newKeystoreRes.isErr(): - return err(newKeystoreRes.error) + ?createAppKeystore(path, appInfo, separator) try: # We read all the file contents @@ -175,13 +173,9 @@ proc addMembershipCredentials*( ): KeystoreResult[void] = # We load the keystore corresponding to the desired parameters # This call ensures that JSON has all required fields - let jsonKeystoreRes = loadAppKeystore(path, appInfo, separator) - - if jsonKeystoreRes.isErr(): - return err(jsonKeystoreRes.error) # We load the JSON node corresponding to the app keystore - var jsonKeystore = jsonKeystoreRes.get() + let jsonKeystore = ?loadAppKeystore(path, appInfo, separator) try: if jsonKeystore.hasKey("credentials"): @@ -193,21 +187,16 @@ proc addMembershipCredentials*( return ok() let encodedMembershipCredential = membership.encode() - let keyfileRes = createKeyFileJson(encodedMembershipCredential, password) - if keyfileRes.isErr(): - return err( - AppKeystoreError(kind: KeystoreCreateKeyfileError, msg: $keyfileRes.error) - ) - # We add it to the credentials field of the keystore - jsonKeystore["credentials"][key] = keyfileRes.get() + jsonKeystore["credentials"][key] = createKeyFileJson( + encodedMembershipCredential, password + ).valueOr: + return err(AppKeystoreError(kind: KeystoreCreateKeyfileError, msg: $error)) except CatchableError: return err(AppKeystoreError(kind: KeystoreJsonError, msg: getCurrentExceptionMsg())) # We save to disk the (updated) keystore. - let saveRes = save(jsonKeystore, path, separator) - if saveRes.isErr(): - return err(saveRes.error) + ?save(jsonKeystore, path, separator) return ok() @@ -218,13 +207,9 @@ proc getMembershipCredentials*( ): KeystoreResult[KeystoreMembership] = # We load the keystore corresponding to the desired parameters # This call ensures that JSON has all required fields - let jsonKeystoreRes = loadAppKeystore(path, appInfo) - - if jsonKeystoreRes.isErr(): - return err(jsonKeystoreRes.error) # We load the JSON node corresponding to the app keystore - var jsonKeystore = jsonKeystoreRes.get() + let jsonKeystore = ?loadAppKeystore(path, appInfo) try: if jsonKeystore.hasKey("credentials"): @@ -254,15 +239,10 @@ proc getMembershipCredentials*( ) keystoreCredential = keystoreCredentials[key] - let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password) - if decodedKeyfileRes.isErr(): - return err( - AppKeystoreError( - kind: KeystoreReadKeyfileError, msg: $decodedKeyfileRes.error - ) - ) + let decodedKeyfile = decodeKeyFileJson(keystoreCredential, password).valueOr: + return err(AppKeystoreError(kind: KeystoreReadKeyfileError, msg: $error)) # we parse the json decrypted keystoreCredential - let decodedCredentialRes = decode(decodedKeyfileRes.get()) + let decodedCredentialRes = decode(decodedKeyfile) let keyfileMembershipCredential = decodedCredentialRes.get() return ok(keyfileMembershipCredential) except CatchableError: diff --git a/waku/waku_keystore/protocol_types.nim b/waku/waku_keystore/protocol_types.nim index 6cfc2f183..0f50c66ee 100644 --- a/waku/waku_keystore/protocol_types.nim +++ b/waku/waku_keystore/protocol_types.nim @@ -119,10 +119,9 @@ proc `==`*(x, y: KeystoreMembership): bool = proc hash*(m: KeystoreMembership): string = # hash together the chainId, address and treeIndex - return - $sha256.digest( - m.membershipContract.chainId & m.membershipContract.address & $m.treeIndex - ) + return $sha256.digest( + m.membershipContract.chainId & m.membershipContract.address & $m.treeIndex + ) type MembershipTable* = Table[string, KeystoreMembership] diff --git a/waku/waku_lightpush/callbacks.nim b/waku/waku_lightpush/callbacks.nim index 4b362e6bb..ac2e562b6 100644 --- a/waku/waku_lightpush/callbacks.nim +++ b/waku/waku_lightpush/callbacks.nim @@ -26,13 +26,12 @@ proc checkAndGenerateRLNProof*( time = getTime().toUnix() senderEpochTime = float64(time) var msgWithProof = message - rlnPeer.get().appendRLNProof(msgWithProof, senderEpochTime).isOkOr: - return err(error) + ?(rlnPeer.get().appendRLNProof(msgWithProof, senderEpochTime)) return ok(msgWithProof) proc getNilPushHandler*(): PushMessageHandler = return proc( - peer: PeerId, pubsubTopic: string, message: WakuMessage + pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = return lightpushResultInternalError("no waku relay found") @@ -40,7 +39,7 @@ proc getRelayPushHandler*( wakuRelay: WakuRelay, rlnPeer: Option[WakuRLNRelay] = none[WakuRLNRelay]() ): PushMessageHandler = return proc( - peer: PeerId, pubsubTopic: string, message: WakuMessage + pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult] {.async.} = # append RLN proof let msgWithProof = checkAndGenerateRLNProof(rlnPeer, message).valueOr: @@ -49,12 +48,10 @@ proc getRelayPushHandler*( (await wakuRelay.validateMessage(pubSubTopic, msgWithProof)).isOkOr: return lighpushErrorResult(LightPushErrorCode.INVALID_MESSAGE, $error) - let publishedResult = await wakuRelay.publish(pubsubTopic, msgWithProof) - - if publishedResult.isErr(): + let publishedResult = (await wakuRelay.publish(pubsubTopic, msgWithProof)).valueOr: let msgHash = computeMessageHash(pubsubTopic, message).to0xHex() notice "Lightpush request has not been published to any peers", - msg_hash = msgHash, reason = $publishedResult.error - return mapPubishingErrorToPushResult(publishedResult.error) + msg_hash = msgHash, reason = $error + return mapPubishingErrorToPushResult(error) - return lightpushSuccessResult(publishedResult.get().uint32) + return lightpushSuccessResult(publishedResult.uint32) diff --git a/waku/waku_lightpush/client.nim b/waku/waku_lightpush/client.nim index 4d0c49a84..fd12c49d2 100644 --- a/waku/waku_lightpush/client.nim +++ b/waku/waku_lightpush/client.nim @@ -5,7 +5,6 @@ import libp2p/peerid, libp2p/stream/connection import ../waku_core/peers, ../node/peer_manager, - ../node/delivery_monitor/publish_observer, ../utils/requests, ../waku_core, ./common, @@ -17,17 +16,24 @@ logScope: topics = "waku lightpush client" type WakuLightPushClient* = ref object - peerManager*: PeerManager rng*: ref rand.HmacDrbgContext - publishObservers: seq[PublishObserver] + peerManager*: PeerManager proc new*( T: type WakuLightPushClient, peerManager: PeerManager, rng: ref rand.HmacDrbgContext ): T = WakuLightPushClient(peerManager: peerManager, rng: rng) -proc addPublishObserver*(wl: WakuLightPushClient, obs: PublishObserver) = - wl.publishObservers.add(obs) +proc ensureTimestampSet(message: var WakuMessage) = + if message.timestamp == 0: + message.timestamp = getNowInNanosecondTime() + +## Short log string for peer identifiers (overloads for convenience) +func shortPeerId(peer: PeerId): string = + shortLog(peer) + +func shortPeerId(peer: RemotePeerInfo): string = + shortLog(peer.peerId) proc sendPushRequest( wl: WakuLightPushClient, @@ -74,64 +80,47 @@ proc publish*( wl: WakuLightPushClient, pubSubTopic: Option[PubsubTopic] = none(PubsubTopic), wakuMessage: WakuMessage, - peer: PeerId | RemotePeerInfo, + dest: Connection | PeerId | RemotePeerInfo, ): Future[WakuLightPushResult] {.async, gcsafe.} = var message = wakuMessage - if message.timestamp == 0: - message.timestamp = getNowInNanosecondTime() + ensureTimestampSet(message) - when peer is PeerId: - info "publish", - peerId = shortLog(peer), - msg_hash = computeMessageHash(pubsubTopic.get(""), message).to0xHex - else: - info "publish", - peerId = shortLog(peer.peerId), - msg_hash = computeMessageHash(pubsubTopic.get(""), message).to0xHex + let msgHash = computeMessageHash(pubSubTopic.get(""), message).to0xHex() - let pushRequest = LightpushRequest( - requestId: generateRequestId(wl.rng), pubSubTopic: pubSubTopic, message: message + let peerIdStr = + when dest is Connection: + shortPeerId(dest.peerId) + else: + shortPeerId(dest) + + info "publish", + myPeerId = wl.peerManager.switch.peerInfo.peerId, + peerId = peerIdStr, + msgHash = msgHash, + sentTime = getNowInNanosecondTime() + + let request = LightpushRequest( + requestId: generateRequestId(wl.rng), pubsubTopic: pubSubTopic, message: message ) - let publishedCount = ?await wl.sendPushRequest(pushRequest, peer) - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic.get(""), message) + let relayPeerCount = + when dest is Connection: + ?await wl.sendPushRequest(request, dest.peerId, some(dest)) + else: + ?await wl.sendPushRequest(request, dest) - return lightpushSuccessResult(publishedCount) + return lightpushSuccessResult(relayPeerCount) proc publishToAny*( - wl: WakuLightPushClient, pubSubTopic: PubsubTopic, wakuMessage: WakuMessage + wl: WakuLightPushClient, pubsubTopic: PubsubTopic, wakuMessage: WakuMessage ): Future[WakuLightPushResult] {.async, gcsafe.} = - ## This proc is similar to the publish one but in this case - ## we don't specify a particular peer and instead we get it from peer manager - - var message = wakuMessage - if message.timestamp == 0: - message.timestamp = getNowInNanosecondTime() - + # Like publish, but selects a peer automatically from the peer manager let peer = wl.peerManager.selectPeer(WakuLightPushCodec).valueOr: # TODO: check if it is matches the situation - shall we distinguish client side missing peers from server side? return lighpushErrorResult( LightPushErrorCode.NO_PEERS_TO_RELAY, "no suitable remote peers" ) - - info "publishToAny", - my_peer_id = wl.peerManager.switch.peerInfo.peerId, - peer_id = peer.peerId, - msg_hash = computeMessageHash(pubsubTopic, message).to0xHex, - sentTime = getNowInNanosecondTime() - - let pushRequest = LightpushRequest( - requestId: generateRequestId(wl.rng), - pubSubTopic: some(pubSubTopic), - message: message, - ) - let publishedCount = ?await wl.sendPushRequest(pushRequest, peer) - - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - - return lightpushSuccessResult(publishedCount) + return await wl.publish(some(pubsubTopic), wakuMessage, peer) proc publishWithConn*( wl: WakuLightPushClient, @@ -140,22 +129,4 @@ proc publishWithConn*( conn: Connection, destPeer: PeerId, ): Future[WakuLightPushResult] {.async, gcsafe.} = - info "publishWithConn", - my_peer_id = wl.peerManager.switch.peerInfo.peerId, - peer_id = destPeer, - msg_hash = computeMessageHash(pubsubTopic, message).to0xHex, - sentTime = getNowInNanosecondTime() - - let pushRequest = LightpushRequest( - requestId: generateRequestId(wl.rng), - pubSubTopic: some(pubSubTopic), - message: message, - ) - #TODO: figure out how to not pass destPeer as this is just a hack - let publishedCount = - ?await wl.sendPushRequest(pushRequest, destPeer, conn = some(conn)) - - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - - return lightpushSuccessResult(publishedCount) + return await wl.publish(some(pubSubTopic), message, conn) diff --git a/waku/waku_lightpush/common.nim b/waku/waku_lightpush/common.nim index f2687834e..f0762e2d2 100644 --- a/waku/waku_lightpush/common.nim +++ b/waku/waku_lightpush/common.nim @@ -25,7 +25,7 @@ type ErrorStatus* = tuple[code: LightpushStatusCode, desc: Option[string]] type WakuLightPushResult* = Result[uint32, ErrorStatus] type PushMessageHandler* = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult] {.async.} const TooManyRequestsMessage* = "Request rejected due to too many requests" @@ -35,7 +35,15 @@ func isSuccess*(response: LightPushResponse): bool = func toPushResult*(response: LightPushResponse): WakuLightPushResult = if isSuccess(response): - return ok(response.relayPeerCount.get(0)) + let relayPeerCount = response.relayPeerCount.get(0) + return ( + if (relayPeerCount == 0): + # Consider publishing to zero peers an error even if the service node + # sent us a "successful" response with zero peers + err((LightPushErrorCode.NO_PEERS_TO_RELAY, response.statusDesc)) + else: + ok(relayPeerCount) + ) else: return err((response.statusCode, response.statusDesc)) @@ -51,11 +59,6 @@ func lightpushResultBadRequest*(msg: string): WakuLightPushResult = func lightpushResultServiceUnavailable*(msg: string): WakuLightPushResult = return err((LightPushErrorCode.SERVICE_NOT_AVAILABLE, some(msg))) -func lighpushErrorResult*( - statusCode: LightpushStatusCode, desc: Option[string] -): WakuLightPushResult = - return err((statusCode, desc)) - func lighpushErrorResult*( statusCode: LightpushStatusCode, desc: string ): WakuLightPushResult = diff --git a/waku/waku_lightpush/protocol.nim b/waku/waku_lightpush/protocol.nim index 2e8c9c2f1..8336f4dfc 100644 --- a/waku/waku_lightpush/protocol.nim +++ b/waku/waku_lightpush/protocol.nim @@ -68,19 +68,20 @@ proc handleRequest( peer_id = peerId, requestId = pushRequest.requestId, pubsubTopic = pushRequest.pubsubTopic, + contentTopic = pushRequest.message.contentTopic, msg_hash = msg_hash, receivedTime = getNowInNanosecondTime() - let res = (await wl.pushHandler(peerId, pubsubTopic, pushRequest.message)).valueOr: + let res = (await wl.pushHandler(pubsubTopic, pushRequest.message)).valueOr: return err((code: error.code, desc: error.desc)) return ok(res) proc handleRequest*( wl: WakuLightPush, peerId: PeerId, buffer: seq[byte] ): Future[LightPushResponse] {.async.} = - let pushRequest = LightPushRequest.decode(buffer).valueOr: + let request = LightPushRequest.decode(buffer).valueOr: let desc = decodeRpcFailure & ": " & $error - error "failed to push message", error = desc + error "failed to decode Lightpush request", error = desc let errorCode = LightPushErrorCode.BAD_REQUEST waku_lightpush_v3_errors.inc(labelValues = [$errorCode]) return LightPushResponse( @@ -89,16 +90,16 @@ proc handleRequest*( statusDesc: some(desc), ) - let relayPeerCount = (await handleRequest(wl, peerId, pushRequest)).valueOr: + let relayPeerCount = (await wl.handleRequest(peerId, request)).valueOr: let desc = error.desc waku_lightpush_v3_errors.inc(labelValues = [$error.code]) error "failed to push message", error = desc return LightPushResponse( - requestId: pushRequest.requestId, statusCode: error.code, statusDesc: desc + requestId: request.requestId, statusCode: error.code, statusDesc: desc ) return LightPushResponse( - requestId: pushRequest.requestId, + requestId: request.requestId, statusCode: LightPushSuccessCode.SUCCESS, statusDesc: none[string](), relayPeerCount: some(relayPeerCount), @@ -123,7 +124,7 @@ proc initProtocolHandler(wl: WakuLightPush) = ) try: - rpc = await handleRequest(wl, conn.peerId, buffer) + rpc = await wl.handleRequest(conn.peerId, buffer) except CatchableError: error "lightpush failed handleRequest", error = getCurrentExceptionMsg() do: diff --git a/waku/waku_lightpush_legacy/callbacks.nim b/waku/waku_lightpush_legacy/callbacks.nim index f5a79eadc..a5b88b5b8 100644 --- a/waku/waku_lightpush_legacy/callbacks.nim +++ b/waku/waku_lightpush_legacy/callbacks.nim @@ -25,13 +25,12 @@ proc checkAndGenerateRLNProof*( time = getTime().toUnix() senderEpochTime = float64(time) var msgWithProof = message - rlnPeer.get().appendRLNProof(msgWithProof, senderEpochTime).isOkOr: - return err(error) + ?(rlnPeer.get().appendRLNProof(msgWithProof, senderEpochTime)) return ok(msgWithProof) proc getNilPushHandler*(): PushMessageHandler = return proc( - peer: PeerId, pubsubTopic: string, message: WakuMessage + pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = return err("no waku relay found") @@ -39,22 +38,18 @@ proc getRelayPushHandler*( wakuRelay: WakuRelay, rlnPeer: Option[WakuRLNRelay] = none[WakuRLNRelay]() ): PushMessageHandler = return proc( - peer: PeerId, pubsubTopic: string, message: WakuMessage + pubsubTopic: string, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} = # append RLN proof - let msgWithProof = checkAndGenerateRLNProof(rlnPeer, message) - if msgWithProof.isErr(): - return err(msgWithProof.error) + let msgWithProof = ?checkAndGenerateRLNProof(rlnPeer, message) - (await wakuRelay.validateMessage(pubSubTopic, msgWithProof.value)).isOkOr: - return err(error) + ?(await wakuRelay.validateMessage(pubSubTopic, msgWithProof)) - let publishResult = await wakuRelay.publish(pubsubTopic, msgWithProof.value) - if publishResult.isErr(): + (await wakuRelay.publish(pubsubTopic, msgWithProof)).isOkOr: ## Agreed change expected to the lightpush protocol to better handle such case. https://github.com/waku-org/pm/issues/93 let msgHash = computeMessageHash(pubsubTopic, message).to0xHex() notice "Lightpush request has not been published to any peers", - msg_hash = msgHash, reason = $publishResult.error + msg_hash = msgHash, reason = $error # for legacy lightpush we do not detail the reason towards clients. All error during publish result in not-published-to-any-peer # this let client of the legacy protocol to react as they did so far. return err(protocol_metrics.notPublishedAnyPeer) diff --git a/waku/waku_lightpush_legacy/client.nim b/waku/waku_lightpush_legacy/client.nim index ee234c996..ab489bec9 100644 --- a/waku/waku_lightpush_legacy/client.nim +++ b/waku/waku_lightpush_legacy/client.nim @@ -5,7 +5,6 @@ import libp2p/peerid import ../waku_core/peers, ../node/peer_manager, - ../node/delivery_monitor/publish_observer, ../utils/requests, ../waku_core, ./common, @@ -19,7 +18,6 @@ logScope: type WakuLegacyLightPushClient* = ref object peerManager*: PeerManager rng*: ref rand.HmacDrbgContext - publishObservers: seq[PublishObserver] proc new*( T: type WakuLegacyLightPushClient, @@ -28,9 +26,6 @@ proc new*( ): T = WakuLegacyLightPushClient(peerManager: peerManager, rng: rng) -proc addPublishObserver*(wl: WakuLegacyLightPushClient, obs: PublishObserver) = - wl.publishObservers.add(obs) - proc sendPushRequest( wl: WakuLegacyLightPushClient, req: PushRequest, peer: PeerId | RemotePeerInfo ): Future[WakuLightPushResult[void]] {.async, gcsafe.} = @@ -52,13 +47,11 @@ proc sendPushRequest( except LPStreamRemoteClosedError: return err("Exception reading: " & getCurrentExceptionMsg()) - let decodeRespRes = PushRPC.decode(buffer) - if decodeRespRes.isErr(): + let pushResponseRes = PushRPC.decode(buffer).valueOr: error "failed to decode response" waku_lightpush_errors.inc(labelValues = [decodeRpcFailure]) return err(decodeRpcFailure) - let pushResponseRes = decodeRespRes.get() if pushResponseRes.response.isNone(): waku_lightpush_errors.inc(labelValues = [emptyResponseBodyFailure]) return err(emptyResponseBodyFailure) @@ -88,9 +81,6 @@ proc publish*( let pushRequest = PushRequest(pubSubTopic: pubSubTopic, message: message) ?await wl.sendPushRequest(pushRequest, peer) - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - notice "publishing message with lightpush", pubsubTopic = pubsubTopic, contentTopic = message.contentTopic, @@ -113,7 +103,4 @@ proc publishToAny*( let pushRequest = PushRequest(pubSubTopic: pubSubTopic, message: message) ?await wl.sendPushRequest(pushRequest, peer) - for obs in wl.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - return ok() diff --git a/waku/waku_lightpush_legacy/common.nim b/waku/waku_lightpush_legacy/common.nim index fcdf1814c..1b40ba72b 100644 --- a/waku/waku_lightpush_legacy/common.nim +++ b/waku/waku_lightpush_legacy/common.nim @@ -9,7 +9,7 @@ export WakuLegacyLightPushCodec type WakuLightPushResult*[T] = Result[T, string] type PushMessageHandler* = proc( - peer: PeerId, pubsubTopic: PubsubTopic, message: WakuMessage + pubsubTopic: PubsubTopic, message: WakuMessage ): Future[WakuLightPushResult[void]] {.async.} const TooManyRequestsMessage* = "TOO_MANY_REQUESTS" diff --git a/waku/waku_lightpush_legacy/protocol.nim b/waku/waku_lightpush_legacy/protocol.nim index d51943cff..f5ed60134 100644 --- a/waku/waku_lightpush_legacy/protocol.nim +++ b/waku/waku_lightpush_legacy/protocol.nim @@ -50,10 +50,11 @@ proc handleRequest*( peer_id = peerId, requestId = requestId, pubsubTopic = pubsubTopic, + contentTopic = message.contentTopic, msg_hash = msg_hash, receivedTime = getNowInNanosecondTime() - let handleRes = await wl.pushHandler(peerId, pubsubTopic, message) + let handleRes = await wl.pushHandler(pubsubTopic, message) isSuccess = handleRes.isOk() pushResponseInfo = (if isSuccess: "OK" else: handleRes.error) diff --git a/waku/waku_metadata/protocol.nim b/waku/waku_metadata/protocol.nim index c112cc5d5..7c72a6934 100644 --- a/waku/waku_metadata/protocol.nim +++ b/waku/waku_metadata/protocol.nim @@ -33,8 +33,8 @@ proc respond( let res = catch: await conn.writeLP(response.encode().buffer) - if res.isErr(): - return err(res.error.msg) + res.isOkOr: + return err(error.msg) return ok() @@ -53,17 +53,14 @@ proc request*( # close no matter what let closeRes = catch: await conn.closeWithEof() - if closeRes.isErr(): - return err("close failed: " & closeRes.error.msg) + closeRes.isOkOr: + return err("close failed: " & error.msg) - if writeRes.isErr(): - return err("write failed: " & writeRes.error.msg) + writeRes.isOkOr: + return err("write failed: " & error.msg) - let buffer = - if readRes.isErr(): - return err("read failed: " & readRes.error.msg) - else: - readRes.get() + let buffer = readRes.valueOr: + return err("read failed: " & error.msg) let response = WakuMetadataResponse.decode(buffer).valueOr: return err("decode failed: " & $error) @@ -111,9 +108,3 @@ proc new*(T: type WakuMetadata, clusterId: uint32, getShards: GetShards): T = clusterId = wm.clusterId, shards = wm.getShards() return wm - -proc start*(wm: WakuMetadata) = - wm.started = true - -proc stop*(wm: WakuMetadata) = - wm.started = false diff --git a/waku/waku_mix/protocol.nim b/waku/waku_mix/protocol.nim index 34b50f8a9..ac8b69eaf 100644 --- a/waku/waku_mix/protocol.nim +++ b/waku/waku_mix/protocol.nim @@ -1,31 +1,33 @@ {.push raises: [].} -import chronicles, std/[options, tables, sequtils], chronos, results, metrics, strutils +import chronicles, std/options, chronos, results, metrics import libp2p/crypto/curve25519, + libp2p/crypto/crypto, libp2p/protocols/mix, libp2p/protocols/mix/mix_node, - libp2p/[multiaddress, multicodec, peerid], + libp2p/protocols/mix/mix_protocol, + libp2p/protocols/mix/mix_metrics, + libp2p/protocols/mix/delay_strategy, + libp2p/[multiaddress, peerid], eth/common/keys import - ../node/peer_manager, - ../waku_core, - ../waku_enr, - ../node/peer_manager/waku_peer_store, - ../common/nimchronos + waku/node/peer_manager, + waku/waku_core, + waku/waku_enr, + waku/node/peer_manager/waku_peer_store logScope: topics = "waku mix" -const mixMixPoolSize = 3 +const minMixPoolSize = 4 type WakuMix* = ref object of MixProtocol peerManager*: PeerManager clusterId: uint16 - nodePoolLoopHandle: Future[void] pubKey*: Curve25519Key WakuMixResult*[T] = Result[T, string] @@ -34,91 +36,10 @@ type multiAddr*: string pubKey*: Curve25519Key -proc mixPoolFilter*(cluster: Option[uint16], peer: RemotePeerInfo): bool = - # Note that origin based(discv5) filtering is not done intentionally - # so that more mix nodes can be discovered. - if peer.enr.isNone(): - trace "peer has no ENR", peer = $peer - return false - - if cluster.isSome() and peer.enr.get().isClusterMismatched(cluster.get()): - trace "peer has mismatching cluster", peer = $peer - return false - - # Filter if mix is enabled - if not peer.enr.get().supportsCapability(Capabilities.Mix): - trace "peer doesn't support mix", peer = $peer - return false - - return true - -proc appendPeerIdToMultiaddr*(multiaddr: MultiAddress, peerId: PeerId): MultiAddress = - if multiaddr.contains(multiCodec("p2p")).get(): - return multiaddr - - var maddrStr = multiaddr.toString().valueOr: - error "Failed to convert multiaddress to string.", err = error - return multiaddr - maddrStr.add("/p2p/" & $peerId) - var cleanAddr = MultiAddress.init(maddrStr).valueOr: - error "Failed to convert string to multiaddress.", err = error - return multiaddr - return cleanAddr - -func getIPv4Multiaddr*(maddrs: seq[MultiAddress]): Option[MultiAddress] = - for multiaddr in maddrs: - trace "checking multiaddr", addr = $multiaddr - if multiaddr.contains(multiCodec("ip4")).get(): - trace "found ipv4 multiaddr", addr = $multiaddr - return some(multiaddr) - trace "no ipv4 multiaddr found" - return none(MultiAddress) - -#[ Not deleting as these can be reused once discovery is sorted - proc populateMixNodePool*(mix: WakuMix) = - # populate only peers that i) are reachable ii) share cluster iii) support mix - let remotePeers = mix.peerManager.switch.peerStore.peers().filterIt( - mixPoolFilter(some(mix.clusterId), it) - ) - var mixNodes = initTable[PeerId, MixPubInfo]() - - for i in 0 ..< min(remotePeers.len, 100): - let remotePeerENR = remotePeers[i].enr.get() - let ipv4addr = getIPv4Multiaddr(remotePeers[i].addrs).valueOr: - trace "peer has no ipv4 address", peer = $remotePeers[i] - continue - let maddrWithPeerId = - toString(appendPeerIdToMultiaddr(ipv4addr, remotePeers[i].peerId)) - trace "remote peer ENR", - peerId = remotePeers[i].peerId, enr = remotePeerENR, maddr = maddrWithPeerId - - let peerMixPubKey = mixKey(remotePeerENR).get() - let mixNodePubInfo = - createMixPubInfo(maddrWithPeerId.value, intoCurve25519Key(peerMixPubKey)) - mixNodes[remotePeers[i].peerId] = mixNodePubInfo - - mix_pool_size.set(len(mixNodes)) - # set the mix node pool - mix.setNodePool(mixNodes) - trace "mix node pool updated", poolSize = mix.getNodePoolSize() - -proc startMixNodePoolMgr*(mix: WakuMix) {.async.} = - info "starting mix node pool manager" - # try more aggressively to populate the pool at startup - var attempts = 50 - # TODO: make initial pool size configurable - while mix.getNodePoolSize() < 100 and attempts > 0: - attempts -= 1 - mix.populateMixNodePool() - await sleepAsync(1.seconds) - - # TODO: make interval configurable - heartbeat "Updating mix node pool", 5.seconds: - mix.populateMixNodePool() - ]# - -proc toMixNodeTable(bootnodes: seq[MixNodePubInfo]): Table[PeerId, MixPubInfo] = - var mixNodes = initTable[PeerId, MixPubInfo]() +proc processBootNodes( + bootnodes: seq[MixNodePubInfo], peermgr: PeerManager, mix: WakuMix +) = + var count = 0 for node in bootnodes: let pInfo = parsePeerInfo(node.multiAddr).valueOr: error "Failed to get peer id from multiaddress: ", @@ -139,12 +60,18 @@ proc toMixNodeTable(bootnodes: seq[MixNodePubInfo]): Table[PeerId, MixPubInfo] = error "Failed to parse multiaddress", multiAddr = node.multiAddr, error = error continue - mixNodes[peerId] = MixPubInfo.init(peerId, multiAddr, node.pubKey, peerPubKey.skkey) - info "using mix bootstrap nodes ", bootNodes = mixNodes - return mixNodes + let mixPubInfo = MixPubInfo.init(peerId, multiAddr, node.pubKey, peerPubKey.skkey) + mix.nodePool.add(mixPubInfo) + count.inc() + + peermgr.addPeer( + RemotePeerInfo.init(peerId, @[multiAddr], mixPubKey = some(node.pubKey)) + ) + mix_pool_size.set(count) + info "using mix bootstrap nodes ", count = count proc new*( - T: type WakuMix, + T: typedesc[WakuMix], nodeAddr: string, peermgr: PeerManager, clusterId: uint16, @@ -152,30 +79,29 @@ proc new*( bootnodes: seq[MixNodePubInfo], ): WakuMixResult[T] = let mixPubKey = public(mixPrivKey) - info "mixPrivKey", mixPrivKey = mixPrivKey, mixPubKey = mixPubKey + info "mixPubKey", mixPubKey = mixPubKey let nodeMultiAddr = MultiAddress.init(nodeAddr).valueOr: return err("failed to parse mix node address: " & $nodeAddr & ", error: " & error) let localMixNodeInfo = initMixNodeInfo( peermgr.switch.peerInfo.peerId, nodeMultiAddr, mixPubKey, mixPrivKey, peermgr.switch.peerInfo.publicKey.skkey, peermgr.switch.peerInfo.privateKey.skkey, ) - if bootnodes.len < mixMixPoolSize: - warn "publishing with mix won't work as there are less than 3 mix nodes in node pool" - let initTable = toMixNodeTable(bootnodes) - if len(initTable) < mixMixPoolSize: - warn "publishing with mix won't work as there are less than 3 mix nodes in node pool" + var m = WakuMix(peerManager: peermgr, clusterId: clusterId, pubKey: mixPubKey) - procCall MixProtocol(m).init(localMixNodeInfo, initTable, peermgr.switch) + procCall MixProtocol(m).init( + localMixNodeInfo, + peermgr.switch, + delayStrategy = + ExponentialDelayStrategy.new(meanDelayMs = 50, rng = crypto.newRng()), + ) + + processBootNodes(bootnodes, peermgr, m) + + if m.nodePool.len < minMixPoolSize: + warn "publishing with mix won't work until atleast 3 mix nodes in node pool" return ok(m) -method start*(mix: WakuMix) = - info "starting waku mix protocol" - #mix.nodePoolLoopHandle = mix.startMixNodePoolMgr() This can be re-enabled once discovery is addressed - -method stop*(mix: WakuMix) {.async.} = - if mix.nodePoolLoopHandle.isNil(): - return - await mix.nodePoolLoopHandle.cancelAndWait() - mix.nodePoolLoopHandle = nil +proc poolSize*(mix: WakuMix): int = + mix.nodePool.len # Mix Protocol diff --git a/waku/waku_node.nim b/waku/waku_node.nim index c81e49bb6..c8b13d4ea 100644 --- a/waku/waku_node.nim +++ b/waku/waku_node.nim @@ -1,8 +1,8 @@ import - ./node/net_config, + ./net/net_config, ./node/waku_switch as switch, ./node/waku_node as node, ./node/health_monitor as health_monitor, - ./node/api as api + ./node/kernel_api as kernel_api -export net_config, switch, node, health_monitor, api +export net_config, switch, node, health_monitor, kernel_api diff --git a/waku/waku_noise/noise_handshake_processing.nim b/waku/waku_noise/noise_handshake_processing.nim index 7688f0a80..8b84bf958 100644 --- a/waku/waku_noise/noise_handshake_processing.nim +++ b/waku/waku_noise/noise_handshake_processing.nim @@ -59,15 +59,14 @@ proc isValid(msg: seq[PreMessagePattern]): bool = var isValid: bool = true # Non-empty pre-messages can only have patterns "e", "s", "e,s" in each direction - let allowedPatterns: seq[PreMessagePattern] = - @[ - PreMessagePattern(direction: D_r, tokens: @[T_s]), - PreMessagePattern(direction: D_r, tokens: @[T_e]), - PreMessagePattern(direction: D_r, tokens: @[T_e, T_s]), - PreMessagePattern(direction: D_l, tokens: @[T_s]), - PreMessagePattern(direction: D_l, tokens: @[T_e]), - PreMessagePattern(direction: D_l, tokens: @[T_e, T_s]), - ] + let allowedPatterns: seq[PreMessagePattern] = @[ + PreMessagePattern(direction: D_r, tokens: @[T_s]), + PreMessagePattern(direction: D_r, tokens: @[T_e]), + PreMessagePattern(direction: D_r, tokens: @[T_e, T_s]), + PreMessagePattern(direction: D_l, tokens: @[T_s]), + PreMessagePattern(direction: D_l, tokens: @[T_e]), + PreMessagePattern(direction: D_l, tokens: @[T_e, T_s]), + ] # We check if pre message patterns are allowed for pattern in msg: diff --git a/waku/waku_noise/noise_types.nim b/waku/waku_noise/noise_types.nim index 3b88c43e8..543bd4329 100644 --- a/waku/waku_noise/noise_types.nim +++ b/waku/waku_noise/noise_types.nim @@ -223,57 +223,51 @@ const NoiseHandshakePatterns* = { "K1K1": HandshakePattern( name: "Noise_K1K1_25519_ChaChaPoly_SHA256", - preMessagePatterns: - @[ - PreMessagePattern(direction: D_r, tokens: @[T_s]), - PreMessagePattern(direction: D_l, tokens: @[T_s]), - ], - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_e]), - MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]), - MessagePattern(direction: D_r, tokens: @[T_se]), - ], + preMessagePatterns: @[ + PreMessagePattern(direction: D_r, tokens: @[T_s]), + PreMessagePattern(direction: D_l, tokens: @[T_s]), + ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_e]), + MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]), + MessagePattern(direction: D_r, tokens: @[T_se]), + ], ), "XK1": HandshakePattern( name: "Noise_XK1_25519_ChaChaPoly_SHA256", preMessagePatterns: @[PreMessagePattern(direction: D_l, tokens: @[T_s])], - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_e]), - MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]), - MessagePattern(direction: D_r, tokens: @[T_s, T_se]), - ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_e]), + MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]), + MessagePattern(direction: D_r, tokens: @[T_s, T_se]), + ], ), "XX": HandshakePattern( name: "Noise_XX_25519_ChaChaPoly_SHA256", preMessagePatterns: EmptyPreMessage, - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_e]), - MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]), - MessagePattern(direction: D_r, tokens: @[T_s, T_se]), - ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_e]), + MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]), + MessagePattern(direction: D_r, tokens: @[T_s, T_se]), + ], ), "XXpsk0": HandshakePattern( name: "Noise_XXpsk0_25519_ChaChaPoly_SHA256", preMessagePatterns: EmptyPreMessage, - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_psk, T_e]), - MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]), - MessagePattern(direction: D_r, tokens: @[T_s, T_se]), - ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_psk, T_e]), + MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]), + MessagePattern(direction: D_r, tokens: @[T_s, T_se]), + ], ), "WakuPairing": HandshakePattern( name: "Noise_WakuPairing_25519_ChaChaPoly_SHA256", preMessagePatterns: @[PreMessagePattern(direction: D_l, tokens: @[T_e])], - messagePatterns: - @[ - MessagePattern(direction: D_r, tokens: @[T_e, T_ee]), - MessagePattern(direction: D_l, tokens: @[T_s, T_es]), - MessagePattern(direction: D_r, tokens: @[T_s, T_se, T_ss]), - ], + messagePatterns: @[ + MessagePattern(direction: D_r, tokens: @[T_e, T_ee]), + MessagePattern(direction: D_l, tokens: @[T_s, T_es]), + MessagePattern(direction: D_r, tokens: @[T_s, T_se, T_ss]), + ], ), }.toTable() diff --git a/waku/waku_peer_exchange/protocol.nim b/waku/waku_peer_exchange/protocol.nim index de81d366e..b99f5eabf 100644 --- a/waku/waku_peer_exchange/protocol.nim +++ b/waku/waku_peer_exchange/protocol.nim @@ -22,7 +22,6 @@ export WakuPeerExchangeCodec declarePublicGauge waku_px_peers_received_unknown, "number of previously unknown ENRs received via peer exchange" -declarePublicGauge waku_px_peers_cached, "number of peer exchange peer ENRs cached" declarePublicCounter waku_px_errors, "number of peer exchange errors", ["type"] declarePublicCounter waku_px_peers_sent, "number of ENRs sent to peer exchange requesters" @@ -32,11 +31,9 @@ logScope: type WakuPeerExchange* = ref object of LPProtocol peerManager*: PeerManager - enrCache*: seq[enr.Record] cluster*: Option[uint16] # todo: next step: ring buffer; future: implement cache satisfying https://rfc.vac.dev/spec/34/ requestRateLimiter*: RequestRateLimiter - pxLoopHandle*: Future[void] proc respond( wpx: WakuPeerExchange, enrs: seq[enr.Record], conn: Connection @@ -79,61 +76,50 @@ proc respondError( return ok() -proc getEnrsFromCache( - wpx: WakuPeerExchange, numPeers: uint64 -): seq[enr.Record] {.gcsafe.} = - if wpx.enrCache.len() == 0: - info "peer exchange ENR cache is empty" - return @[] - - # copy and shuffle - randomize() - var shuffledCache = wpx.enrCache - shuffledCache.shuffle() - - # return numPeers or less if cache is smaller - return shuffledCache[0 ..< min(shuffledCache.len.int, numPeers.int)] - -proc poolFilter*(cluster: Option[uint16], peer: RemotePeerInfo): bool = - if peer.origin != Discv5: - trace "peer not from discv5", peer = $peer, origin = $peer.origin - return false +proc poolFilter*( + cluster: Option[uint16], origin: PeerOrigin, enr: enr.Record +): Result[void, string] = + if origin != Discv5: + trace "peer not from discv5", origin = $origin + return err("peer not from discv5: " & $origin) + if cluster.isSome() and enr.isClusterMismatched(cluster.get()): + trace "peer has mismatching cluster" + return err("peer has mismatching cluster") + return ok() +proc poolFilter*(cluster: Option[uint16], peer: RemotePeerInfo): Result[void, string] = if peer.enr.isNone(): info "peer has no ENR", peer = $peer - return false + return err("peer has no ENR: " & $peer) + return poolFilter(cluster, peer.origin, peer.enr.get()) - if cluster.isSome() and peer.enr.get().isClusterMismatched(cluster.get()): - info "peer has mismatching cluster", peer = $peer - return false - - return true - -proc populateEnrCache(wpx: WakuPeerExchange) = - # share only peers that i) are reachable ii) come from discv5 iii) share cluster - let withEnr = wpx.peerManager.switch.peerStore.getReachablePeers().filterIt( - poolFilter(wpx.cluster, it) - ) - - # either what we have or max cache size - var newEnrCache = newSeq[enr.Record](0) - for i in 0 ..< min(withEnr.len, MaxPeersCacheSize): - newEnrCache.add(withEnr[i].enr.get()) - - # swap cache for new - wpx.enrCache = newEnrCache - trace "ENR cache populated" - -proc updatePxEnrCache(wpx: WakuPeerExchange) {.async.} = - # try more aggressively to fill the cache at startup - var attempts = 50 - while wpx.enrCache.len < MaxPeersCacheSize and attempts > 0: - attempts -= 1 - wpx.populateEnrCache() - await sleepAsync(1.seconds) - - heartbeat "Updating px enr cache", CacheRefreshInterval: - wpx.populateEnrCache() +proc getEnrsFromStore( + wpx: WakuPeerExchange, numPeers: uint64 +): seq[enr.Record] {.gcsafe.} = + # Reservoir sampling (Algorithm R) + var i = 0 + let k = min(MaxPeersCacheSize, numPeers.int) + let enrStoreLen = wpx.peerManager.switch.peerStore[ENRBook].len + var enrs = newSeqOfCap[enr.Record](min(k, enrStoreLen)) + wpx.peerManager.switch.peerStore.forEnrPeers( + peerId, peerConnectedness, peerOrigin, peerEnrRecord + ): + if peerConnectedness == CannotConnect: + debug "Could not retrieve ENR because cannot connect to peer", + remotePeerId = peerId + continue + poolFilter(wpx.cluster, peerOrigin, peerEnrRecord).isOkOr: + debug "Could not get ENR because no peer matched pool", error = error + continue + if i < k: + enrs.add(peerEnrRecord) + else: + # Add some randomness + let j = rand(i) + if j < k: + enrs[j] = peerEnrRecord + inc(i) + return enrs proc initProtocolHandler(wpx: WakuPeerExchange) = proc handler(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} = @@ -157,15 +143,14 @@ proc initProtocolHandler(wpx: WakuPeerExchange) = error "Failed to respond with BAD_REQUEST:", error = $error return - let decBuf = PeerExchangeRpc.decode(buffer) - if decBuf.isErr(): + let decBuf = PeerExchangeRpc.decode(buffer).valueOr: waku_px_errors.inc(labelValues = [decodeRpcFailure]) - error "Failed to decode PeerExchange request", error = $decBuf.error + error "Failed to decode PeerExchange request", error = $error ( try: await wpx.respondError( - PeerExchangeResponseStatusCode.BAD_REQUEST, some($decBuf.error), conn + PeerExchangeResponseStatusCode.BAD_REQUEST, some($error), conn ) except CatchableError: error "could not send error response decode", @@ -175,7 +160,8 @@ proc initProtocolHandler(wpx: WakuPeerExchange) = error "Failed to respond with BAD_REQUEST:", error = $error return - let enrs = wpx.getEnrsFromCache(decBuf.get().request.numPeers) + let enrs = wpx.getEnrsFromStore(decBuf.request.numPeers) + info "peer exchange request received" trace "px enrs to respond", enrs = $enrs try: @@ -215,5 +201,4 @@ proc new*( ) wpx.initProtocolHandler() setServiceLimitMetric(WakuPeerExchangeCodec, rateLimitSetting) - asyncSpawn wpx.updatePxEnrCache() return wpx diff --git a/waku/waku_relay.nim b/waku/waku_relay.nim index 96328d984..a91033cf1 100644 --- a/waku/waku_relay.nim +++ b/waku/waku_relay.nim @@ -1,3 +1,4 @@ -import ./waku_relay/[protocol, topic_health] +import ./waku_relay/protocol +import waku/node/health_monitor/topic_health export protocol, topic_health diff --git a/waku/waku_relay/protocol.nim b/waku/waku_relay/protocol.nim index 9ebbc480f..d0b1ddb48 100644 --- a/waku/waku_relay/protocol.nim +++ b/waku/waku_relay/protocol.nim @@ -5,7 +5,7 @@ {.push raises: [].} import - std/[strformat, strutils], + std/[strformat, strutils, sets], stew/byteutils, results, sequtils, @@ -16,11 +16,18 @@ import libp2p/protocols/pubsub/gossipsub, libp2p/protocols/pubsub/rpc/messages, libp2p/stream/connection, - libp2p/switch -import - ../waku_core, ./message_id, ./topic_health, ../node/delivery_monitor/publish_observer + libp2p/switch, + brokers/broker_context -from ../waku_core/codecs import WakuRelayCodec +import + waku/waku_core, + waku/node/health_monitor/topic_health, + waku/requests/health_requests, + waku/events/health_events, + ./message_id, + waku/events/peer_events + +from waku/waku_core/codecs import WakuRelayCodec export WakuRelayCodec type ShardMetrics = object @@ -149,6 +156,8 @@ type pubsubTopic: PubsubTopic, message: WakuMessage ): Future[ValidationResult] {.gcsafe, raises: [Defect].} WakuRelay* = ref object of GossipSub + brokerCtx: BrokerContext + peerEventListener: WakuPeerEventListener # seq of tuples: the first entry in the tuple contains the validators are called for every topic # the second entry contains the error messages to be returned when the validator fails wakuValidators: seq[tuple[handler: WakuValidatorHandler, errorMessage: string]] @@ -157,10 +166,14 @@ type # map topic with its assigned validator within pubsub topicHandlers: Table[PubsubTopic, TopicHandler] # map topic with the TopicHandler proc in charge of attending topic's incoming message events - publishObservers: seq[PublishObserver] topicsHealth*: Table[string, TopicHealth] onTopicHealthChange*: TopicHealthChangeHandler topicHealthLoopHandle*: Future[void] + topicHealthUpdateEvent: AsyncEvent + topicHealthDirty: HashSet[string] + # list of topics that need their health updated in the update event + topicHealthCheckAll: bool + # true if all topics need to have their health status refreshed in the update event msgMetricsPerShard*: Table[string, ShardMetrics] # predefinition for more detailed results from publishing new message @@ -210,6 +223,7 @@ proc logMessageInfo*( msg_id = msg_id_short, from_peer_id = remotePeerId, topic = topic, + contentTopic = msg.contentTopic, receivedTime = getNowInNanosecondTime(), payloadSizeBytes = payloadSize else: @@ -219,6 +233,7 @@ proc logMessageInfo*( msg_id = msg_id_short, to_peer_id = remotePeerId, topic = topic, + contentTopic = msg.contentTopic, sentTime = getNowInNanosecondTime(), payloadSizeBytes = payloadSize @@ -283,6 +298,21 @@ proc initRelayObservers(w: WakuRelay) = ) proc onRecv(peer: PubSubPeer, msgs: var RPCMsg) = + if msgs.control.isSome(): + let ctrl = msgs.control.get() + var topicsChanged = false + + for graft in ctrl.graft: + w.topicHealthDirty.incl(graft.topicID) + topicsChanged = true + + for prune in ctrl.prune: + w.topicHealthDirty.incl(prune.topicID) + topicsChanged = true + + if topicsChanged: + w.topicHealthUpdateEvent.fire() + for msg in msgs.messages: let (msg_id_short, topic, wakuMessage, msgSize) = decodeRpcMessageInfo(peer, msg).valueOr: continue @@ -338,11 +368,25 @@ proc new*( maxMessageSize = maxMessageSize, parameters = GossipsubParameters, ) + w.brokerCtx = globalBrokerContext() procCall GossipSub(w).initPubSub() + w.topicsHealth = initTable[string, TopicHealth]() + w.topicHealthUpdateEvent = newAsyncEvent() + w.topicHealthDirty = initHashSet[string]() + w.topicHealthCheckAll = false w.initProtocolHandler() w.initRelayObservers() - w.topicsHealth = initTable[string, TopicHealth]() + + w.peerEventListener = WakuPeerEvent.listen( + w.brokerCtx, + proc(evt: WakuPeerEvent): Future[void] {.async: (raises: []), gcsafe.} = + if evt.kind == WakuPeerEventKind.EventDisconnected: + w.topicHealthCheckAll = true + w.topicHealthUpdateEvent.fire() + , + ).valueOr: + return err("Failed to subscribe to peer events: " & error) except InitializationError: return err("initialization error: " & getCurrentExceptionMsg()) @@ -353,12 +397,6 @@ proc addValidator*( ) {.gcsafe.} = w.wakuValidators.add((handler, errorMessage)) -proc addPublishObserver*(w: WakuRelay, obs: PublishObserver) = - ## Observer when the api client performed a publish operation. This - ## is initially aimed for bringing an additional layer of delivery reliability thanks - ## to store - w.publishObservers.add(obs) - proc addObserver*(w: WakuRelay, observer: PubSubObserver) {.gcsafe.} = ## Observes when a message is sent/received from the GossipSub PoV procCall GossipSub(w).addObserver(observer) @@ -399,8 +437,7 @@ proc getPeersInMesh*( ): Result[seq[PeerId], string] = ## Returns the list of peerIds in a mesh defined by the passed pubsub topic. ## The 'mesh' atribute is defined in the GossipSub ref object. - let pubSubPeers = w.getPubSubPeersInMesh(pubsubTopic).valueOr: - return err(error) + let pubSubPeers = ?w.getPubSubPeersInMesh(pubsubTopic) let peerIds = toSeq(pubSubPeers).mapIt(it.peerId) return ok(peerIds) @@ -427,56 +464,73 @@ proc calculateTopicHealth(wakuRelay: WakuRelay, topic: string): TopicHealth = return TopicHealth.MINIMALLY_HEALTHY return TopicHealth.SUFFICIENTLY_HEALTHY -proc updateTopicsHealth(wakuRelay: WakuRelay) {.async.} = - var futs = newSeq[Future[void]]() - for topic in toSeq(wakuRelay.topics.keys): - ## loop over all the topics I'm subscribed to - let - oldHealth = wakuRelay.topicsHealth.getOrDefault(topic) - currentHealth = wakuRelay.calculateTopicHealth(topic) - - if oldHealth == currentHealth: - continue - - wakuRelay.topicsHealth[topic] = currentHealth - if not wakuRelay.onTopicHealthChange.isNil(): - let fut = wakuRelay.onTopicHealthChange(topic, currentHealth) - if not fut.completed(): # Fast path for successful sync handlers - futs.add(fut) - - if futs.len() > 0: - # slow path - we have to wait for the handlers to complete - try: - futs = await allFinished(futs) - except CancelledError: - # check for errors in futures - for fut in futs: - if fut.failed: - let err = fut.readError() - warn "Error in health change handler", description = err.msg - -proc topicsHealthLoop(wakuRelay: WakuRelay) {.async.} = - while true: - await wakuRelay.updateTopicsHealth() - await sleepAsync(10.seconds) - -method start*(w: WakuRelay) {.async, base.} = - info "start" - await procCall GossipSub(w).start() - w.topicHealthLoopHandle = w.topicsHealthLoop() - -method stop*(w: WakuRelay) {.async, base.} = - info "stop" - await procCall GossipSub(w).stop() - if not w.topicHealthLoopHandle.isNil(): - await w.topicHealthLoopHandle.cancelAndWait() - proc isSubscribed*(w: WakuRelay, topic: PubsubTopic): bool = GossipSub(w).topics.hasKey(topic) proc subscribedTopics*(w: WakuRelay): seq[PubsubTopic] = return toSeq(GossipSub(w).topics.keys()) +proc topicsHealthLoop(w: WakuRelay) {.async.} = + while true: + await w.topicHealthUpdateEvent.wait() + w.topicHealthUpdateEvent.clear() + + var topicsToCheck: seq[string] + + if w.topicHealthCheckAll: + topicsToCheck = toSeq(w.topics.keys) + else: + topicsToCheck = toSeq(w.topicHealthDirty) + + w.topicHealthCheckAll = false + w.topicHealthDirty.clear() + + var futs = newSeq[Future[void]]() + + for topic in topicsToCheck: + # guard against topic being unsubscribed since fire() + if not w.isSubscribed(topic): + continue + + let + oldHealth = w.topicsHealth.getOrDefault(topic, TopicHealth.UNHEALTHY) + currentHealth = w.calculateTopicHealth(topic) + + if oldHealth == currentHealth: + continue + + w.topicsHealth[topic] = currentHealth + + EventShardTopicHealthChange.emit(w.brokerCtx, topic, currentHealth) + + if not w.onTopicHealthChange.isNil(): + futs.add(w.onTopicHealthChange(topic, currentHealth)) + + if futs.len() > 0: + try: + discard await allFinished(futs) + except CancelledError: + break + except CatchableError as e: + warn "Error in topic health callback", error = e.msg + + # safety cooldown to protect from edge cases + await sleepAsync(100.milliseconds) + +method start*(w: WakuRelay) {.async: (raises: [CancelledError]).} = + info "start" + await procCall GossipSub(w).start() + w.topicHealthLoopHandle = w.topicsHealthLoop() + +method stop*(w: WakuRelay) {.async: (raises: []).} = + info "stop" + await procCall GossipSub(w).stop() + + await WakuPeerEvent.dropListener(w.brokerCtx, w.peerEventListener) + + if not w.topicHealthLoopHandle.isNil(): + await w.topicHealthLoopHandle.cancelAndWait() + proc generateOrderedValidator(w: WakuRelay): ValidatorHandler {.gcsafe.} = # rejects messages that are not WakuMessage let wrappedValidator = proc( @@ -544,29 +598,27 @@ proc subscribe*(w: WakuRelay, pubsubTopic: PubsubTopic, handler: WakuRelayHandle let topicHandler = proc( pubsubTopic: string, data: seq[byte] ): Future[void] {.gcsafe, raises: [].} = - let decMsg = WakuMessage.decode(data) - if decMsg.isErr(): + let decMsg = WakuMessage.decode(data).valueOr: # fine if triggerSelf enabled, since validators are bypassed error "failed to decode WakuMessage, validator passed a wrong message", - pubsubTopic = pubsubTopic, error = decMsg.error + pubsubTopic = pubsubTopic, error = error let fut = newFuture[void]() fut.complete() return fut - else: - # this subscription handler is called once for every validated message - # that will be relayed, hence this is the place we can count net incoming traffic - waku_relay_network_bytes.inc( - data.len.int64 + pubsubTopic.len.int64, labelValues = [pubsubTopic, "net", "in"] - ) + # this subscription handler is called once for every validated message + # that will be relayed, hence this is the place we can count net incoming traffic + waku_relay_network_bytes.inc( + data.len.int64 + pubsubTopic.len.int64, labelValues = [pubsubTopic, "net", "in"] + ) - return handler(pubsubTopic, decMsg.get()) + return handler(pubsubTopic, decMsg) # Add the ordered validator to the topic # This assumes that if `w.validatorInserted.hasKey(pubSubTopic) is true`, it contains the ordered validator. # Otherwise this might lead to unintended behaviour. if not w.topicValidator.hasKey(pubSubTopic): let newValidator = w.generateOrderedValidator() - procCall GossipSub(w).addValidator(pubSubTopic, w.generateOrderedValidator()) + procCall GossipSub(w).addValidator(pubSubTopic, newValidator) w.topicValidator[pubSubTopic] = newValidator # set this topic parameters for scoring @@ -576,6 +628,8 @@ proc subscribe*(w: WakuRelay, pubsubTopic: PubsubTopic, handler: WakuRelayHandle procCall GossipSub(w).subscribe(pubsubTopic, topicHandler) w.topicHandlers[pubsubTopic] = topicHandler + w.topicHealthDirty.incl(pubsubTopic) + w.topicHealthUpdateEvent.fire() proc unsubscribeAll*(w: WakuRelay, pubsubTopic: PubsubTopic) = ## Unsubscribe all handlers on this pubsub topic @@ -585,6 +639,8 @@ proc unsubscribeAll*(w: WakuRelay, pubsubTopic: PubsubTopic) = procCall GossipSub(w).unsubscribeAll(pubsubTopic) w.topicValidator.del(pubsubTopic) w.topicHandlers.del(pubsubTopic) + w.topicsHealth.del(pubsubTopic) + w.topicHealthDirty.excl(pubsubTopic) proc unsubscribe*(w: WakuRelay, pubsubTopic: PubsubTopic) = if not w.topicValidator.hasKey(pubsubTopic): @@ -610,6 +666,8 @@ proc unsubscribe*(w: WakuRelay, pubsubTopic: PubsubTopic) = w.topicValidator.del(pubsubTopic) w.topicHandlers.del(pubsubTopic) + w.topicsHealth.del(pubsubTopic) + w.topicHealthDirty.excl(pubsubTopic) proc publish*( w: WakuRelay, pubsubTopic: PubsubTopic, wakuMessage: WakuMessage @@ -624,16 +682,14 @@ proc publish*( let data = message.encode().buffer let msgHash = computeMessageHash(pubsubTopic, message).to0xHex() - notice "start publish Waku message", msg_hash = msgHash, pubsubTopic = pubsubTopic + notice "start publish Waku message", + msg_hash = msgHash, pubsubTopic = pubsubTopic, contentTopic = message.contentTopic let relayedPeerCount = await procCall GossipSub(w).publish(pubsubTopic, data) if relayedPeerCount <= 0: return err(NoPeersToPublish) - for obs in w.publishObservers: - obs.onMessagePublished(pubSubTopic, message) - return ok(relayedPeerCount) proc getConnectedPubSubPeers*( @@ -670,8 +726,7 @@ proc getConnectedPeers*( ## Returns the list of peerIds of connected peers and subscribed to the passed pubsub topic. ## The 'gossipsub' atribute is defined in the GossipSub ref object. - let peers = w.getConnectedPubSubPeers(pubsubTopic).valueOr: - return err(error) + let peers = ?w.getConnectedPubSubPeers(pubsubTopic) let peerIds = toSeq(peers).mapIt(it.peerId) return ok(peerIds) diff --git a/waku/waku_rendezvous/client.nim b/waku/waku_rendezvous/client.nim new file mode 100644 index 000000000..09e789774 --- /dev/null +++ b/waku/waku_rendezvous/client.nim @@ -0,0 +1,142 @@ +{.push raises: [].} + +import + std/[options, sequtils, tables], + results, + chronos, + chronicles, + libp2p/protocols/rendezvous, + libp2p/crypto/curve25519, + libp2p/switch, + libp2p/utils/semaphore + +import metrics except collect + +import + waku/node/peer_manager, + waku/waku_core/peers, + waku/waku_core/codecs, + ./common, + ./waku_peer_record + +logScope: + topics = "waku rendezvous client" + +declarePublicCounter rendezvousPeerFoundTotal, + "total number of peers found via rendezvous" + +type WakuRendezVousClient* = ref object + switch: Switch + peerManager: PeerManager + clusterId: uint16 + requestInterval: timer.Duration + periodicRequestFut: Future[void] + # Internal rendezvous instance for making requests + rdv: GenericRendezVous[WakuPeerRecord] + +const MaxSimultanesousAdvertisements = 5 +const RendezVousLookupInterval = 10.seconds + +proc requestAll*( + self: WakuRendezVousClient +): Future[Result[void, string]] {.async: (raises: []).} = + trace "waku rendezvous client requests started" + + let namespace = computeMixNamespace(self.clusterId) + + # Get a random WakuRDV peer + let rpi = self.peerManager.selectPeer(WakuRendezVousCodec).valueOr: + return err("could not get a peer supporting WakuRendezVousCodec") + + var records: seq[WakuPeerRecord] + try: + # Use the libp2p rendezvous request method + records = await self.rdv.request( + Opt.some(namespace), Opt.some(PeersRequestedCount), Opt.some(@[rpi.peerId]) + ) + except CatchableError as e: + return err("rendezvous request failed: " & e.msg) + + trace "waku rendezvous client request got peers", count = records.len + for record in records: + if not self.switch.peerStore.peerExists(record.peerId): + rendezvousPeerFoundTotal.inc() + if record.mixKey.len == 0 or record.peerId == self.switch.peerInfo.peerId: + continue + trace "adding peer from rendezvous", + peerId = record.peerId, addresses = $record.addresses, mixKey = record.mixKey + let rInfo = RemotePeerInfo.init( + record.peerId, + record.addresses, + mixPubKey = some(intoCurve25519Key(fromHex(record.mixKey))), + ) + self.peerManager.addPeer(rInfo) + + trace "waku rendezvous client request finished" + + return ok() + +proc periodicRequests(self: WakuRendezVousClient) {.async.} = + info "waku rendezvous periodic requests started", interval = self.requestInterval + + # infinite loop + while true: + await sleepAsync(self.requestInterval) + + (await self.requestAll()).isOkOr: + error "waku rendezvous requests failed", error = error + + # Exponential backoff + +#[ TODO: Reevaluate for mix, maybe be aggresive in the start until a sizeable pool is built and then backoff + self.requestInterval += self.requestInterval + + if self.requestInterval >= 1.days: + break ]# + +proc new*( + T: type WakuRendezVousClient, + switch: Switch, + peerManager: PeerManager, + clusterId: uint16, +): Result[T, string] {.raises: [].} = + # Create a minimal GenericRendezVous instance for client-side requests + # We don't need the full server functionality, just the request method + let rng = newRng() + let rdv = GenericRendezVous[WakuPeerRecord]( + switch: switch, + rng: rng, + sema: newAsyncSemaphore(MaxSimultanesousAdvertisements), + minDuration: rendezvous.MinimumAcceptedDuration, + maxDuration: rendezvous.MaximumDuration, + minTTL: rendezvous.MinimumAcceptedDuration.seconds.uint64, + maxTTL: rendezvous.MaximumDuration.seconds.uint64, + peers: @[], # Will be populated from selectPeer calls + cookiesSaved: initTable[PeerId, Table[string, seq[byte]]](), + peerRecordValidator: checkWakuPeerRecord, + ) + + # Set codec separately as it's inherited from LPProtocol + rdv.codec = WakuRendezVousCodec + + let client = T( + switch: switch, + peerManager: peerManager, + clusterId: clusterId, + requestInterval: RendezVousLookupInterval, + rdv: rdv, + ) + + info "waku rendezvous client initialized", clusterId = clusterId + + return ok(client) + +proc start*(self: WakuRendezVousClient) {.async: (raises: []).} = + self.periodicRequestFut = self.periodicRequests() + info "waku rendezvous client started" + +proc stopWait*(self: WakuRendezVousClient) {.async: (raises: []).} = + if not self.periodicRequestFut.isNil(): + await self.periodicRequestFut.cancelAndWait() + + info "waku rendezvous client stopped" diff --git a/waku/waku_rendezvous/common.nim b/waku/waku_rendezvous/common.nim index 6125ac860..18c633efb 100644 --- a/waku/waku_rendezvous/common.nim +++ b/waku/waku_rendezvous/common.nim @@ -11,6 +11,14 @@ const DefaultRequestsInterval* = 1.minutes const MaxRegistrationInterval* = 5.minutes const PeersRequestedCount* = 12 +proc computeMixNamespace*(clusterId: uint16): string = + var namespace = "rs/" + + namespace &= $clusterId + namespace &= "/mix" + + return namespace + proc computeNamespace*(clusterId: uint16, shard: uint16): string = var namespace = "rs/" diff --git a/waku/waku_rendezvous/protocol.nim b/waku/waku_rendezvous/protocol.nim index 876082210..89433f533 100644 --- a/waku/waku_rendezvous/protocol.nim +++ b/waku/waku_rendezvous/protocol.nim @@ -1,62 +1,80 @@ {.push raises: [].} import - std/[sugar, options], + std/[sugar, options, sequtils, tables], results, chronos, chronicles, - metrics, + stew/byteutils, libp2p/protocols/rendezvous, + libp2p/protocols/rendezvous/protobuf, + libp2p/utils/semaphore, + libp2p/utils/offsettedseq, + libp2p/crypto/curve25519, libp2p/switch, libp2p/utility +import metrics except collect + import ../node/peer_manager, ../common/callbacks, ../waku_enr/capabilities, ../waku_core/peers, - ../waku_core/topics, - ../waku_core/topics/pubsub_topic, - ./common + ../waku_core/codecs, + ./common, + ./waku_peer_record logScope: topics = "waku rendezvous" -declarePublicCounter rendezvousPeerFoundTotal, - "total number of peers found via rendezvous" - -type WakuRendezVous* = ref object - rendezvous: Rendezvous +type WakuRendezVous* = ref object of GenericRendezVous[WakuPeerRecord] peerManager: PeerManager clusterId: uint16 getShards: GetShards getCapabilities: GetCapabilities + getPeerRecord: GetWakuPeerRecord registrationInterval: timer.Duration periodicRegistrationFut: Future[void] - requestInterval: timer.Duration - periodicRequestFut: Future[void] +const MaximumNamespaceLen = 255 -proc batchAdvertise*( +method discover*( + self: WakuRendezVous, conn: Connection, d: Discover +) {.async: (raises: [CancelledError, LPStreamError]).} = + # Override discover method to avoid collect macro generic instantiation issues + trace "Received Discover", peerId = conn.peerId, ns = d.ns + await procCall GenericRendezVous[WakuPeerRecord](self).discover(conn, d) + +proc advertise*( self: WakuRendezVous, namespace: string, - ttl: Duration = DefaultRegistrationTTL, peers: seq[PeerId], + ttl: timer.Duration = self.minDuration, ): Future[Result[void, string]] {.async: (raises: []).} = - ## Register with all rendezvous peers under a namespace + trace "advertising via waku rendezvous", + namespace = namespace, ttl = ttl, peers = $peers, peerRecord = $self.getPeerRecord() + let se = SignedPayload[WakuPeerRecord].init( + self.switch.peerInfo.privateKey, self.getPeerRecord() + ).valueOr: + return + err("rendezvous advertisement failed: Failed to sign Waku Peer Record: " & $error) + let sprBuff = se.encode().valueOr: + return err("rendezvous advertisement failed: Wrong Signed Peer Record: " & $error) # rendezvous.advertise expects already opened connections # must dial first + var futs = collect(newSeq): for peerId in peers: - self.peerManager.dialPeer(peerId, RendezVousCodec) + self.peerManager.dialPeer(peerId, self.codec) let dialCatch = catch: await allFinished(futs) if dialCatch.isErr(): - return err("batchAdvertise: " & dialCatch.error.msg) + return err("advertise: " & dialCatch.error.msg) futs = dialCatch.get() @@ -76,151 +94,34 @@ proc batchAdvertise*( conn - let advertCatch = catch: - await self.rendezvous.advertise(namespace, Opt.some(ttl)) - - for conn in conns: - await conn.close() - - if advertCatch.isErr(): - return err("batchAdvertise: " & advertCatch.error.msg) + if conns.len == 0: + return err("could not establish any connections to rendezvous peers") + try: + await self.advertise(namespace, ttl, peers, sprBuff) + except Exception as e: + return err("rendezvous advertisement failed: " & e.msg) + finally: + for conn in conns: + await conn.close() return ok() -proc batchRequest*( - self: WakuRendezVous, - namespace: string, - count: int = DiscoverLimit, - peers: seq[PeerId], -): Future[Result[seq[PeerRecord], string]] {.async: (raises: []).} = - ## Request all records from all rendezvous peers matching a namespace - - # rendezvous.request expects already opened connections - # must dial first - var futs = collect(newSeq): - for peerId in peers: - self.peerManager.dialPeer(peerId, RendezVousCodec) - - let dialCatch = catch: - await allFinished(futs) - - if dialCatch.isErr(): - return err("batchRequest: " & dialCatch.error.msg) - - futs = dialCatch.get() - - let conns = collect(newSeq): - for fut in futs: - let catchable = catch: - fut.read() - - if catchable.isErr(): - warn "a rendezvous dial failed", cause = catchable.error.msg - continue - - let connOpt = catchable.get() - - let conn = connOpt.valueOr: - continue - - conn - - let reqCatch = catch: - await self.rendezvous.request(Opt.some(namespace), Opt.some(count), Opt.some(peers)) - - for conn in conns: - await conn.close() - - if reqCatch.isErr(): - return err("batchRequest: " & reqCatch.error.msg) - - return ok(reqCatch.get()) - -proc advertiseAll( +proc advertiseAll*( self: WakuRendezVous ): Future[Result[void, string]] {.async: (raises: []).} = - info "waku rendezvous advertisements started" + trace "waku rendezvous advertisements started" - let shards = self.getShards() - - let futs = collect(newSeq): - for shardId in shards: - # Get a random RDV peer for that shard - - let pubsub = - toPubsubTopic(RelayShard(clusterId: self.clusterId, shardId: shardId)) - - let rpi = self.peerManager.selectPeer(RendezVousCodec, some(pubsub)).valueOr: - continue - - let namespace = computeNamespace(self.clusterId, shardId) - - # Advertise yourself on that peer - self.batchAdvertise(namespace, DefaultRegistrationTTL, @[rpi.peerId]) - - if futs.len < 1: + let rpi = self.peerManager.selectPeer(self.codec).valueOr: return err("could not get a peer supporting RendezVousCodec") - let catchable = catch: - await allFinished(futs) + let namespace = computeMixNamespace(self.clusterId) - if catchable.isErr(): - return err(catchable.error.msg) + # Advertise yourself on that peer + let res = await self.advertise(namespace, @[rpi.peerId]) - for fut in catchable.get(): - if fut.failed(): - warn "a rendezvous advertisement failed", cause = fut.error.msg + trace "waku rendezvous advertisements finished" - info "waku rendezvous advertisements finished" - - return ok() - -proc initialRequestAll*( - self: WakuRendezVous -): Future[Result[void, string]] {.async: (raises: []).} = - info "waku rendezvous initial requests started" - - let shards = self.getShards() - - let futs = collect(newSeq): - for shardId in shards: - let namespace = computeNamespace(self.clusterId, shardId) - # Get a random RDV peer for that shard - let rpi = self.peerManager.selectPeer( - RendezVousCodec, - some(toPubsubTopic(RelayShard(clusterId: self.clusterId, shardId: shardId))), - ).valueOr: - continue - - # Ask for peer records for that shard - self.batchRequest(namespace, PeersRequestedCount, @[rpi.peerId]) - - if futs.len < 1: - return err("could not get a peer supporting RendezVousCodec") - - let catchable = catch: - await allFinished(futs) - - if catchable.isErr(): - return err(catchable.error.msg) - - for fut in catchable.get(): - if fut.failed(): - warn "a rendezvous request failed", cause = fut.error.msg - elif fut.finished(): - let res = fut.value() - - let records = res.valueOr: - warn "a rendezvous request failed", cause = $res.error - continue - - for record in records: - rendezvousPeerFoundTotal.inc() - self.peerManager.addPeer(record) - - info "waku rendezvous initial request finished" - - return ok() + return res proc periodicRegistration(self: WakuRendezVous) {.async.} = info "waku rendezvous periodic registration started", @@ -241,22 +142,6 @@ proc periodicRegistration(self: WakuRendezVous) {.async.} = # Back to normal interval if no errors self.registrationInterval = DefaultRegistrationInterval -proc periodicRequests(self: WakuRendezVous) {.async.} = - info "waku rendezvous periodic requests started", interval = self.requestInterval - - # infinite loop - while true: - (await self.initialRequestAll()).isOkOr: - error "waku rendezvous requests failed", error = error - - await sleepAsync(self.requestInterval) - - # Exponential backoff - self.requestInterval += self.requestInterval - - if self.requestInterval >= 1.days: - break - proc new*( T: type WakuRendezVous, switch: Switch, @@ -264,48 +149,84 @@ proc new*( clusterId: uint16, getShards: GetShards, getCapabilities: GetCapabilities, + getPeerRecord: GetWakuPeerRecord, ): Result[T, string] {.raises: [].} = - let rvCatchable = catch: - RendezVous.new(switch = switch, minDuration = DefaultRegistrationTTL) + let rng = newRng() + let wrv = T( + rng: rng, + salt: string.fromBytes(generateBytes(rng[], 8)), + registered: initOffsettedSeq[RegisteredData](), + expiredDT: Moment.now() - 1.days, + sema: newAsyncSemaphore(SemaphoreDefaultSize), + minDuration: rendezvous.MinimumAcceptedDuration, + maxDuration: rendezvous.MaximumDuration, + minTTL: rendezvous.MinimumAcceptedDuration.seconds.uint64, + maxTTL: rendezvous.MaximumDuration.seconds.uint64, + peerRecordValidator: checkWakuPeerRecord, + ) - if rvCatchable.isErr(): - return err(rvCatchable.error.msg) - - let rv = rvCatchable.get() - - let mountCatchable = catch: - switch.mount(rv) - - if mountCatchable.isErr(): - return err(mountCatchable.error.msg) - - var wrv = WakuRendezVous() - wrv.rendezvous = rv wrv.peerManager = peerManager wrv.clusterId = clusterId wrv.getShards = getShards wrv.getCapabilities = getCapabilities wrv.registrationInterval = DefaultRegistrationInterval - wrv.requestInterval = DefaultRequestsInterval + wrv.getPeerRecord = getPeerRecord + wrv.switch = switch + wrv.codec = WakuRendezVousCodec + + proc handleStream( + conn: Connection, proto: string + ) {.async: (raises: [CancelledError]).} = + try: + let + buf = await conn.readLp(4096) + msg = Message.decode(buf).tryGet() + case msg.msgType + of MessageType.Register: + #TODO: override this to store peers registered with us in peerstore with their info as well. + await wrv.register(conn, msg.register.tryGet(), wrv.getPeerRecord()) + of MessageType.RegisterResponse: + trace "Got an unexpected Register Response", response = msg.registerResponse + of MessageType.Unregister: + wrv.unregister(conn, msg.unregister.tryGet()) + of MessageType.Discover: + await wrv.discover(conn, msg.discover.tryGet()) + of MessageType.DiscoverResponse: + trace "Got an unexpected Discover Response", response = msg.discoverResponse + except CancelledError as exc: + trace "cancelled rendezvous handler" + raise exc + except CatchableError as exc: + trace "exception in rendezvous handler", description = exc.msg + finally: + await conn.close() + + wrv.handler = handleStream info "waku rendezvous initialized", - clusterId = clusterId, shards = getShards(), capabilities = getCapabilities() + clusterId = clusterId, + shards = getShards(), + capabilities = getCapabilities(), + wakuPeerRecord = getPeerRecord() return ok(wrv) -proc start*(self: WakuRendezVous) {.async: (raises: []).} = +method start*(self: WakuRendezVous) {.async: (raises: [CancelledError]).} = + # Start the parent GenericRendezVous (starts the register deletion loop) + if self.started: + warn "waku rendezvous already started" + return + await procCall GenericRendezVous[WakuPeerRecord](self).start() # start registering forever self.periodicRegistrationFut = self.periodicRegistration() - self.periodicRequestFut = self.periodicRequests() - info "waku rendezvous discovery started" -proc stopWait*(self: WakuRendezVous) {.async: (raises: []).} = +method stop*(self: WakuRendezVous) {.async: (raises: []).} = if not self.periodicRegistrationFut.isNil(): await self.periodicRegistrationFut.cancelAndWait() - if not self.periodicRequestFut.isNil(): - await self.periodicRequestFut.cancelAndWait() + # Stop the parent GenericRendezVous (stops the register deletion loop) + await procCall GenericRendezVous[WakuPeerRecord](self).stop() info "waku rendezvous discovery stopped" diff --git a/waku/waku_rendezvous/waku_peer_record.nim b/waku/waku_rendezvous/waku_peer_record.nim new file mode 100644 index 000000000..d6e700eb5 --- /dev/null +++ b/waku/waku_rendezvous/waku_peer_record.nim @@ -0,0 +1,74 @@ +import std/times, sugar + +import + libp2p/[ + protocols/rendezvous, + signed_envelope, + multicodec, + multiaddress, + protobuf/minprotobuf, + peerid, + ] + +type WakuPeerRecord* = object + # Considering only mix as of now, but we can keep extending this to include all capabilities part of Waku ENR + peerId*: PeerId + seqNo*: uint64 + addresses*: seq[MultiAddress] + mixKey*: string + +proc payloadDomain*(T: typedesc[WakuPeerRecord]): string = + $multiCodec("libp2p-custom-peer-record") + +proc payloadType*(T: typedesc[WakuPeerRecord]): seq[byte] = + @[(byte) 0x30, (byte) 0x00, (byte) 0x00] + +proc init*( + T: typedesc[WakuPeerRecord], + peerId: PeerId, + seqNo = getTime().toUnix().uint64, + addresses: seq[MultiAddress], + mixKey: string, +): T = + WakuPeerRecord(peerId: peerId, seqNo: seqNo, addresses: addresses, mixKey: mixKey) + +proc decode*( + T: typedesc[WakuPeerRecord], buffer: seq[byte] +): Result[WakuPeerRecord, ProtoError] = + let pb = initProtoBuffer(buffer) + var record = WakuPeerRecord() + + ?pb.getRequiredField(1, record.peerId) + ?pb.getRequiredField(2, record.seqNo) + discard ?pb.getRepeatedField(3, record.addresses) + + if record.addresses.len == 0: + return err(ProtoError.RequiredFieldMissing) + + ?pb.getRequiredField(4, record.mixKey) + + return ok(record) + +proc encode*(record: WakuPeerRecord): seq[byte] = + var pb = initProtoBuffer() + + pb.write(1, record.peerId) + pb.write(2, record.seqNo) + + for address in record.addresses: + pb.write(3, address) + + pb.write(4, record.mixKey) + + pb.finish() + return pb.buffer + +proc checkWakuPeerRecord*( + _: WakuPeerRecord, spr: seq[byte], peerId: PeerId +): Result[void, string] {.gcsafe.} = + if spr.len == 0: + return err("Empty peer record") + let signedEnv = ?SignedPayload[WakuPeerRecord].decode(spr).mapErr(x => $x) + if signedEnv.data.peerId != peerId: + return err("Bad Peer ID") + return ok() diff --git a/waku/waku_rest.nim b/waku/waku_rest.nim new file mode 100644 index 000000000..8a1737335 --- /dev/null +++ b/waku/waku_rest.nim @@ -0,0 +1,3 @@ +import ./rest_api/message_cache, ./rest_api/endpoint, ./rest_api/json_rpc + +export message_cache, rest diff --git a/waku/waku_rln_relay/constants.nim b/waku/waku_rln_relay/constants.nim index 3e4757537..8532abaaa 100644 --- a/waku/waku_rln_relay/constants.nim +++ b/waku/waku_rln_relay/constants.nim @@ -25,8 +25,6 @@ const # the size of poseidon hash output as the number hex digits HashHexSize* = int(HashBitSize / 4) -const DefaultRlnTreePath* = "rln_tree.db" - const # pre-processed "rln/waku-rln-relay/v2.0.0" to array[32, byte] DefaultRlnIdentifier*: RlnIdentifier = [ diff --git a/waku/waku_rln_relay/conversion_utils.nim b/waku/waku_rln_relay/conversion_utils.nim index 4a168ebeb..fc130621b 100644 --- a/waku/waku_rln_relay/conversion_utils.nim +++ b/waku/waku_rln_relay/conversion_utils.nim @@ -75,48 +75,6 @@ proc serialize*( ) return output -proc serialize*(witness: RLNWitnessInput): seq[byte] = - ## Serializes the RLN witness into a byte array following zerokit's expected format. - ## The serialized format includes: - ## - identity_secret (32 bytes, little-endian with zero padding) - ## - user_message_limit (32 bytes, little-endian with zero padding) - ## - message_id (32 bytes, little-endian with zero padding) - ## - merkle tree depth (8 bytes, little-endian) = path_elements.len / 32 - ## - path_elements (each 32 bytes, ordered bottom-to-top) - ## - merkle tree depth again (8 bytes, little-endian) - ## - identity_path_index (sequence of bits as bytes, 0 = left, 1 = right) - ## - x (32 bytes, little-endian with zero padding) - ## - external_nullifier (32 bytes, little-endian with zero padding) - var buffer: seq[byte] - buffer.add(@(witness.identity_secret)) - buffer.add(@(witness.user_message_limit)) - buffer.add(@(witness.message_id)) - buffer.add(toBytes(uint64(witness.path_elements.len / 32), Endianness.littleEndian)) - for element in witness.path_elements: - buffer.add(element) - buffer.add(toBytes(uint64(witness.path_elements.len / 32), Endianness.littleEndian)) - buffer.add(witness.identity_path_index) - buffer.add(@(witness.x)) - buffer.add(@(witness.external_nullifier)) - return buffer - -proc serialize*(proof: RateLimitProof, data: openArray[byte]): seq[byte] = - ## a private proc to convert RateLimitProof and data to a byte seq - ## this conversion is used in the proof verification proc - ## [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal ] - let lenPrefMsg = encodeLengthPrefix(@data) - var proofBytes = concat( - @(proof.proof), - @(proof.merkleRoot), - @(proof.externalNullifier), - @(proof.shareX), - @(proof.shareY), - @(proof.nullifier), - lenPrefMsg, - ) - - return proofBytes - # Serializes a sequence of MerkleNodes proc serialize*(roots: seq[MerkleNode]): seq[byte] = var rootsBytes: seq[byte] = @[] diff --git a/waku/waku_rln_relay/group_manager/group_manager_base.nim b/waku/waku_rln_relay/group_manager/group_manager_base.nim index de2962e42..9c088d4c5 100644 --- a/waku/waku_rln_relay/group_manager/group_manager_base.nim +++ b/waku/waku_rln_relay/group_manager/group_manager_base.nim @@ -144,6 +144,4 @@ method generateProof*( return err("generateProof is not implemented") method isReady*(g: GroupManager): Future[bool] {.base, async.} = - raise newException( - CatchableError, "isReady proc for " & $g.type & " is not implemented yet" - ) + return true diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 13c1f700d..2af2c3971 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -11,7 +11,7 @@ import stint, json, std/[strutils, tables, algorithm, strformat], - stew/[byteutils, arrayops], + stew/byteutils, sequtils import @@ -50,24 +50,23 @@ type proc fetchMerkleProofElements*( g: OnchainGroupManager ): Future[Result[seq[byte], string]] {.async.} = - try: - let membershipIndex = g.membershipIndex.get() - let index40 = stuint(membershipIndex, 40) + let membershipIndex = g.membershipIndex.get() + let index40 = stuint(membershipIndex, 40) - let methodSig = "getMerkleProof(uint40)" - var paddedParam = newSeq[byte](32) - let indexBytes = index40.toBytesBE() - for i in 0 ..< min(indexBytes.len, paddedParam.len): - paddedParam[paddedParam.len - indexBytes.len + i] = indexBytes[i] + let methodSig = "getMerkleProof(uint40)" + var paddedParam = newSeq[byte](32) + let indexBytes = index40.toBytesBE() + for i in 0 ..< min(indexBytes.len, paddedParam.len): + paddedParam[paddedParam.len - indexBytes.len + i] = indexBytes[i] - let response = await sendEthCallWithParams( - ethRpc = g.ethRpc.get(), - functionSignature = methodSig, - params = paddedParam, - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) + let response = await sendEthCallWithParams( + ethRpc = g.ethRpc.get(), + functionSignature = methodSig, + params = paddedParam, + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) return response except CatchableError: @@ -111,25 +110,21 @@ proc fetchMerkleRoot*( proc fetchNextFreeIndex*( g: OnchainGroupManager ): Future[Result[UInt256, string]] {.async.} = - try: - let nextFreeIndex = await sendEthCallWithoutParams( - ethRpc = g.ethRpc.get(), - functionSignature = "nextFreeIndex()", - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - return nextFreeIndex - except CatchableError: - error "Failed to fetch next free index", error = getCurrentExceptionMsg() - return err("Failed to fetch next free index: " & getCurrentExceptionMsg()) + let nextFreeIndex = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "nextFreeIndex()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) + return nextFreeIndex proc fetchMembershipStatus*( g: OnchainGroupManager, idCommitment: IDCommitment ): Future[Result[bool, string]] {.async.} = - try: - let params = idCommitment.reversed() - let resultBytes = await sendEthCallWithParams( + let params = idCommitment.reversed() + let responseBytes = ( + await sendEthCallWithParams( ethRpc = g.ethRpc.get(), functionSignature = "isInMembershipSet(uint256)", params = params, @@ -137,32 +132,25 @@ proc fetchMembershipStatus*( toAddress = fromHex(Address, g.ethContractAddress), chainId = g.chainId, ) - if resultBytes.isErr(): - return err("Failed to check membership: " & resultBytes.error) - let responseBytes = resultBytes.get() + ).valueOr: + return err("Failed to check membership: " & error) - return ok(responseBytes.len == 32 and responseBytes[^1] == 1'u8) - except CatchableError: - error "Failed to fetch membership set membership", error = getCurrentExceptionMsg() - return err("Failed to fetch membership set membership: " & getCurrentExceptionMsg()) + return ok(responseBytes.len == 32 and responseBytes[^1] == 1'u8) proc fetchMaxMembershipRateLimit*( g: OnchainGroupManager ): Future[Result[UInt256, string]] {.async.} = - try: - let maxMembershipRateLimit = await sendEthCallWithoutParams( - ethRpc = g.ethRpc.get(), - functionSignature = "maxMembershipRateLimit()", - fromAddress = g.ethRpc.get().defaultAccount, - toAddress = fromHex(Address, g.ethContractAddress), - chainId = g.chainId, - ) - return maxMembershipRateLimit - except CatchableError: - error "Failed to fetch max membership rate limit", error = getCurrentExceptionMsg() - return err("Failed to fetch max membership rate limit: " & getCurrentExceptionMsg()) + let maxMembershipRateLimit = await sendEthCallWithoutParams( + ethRpc = g.ethRpc.get(), + functionSignature = "maxMembershipRateLimit()", + fromAddress = g.ethRpc.get().defaultAccount, + toAddress = fromHex(Address, g.ethContractAddress), + chainId = g.chainId, + ) -template initializedGuard(g: OnchainGroupManager): untyped = + return maxMembershipRateLimit + +proc checkInitialized(g: OnchainGroupManager): Result[void, string] = if not g.initialized: raise newException(CatchableError, "OnchainGroupManager is not initialized") @@ -287,37 +275,32 @@ proc trackRootChanges*(g: OnchainGroupManager) {.async: (raises: [CatchableError initializedGuard(g) const rpcDelay = 5.seconds - while true: - await sleepAsync(rpcDelay) - let rootUpdated = await g.updateRoots() + while true: + await sleepAsync(rpcDelay) + let rootUpdated = await g.updateRoots() - if rootUpdated: - ## The membership set on-chain has changed (some new members have joined or some members have left) - if g.membershipIndex.isSome(): - ## A membership index exists only if the node has registered with RLN. - ## Non-registered nodes cannot have Merkle proof elements. - let proofResult = await g.fetchMerkleProofElements() - if proofResult.isErr(): - error "Failed to fetch Merkle proof", error = proofResult.error - else: - g.merkleProofCache = proofResult.get() + if rootUpdated: + ## The membership set on-chain has changed (some new members have joined or some members have left) + if g.membershipIndex.isSome(): + ## A membership index exists only if the node has registered with RLN. + ## Non-registered nodes cannot have Merkle proof elements. + let proofResult = await g.fetchMerkleProofElements() + if proofResult.isErr(): + error "Failed to fetch Merkle proof", error = proofResult.error + else: + g.merkleProofCache = proofResult.get() - let nextFreeIndex = await g.fetchNextFreeIndex() - if nextFreeIndex.isErr(): - error "Failed to fetch next free index", error = nextFreeIndex.error - raise newException( - CatchableError, "Failed to fetch next free index: " & nextFreeIndex.error - ) + let nextFreeIndex = (await g.fetchNextFreeIndex()).valueOr: + error "Failed to fetch next free index", error = error + return err("Failed to fetch next free index: " & error) - let memberCount = cast[int64](nextFreeIndex.get()) - waku_rln_number_registered_memberships.set(float64(memberCount)) - except CatchableError: - error "Fatal error in trackRootChanges", error = getCurrentExceptionMsg() + let memberCount = cast[int64](nextFreeIndex) + waku_rln_number_registered_memberships.set(float64(memberCount)) method register*( g: OnchainGroupManager, rateCommitment: RateCommitment -): Future[void] {.async: (raises: [Exception]).} = - initializedGuard(g) +): Future[Result[void, string]] {.async.} = + ?checkInitialized(g) try: let leaf = rateCommitment.toLeaf().get() @@ -326,65 +309,99 @@ method register*( info "registering member via callback", rateCommitment = leaf, index = idx await g.registerCb.get()(@[Membership(rateCommitment: leaf, index: idx)]) g.latestIndex.inc() - except CatchableError: - raise newException(ValueError, getCurrentExceptionMsg()) + except Exception as e: + return err("Failed to call register callback: " & e.msg) + + return ok() method register*( g: OnchainGroupManager, identityCredential: IdentityCredential, userMessageLimit: UserMessageLimit, -): Future[void] {.async: (raises: [Exception]).} = - initializedGuard(g) +): Future[Result[void, string]] {.async.} = + ?checkInitialized(g) let ethRpc = g.ethRpc.get() let wakuRlnContract = g.wakuRlnContract.get() - var gasPrice: int - g.retryWrapper(gasPrice, "Failed to get gas price"): - int(await ethRpc.provider.eth_gasPrice()) * 2 + let gasPrice = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to get gas price", + proc(): Future[int] {.async.} = + let fetchedGasPrice = uint64(await ethRpc.provider.eth_gasPrice()) + if fetchedGasPrice > uint64(high(int) div 2): + warn "Gas price overflow detected, capping at maximum int value", + fetchedGasPrice = fetchedGasPrice, maxInt = high(int) + return high(int) + else: + let calculatedGasPrice = int(fetchedGasPrice) * 2 + debug "Gas price calculated", + fetchedGasPrice = fetchedGasPrice, gasPrice = calculatedGasPrice + return calculatedGasPrice, + ) + ).valueOr: + return err("Failed to get gas price: " & error) + let idCommitmentHex = identityCredential.idCommitment.inHex() - info "identityCredential idCommitmentHex", idCommitment = idCommitmentHex + debug "identityCredential idCommitmentHex", idCommitment = idCommitmentHex let idCommitment = identityCredential.idCommitment.toUInt256() let idCommitmentsToErase: seq[UInt256] = @[] info "registering the member", idCommitment = idCommitment, userMessageLimit = userMessageLimit, idCommitmentsToErase = idCommitmentsToErase - var txHash: TxHash - g.retryWrapper(txHash, "Failed to register the member"): - await wakuRlnContract - .register(idCommitment, userMessageLimit.stuint(32), idCommitmentsToErase) - .send(gasPrice = gasPrice) + let txHash = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to register the member", + proc(): Future[TxHash] {.async.} = + return await wakuRlnContract + .register(idCommitment, userMessageLimit.stuint(32), idCommitmentsToErase) + .send(gasPrice = gasPrice), + ) + ).valueOr: + return err("Failed to register member: " & error) # wait for the transaction to be mined - var tsReceipt: ReceiptObject - g.retryWrapper(tsReceipt, "Failed to get the transaction receipt"): - await ethRpc.getMinedTransactionReceipt(txHash) - info "registration transaction mined", txHash = txHash + let tsReceipt = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to get the transaction receipt", + proc(): Future[ReceiptObject] {.async.} = + return await ethRpc.getMinedTransactionReceipt(txHash), + ) + ).valueOr: + return err("Failed to get transaction receipt: " & error) + debug "registration transaction mined", txHash = txHash g.registrationTxHash = some(txHash) # the receipt topic holds the hash of signature of the raised events - # TODO: make this robust. search within the event list for the event - info "ts receipt", receipt = tsReceipt[] + debug "ts receipt", receipt = tsReceipt[] if tsReceipt.status.isNone(): - raise newException(ValueError, "Transaction failed: status is None") + return err("Transaction failed: status is None") if tsReceipt.status.get() != 1.Quantity: - raise newException( - ValueError, "Transaction failed with status: " & $tsReceipt.status.get() - ) + return err("Transaction failed with status: " & $tsReceipt.status.get()) - ## Extract MembershipRegistered event from transaction logs (third event) - let thirdTopic = tsReceipt.logs[2].topics[0] - info "third topic", thirdTopic = thirdTopic - if thirdTopic != - cast[FixedBytes[32]](keccak.keccak256.digest( - "MembershipRegistered(uint256,uint256,uint32)" - ).data): - raise newException(ValueError, "register: unexpected event signature") + ## Search through all transaction logs to find the MembershipRegistered event + let expectedEventSignature = cast[FixedBytes[32]](keccak.keccak256.digest( + "MembershipRegistered(uint256,uint256,uint32)" + ).data) - ## Parse MembershipRegistered event data: rateCommitment(256) || membershipRateLimit(256) || index(32) - let arguments = tsReceipt.logs[2].data - info "tx log data", arguments = arguments + var membershipRegisteredLog: Option[LogObject] + for log in tsReceipt.logs: + if log.topics.len > 0 and log.topics[0] == expectedEventSignature: + membershipRegisteredLog = some(log) + break + + if membershipRegisteredLog.isNone(): + return err("register: MembershipRegistered event not found in transaction logs") + + let registrationLog = membershipRegisteredLog.get() + + ## Parse MembershipRegistered event data: idCommitment(256) || membershipRateLimit(256) || index(32) + let arguments = registrationLog.data + trace "registration transaction log data", arguments = arguments let ## Extract membership index from transaction log data (big endian) membershipIndex = UInt256.fromBytesBE(arguments[64 .. 95]) @@ -402,20 +419,28 @@ method register*( if g.registerCb.isSome(): let member = Membership(rateCommitment: rateCommitment, index: g.latestIndex) - await g.registerCb.get()(@[member]) + try: + await g.registerCb.get()(@[member]) + except Exception as e: + return err("Failed to call register callback: " & e.msg) g.latestIndex.inc() - return + return ok() method withdraw*( g: OnchainGroupManager, idCommitment: IDCommitment -): Future[void] {.async: (raises: [Exception]).} = - initializedGuard(g) # TODO: after slashing is enabled on the contract +): Future[Result[void, string]] {.async.} = + checkInitialized(g).isOkOr: + return err(error) + return ok() method withdrawBatch*( g: OnchainGroupManager, idCommitments: seq[IDCommitment] -): Future[void] {.async: (raises: [Exception]).} = - initializedGuard(g) +): Future[Result[void, string]] {.async.} = + checkInitialized(g).isOkOr: + return err(error) + + return ok() proc getRootFromProofAndIndex( g: OnchainGroupManager, elements: seq[byte], bits: seq[byte] @@ -424,23 +449,20 @@ proc getRootFromProofAndIndex( # it's currently not used anywhere, but can be used to verify the root from the proof and index # Compute leaf hash from idCommitment and messageLimit let messageLimitField = uint64ToField(g.userMessageLimit.get()) - let leafHashRes = poseidon(@[g.idCredentials.get().idCommitment, @messageLimitField]) - if leafHashRes.isErr(): - return err("Failed to compute leaf hash: " & leafHashRes.error) + var hash = poseidon(g.idCredentials.get().idCommitment, @messageLimitField).valueOr: + return err("Failed to compute leaf hash: " & error) - var hash = leafHashRes.get() for i in 0 ..< bits.len: let sibling = elements[i * 32 .. (i + 1) * 32 - 1] let hashRes = if bits[i] == 0: - poseidon(@[@hash, sibling]) + poseidon(@hash, sibling) else: - poseidon(@[sibling, @hash]) + poseidon(sibling, @hash) hash = hashRes.valueOr: return err("Failed to compute poseidon hash: " & error) - hash = hashRes.get() return ok(hash) @@ -450,7 +472,7 @@ method generateProof*( epoch: Epoch, messageId: MessageId, rlnIdentifier = DefaultRlnIdentifier, -): GroupManagerResult[RateLimitProof] {.gcsafe, raises: [].} = +): GroupManagerResult[RateLimitProof] {.gcsafe.} = ## Generates an RLN proof using the cached Merkle proof and custom witness # Ensure identity credentials and membership index are set if g.idCredentials.isNone(): @@ -473,9 +495,14 @@ method generateProof*( let chunk = g.merkleProofCache[i * 32 .. (i + 1) * 32 - 1] path_elements.add(chunk.reversed()) - let x = keccak.keccak256.digest(data) + let xCfr = hashToFieldLe(data).valueOr: + return err("Failed to hash signal to field: " & error) + defer: + ffi_cfr_free(xCfr) + let x = cfrToBytesLe(xCfr).valueOr: + return err("Failed to serialize signal hash: " & error) - let extNullifier = poseidon(@[@(epoch), @(rlnIdentifier)]).valueOr: + let extNullifier = generateExternalNullifier(epoch, rlnIdentifier).valueOr: return err("Failed to compute external nullifier: " & error) let witness = RLNWitnessInput( @@ -488,57 +515,8 @@ method generateProof*( external_nullifier: extNullifier, ) - let serializedWitness = serialize(witness) - - var input_witness_buffer = toBuffer(serializedWitness) - - # Generate the proof using the zerokit API - var output_witness_buffer: Buffer - let witness_success = generate_proof_with_witness( - g.rlnInstance, addr input_witness_buffer, addr output_witness_buffer - ) - - if not witness_success: - return err("Failed to generate proof") - - # Parse the proof into a RateLimitProof object - var proofValue = cast[ptr array[320, byte]](output_witness_buffer.`ptr`) - let proofBytes: array[320, byte] = proofValue[] - - ## Parse the proof as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ] - let - proofOffset = 128 - rootOffset = proofOffset + 32 - externalNullifierOffset = rootOffset + 32 - shareXOffset = externalNullifierOffset + 32 - shareYOffset = shareXOffset + 32 - nullifierOffset = shareYOffset + 32 - - var - zkproof: ZKSNARK - proofRoot, shareX, shareY: MerkleNode - externalNullifier: ExternalNullifier - nullifier: Nullifier - - discard zkproof.copyFrom(proofBytes[0 .. proofOffset - 1]) - discard proofRoot.copyFrom(proofBytes[proofOffset .. rootOffset - 1]) - discard - externalNullifier.copyFrom(proofBytes[rootOffset .. externalNullifierOffset - 1]) - discard shareX.copyFrom(proofBytes[externalNullifierOffset .. shareXOffset - 1]) - discard shareY.copyFrom(proofBytes[shareXOffset .. shareYOffset - 1]) - discard nullifier.copyFrom(proofBytes[shareYOffset .. nullifierOffset - 1]) - - # Create the RateLimitProof object - let output = RateLimitProof( - proof: zkproof, - merkleRoot: proofRoot, - externalNullifier: externalNullifier, - epoch: epoch, - rlnIdentifier: rlnIdentifier, - shareX: shareX, - shareY: shareY, - nullifier: nullifier, - ) + let output = generateRlnProofWithWitness(g.rlnInstance, witness, epoch, rlnIdentifier).valueOr: + return err("Failed to generate proof: " & error) info "Proof generated successfully", proof = output @@ -548,36 +526,13 @@ method generateProof*( method verifyProof*( g: OnchainGroupManager, input: seq[byte], proof: RateLimitProof -): GroupManagerResult[bool] {.gcsafe, raises: [].} = - ## -- Verifies an RLN rate-limit proof against the set of valid Merkle roots -- - - var normalizedProof = proof - - normalizedProof.externalNullifier = poseidon( - @[@(proof.epoch), @(proof.rlnIdentifier)] +): GroupManagerResult[bool] {.gcsafe.} = + let validProof = verifyRlnProof( + g.rlnInstance, proof, input, g.validRoots.items().toSeq() ).valueOr: - return err("Failed to compute external nullifier: " & error) - - let proofBytes = serialize(normalizedProof, input) - let proofBuffer = proofBytes.toBuffer() - - let rootsBytes = serialize(g.validRoots.items().toSeq()) - let rootsBuffer = rootsBytes.toBuffer() - - var validProof: bool # out-param - let ffiOk = verify_with_roots( - g.rlnInstance, # RLN context created at init() - addr proofBuffer, # (proof + signal) - addr rootsBuffer, # valid Merkle roots - addr validProof # will be set by the FFI call - , - ) - - if not ffiOk: - return err("could not verify the proof") - else: - info "Proof verified successfully" + return err("could not verify the proof: " & error) + info "Proof verified", isValid = validProof return ok(validProof) method onRegister*(g: OnchainGroupManager, cb: OnRegisterCallback) {.gcsafe.} = @@ -589,25 +544,31 @@ method onWithdraw*(g: OnchainGroupManager, cb: OnWithdrawCallback) {.gcsafe.} = proc establishConnection( g: OnchainGroupManager ): Future[GroupManagerResult[Web3]] {.async.} = - var ethRpc: Web3 + let ethRpc = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to connect to the Ethereum client", + proc(): Future[Web3] {.async.} = + var innerEthRpc: Web3 + var connected = false + for clientUrl in g.ethClientUrls: + ## We give a chance to the user to provide multiple clients + ## and we try to connect to each of them + try: + innerEthRpc = await newWeb3(clientUrl) + connected = true + break + except CatchableError: + error "failed connect Eth client", error = getCurrentExceptionMsg() - g.retryWrapper(ethRpc, "Failed to connect to the Ethereum client"): - var innerEthRpc: Web3 - var connected = false - for clientUrl in g.ethClientUrls: - ## We give a chance to the user to provide multiple clients - ## and we try to connect to each of them - try: - innerEthRpc = await newWeb3(clientUrl) - connected = true - break - except CatchableError: - error "failed connect Eth client", error = getCurrentExceptionMsg() + ## this exception is handled by the retrywrapper + if not connected: + raise newException(CatchableError, "all failed") - if not connected: - raise newException(CatchableError, "all failed") - - innerEthRpc + return innerEthRpc, + ) + ).valueOr: + return err("Failed to establish Ethereum connection: " & error) return ok(ethRpc) @@ -616,9 +577,15 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} let ethRpc: Web3 = (await establishConnection(g)).valueOr: return err("failed to connect to Ethereum clients: " & $error) - var fetchedChainId: UInt256 - g.retryWrapper(fetchedChainId, "Failed to get the chain id"): - await ethRpc.provider.eth_chainId() + let fetchedChainId = ( + await retryWrapper( + RetryStrategy.new(), + "Failed to get the chain id", + proc(): Future[UInt256] {.async.} = + return await ethRpc.provider.eth_chainId(), + ) + ).valueOr: + return err("Failed to get chain id: " & error) # Set the chain id if g.chainId == 0: @@ -692,8 +659,10 @@ method init*(g: OnchainGroupManager): Future[GroupManagerResult[void]] {.async.} proc onDisconnect() {.async.} = error "Ethereum client disconnected" - var newEthRpc: Web3 = (await g.establishConnection()).valueOr: - g.onFatalErrorAction("failed to connect to Ethereum clients onDisconnect") + let newEthRpc: Web3 = (await g.establishConnection()).valueOr: + error "Fatal: failed to reconnect to Ethereum clients after disconnect", + error = error + g.onFatalErrorAction("failed to reconnect to Ethereum clients: " & error) return newEthRpc.ondisconnect = ethRpc.ondisconnect @@ -710,15 +679,21 @@ method stop*(g: OnchainGroupManager): Future[void] {.async, gcsafe.} = g.ethRpc.get().ondisconnect = nil await g.ethRpc.get().close() + if not g.rlnInstance.isNil: + ffi_rln_free(g.rlnInstance) + g.rlnInstance = nil + g.initialized = false method isReady*(g: OnchainGroupManager): Future[bool] {.async.} = - initializedGuard(g) + checkInitialized(g).isOkOr: + return false if g.ethRpc.isNone(): + error "Ethereum RPC client is not configured" return false if g.wakuRlnContract.isNone(): + error "Waku RLN contract is not configured" return false - return true diff --git a/waku/waku_rln_relay/group_manager/on_chain/retry_wrapper.nim b/waku/waku_rln_relay/group_manager/on_chain/retry_wrapper.nim index df8716279..97bc0c435 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/retry_wrapper.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/retry_wrapper.nim @@ -1,36 +1,31 @@ -import ../../../common/error_handling import chronos import results +const + DefaultRetryDelay* = 4000.millis + DefaultRetryCount* = 15'u + type RetryStrategy* = object - shouldRetry*: bool retryDelay*: Duration retryCount*: uint proc new*(T: type RetryStrategy): RetryStrategy = - return RetryStrategy(shouldRetry: true, retryDelay: 4000.millis, retryCount: 15) + return RetryStrategy(retryDelay: DefaultRetryDelay, retryCount: DefaultRetryCount) -template retryWrapper*( - res: auto, - retryStrategy: RetryStrategy, - errStr: string, - errCallback: OnFatalErrorHandler, - body: untyped, -): auto = - if errCallback == nil: - raise newException(CatchableError, "Ensure that the errCallback is set") +proc retryWrapper*[T]( + retryStrategy: RetryStrategy, errStr: string, body: proc(): Future[T] {.async.} +): Future[Result[T, string]] {.async.} = var retryCount = retryStrategy.retryCount - var shouldRetry = retryStrategy.shouldRetry - var exceptionMessage = "" + var lastError = "" - while shouldRetry and retryCount > 0: + while retryCount > 0: try: - res = body - shouldRetry = false - except: + let value = await body() + return ok(value) + except CatchableError as e: retryCount -= 1 - exceptionMessage = getCurrentExceptionMsg() - await sleepAsync(retryStrategy.retryDelay) - if shouldRetry: - errCallback(errStr & ": " & exceptionMessage) - return + lastError = e.msg + if retryCount > 0: + await sleepAsync(retryStrategy.retryDelay) + + return err(errStr & ": " & lastError) diff --git a/waku/waku_rln_relay/protocol_metrics.nim b/waku/waku_rln_relay/protocol_metrics.nim index 1551f022e..2cea329fe 100644 --- a/waku/waku_rln_relay/protocol_metrics.nim +++ b/waku/waku_rln_relay/protocol_metrics.nim @@ -56,7 +56,7 @@ declarePublicGauge( ) declarePublicGauge( waku_rln_membership_insertion_duration_seconds, - "time taken to insert a new member into the local merkle tree", + "time taken to process a new membership registration", ) declarePublicGauge( waku_rln_membership_credentials_import_duration_seconds, diff --git a/waku/waku_rln_relay/rln/rln_interface.nim b/waku/waku_rln_relay/rln/rln_interface.nim index 0bb0ef6b0..612d1a2cc 100644 --- a/waku/waku_rln_relay/rln/rln_interface.nim +++ b/waku/waku_rln_relay/rln/rln_interface.nim @@ -1,168 +1,378 @@ -## Nim wrappers for the functions defined in librln +## Nim wrappers for librln (zerokit v2.0.2, safer-ffi typed handles). +## +## Built against the `stateless` zerokit feature: tree-mutation FFI is not +## bound here because logos-delivery does not maintain a local Merkle tree +## (post-PR #3312); the WakuRlnV2 contract is the source of truth and the +## per-index Merkle path is fetched via getMerkleProof(index). +## +## Memory model: every CResult.err must be checked with `hasError` and +## consumed via `consumeError`. Every CFr / Vec_CFr / Vec_uint8 returned by +## the FFI owns memory the caller must release with the corresponding +## ffi_*_free. Use `defer:` immediately after acquisition. +## +## Wire format (v2.0.2 single-message-id): +## RLNProof: [ 0x00 | proof<128> | RLNProofValues(0x00) ] +## RLNProofValues: [ 0x00 | root<32> | external_nullifier<32> | +## x<32> | y<32> | nullifier<32> ] +## Total RLNProof byte size: 1 + 128 + 1 + 5*32 = 290 bytes. + +import results import ../protocol_types -{.push raises: [].} +{.push raises: [], gcsafe.} -## Buffer struct is taken from -# https://github.com/celo-org/celo-threshold-bls-rs/blob/master/crates/threshold-bls-ffi/src/ffi.rs -type Buffer* = object - `ptr`*: ptr uint8 - len*: uint +# --- Types ------------------------------------------------------------------ -proc toBuffer*(x: openArray[byte]): Buffer = - ## converts the input to a Buffer object - ## the Buffer object is used to communicate data with the rln lib - var temp = @x - let baseAddr = cast[pointer](x) - let output = Buffer(`ptr`: cast[ptr uint8](baseAddr), len: uint(temp.len)) - return output +type + CSize = csize_t -###################################################################### -## RLN Zerokit module APIs -###################################################################### + CFr* = object ## opaque ark_bn254::Fr handle + FFI_RLNProof* = object + FFI_RLNPartialProof* = object + FFI_RLNWitnessInput* = object + FFI_RLNPartialWitnessInput* = object + FFI_RLNProofValues* = object -#-------------------------------- zkSNARKs operations ----------------------------------------- -proc key_gen*( - output_buffer: ptr Buffer, is_little_endian: bool -): bool {.importc: "extended_key_gen".} + Vec_CFr* = object + dataPtr*: ptr CFr + len*: CSize + cap*: CSize -## generates identity trapdoor, identity nullifier, identity secret hash and id commitment tuple serialized inside output_buffer as | identity_trapdoor<32> | identity_nullifier<32> | identity_secret_hash<32> | id_commitment<32> | -## identity secret hash is the poseidon hash of [identity_trapdoor, identity_nullifier] -## id commitment is the poseidon hash of the identity secret hash -## the return bool value indicates the success or failure of the operation + Vec_uint8* = object + dataPtr*: ptr uint8 + len*: CSize + cap*: CSize -proc seeded_key_gen*( - input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool -): bool {.importc: "seeded_extended_key_gen".} + # CResult variants — safer-ffi lowers Result to a struct of + # (ok: T-or-null, err: Vec_uint8-or-null). Exactly one is populated. + CBoolResult* = object + ok*: bool + err*: Vec_uint8 -## generates identity trapdoor, identity nullifier, identity secret hash and id commitment tuple serialized inside output_buffer as | identity_trapdoor<32> | identity_nullifier<32> | identity_secret_hash<32> | id_commitment<32> | using ChaCha20 -## seeded with an arbitrary long seed serialized in input_buffer -## The input seed provided by the user is hashed using Keccak256 before being passed to ChaCha20 as seed. -## identity secret hash is the poseidon hash of [identity_trapdoor, identity_nullifier] -## id commitment is the poseidon hash of the identity secret hash -# use_little_endian: if true, uses big or little endian for serialization (default: true) -## the return bool value indicates the success or failure of the operation + CResultRLNPtrVecU8* = object + ok*: ptr RLN + err*: Vec_uint8 -proc generate_proof*( - ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer -): bool {.importc: "generate_rln_proof".} + CResultCFrPtrVecU8* = object + ok*: ptr CFr + err*: Vec_uint8 -## rln-v2 -## input_buffer has to be serialized as [ identity_secret<32> | identity_index<8> | user_message_limit<32> | message_id<32> | external_nullifier<32> | signal_len<8> | signal ] -## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ] -## rln-v1 -## input_buffer has to be serialized as [ id_key<32> | id_index<8> | epoch<32> | signal_len<8> | signal ] -## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ] -## integers wrapped in <> indicate value sizes in bytes -## the return bool value indicates the success or failure of the operation + CResultProofPtrVecU8* = object + ok*: ptr FFI_RLNProof + err*: Vec_uint8 -proc generate_proof_with_witness*( - ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer -): bool {.importc: "generate_rln_proof_with_witness".} + CResultPartialProofPtrVecU8* = object + ok*: ptr FFI_RLNPartialProof + err*: Vec_uint8 -## rln-v2 -## "witness" term refer to collection of secret inputs with proper serialization -## input_buffer has to be serialized as [ identity_secret<32> | user_message_limit<32> | message_id<32> | path_elements> | identity_path_index> | x<32> | external_nullifier<32> ] -## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> ] -## rln-v1 -## input_buffer has to be serialized as [ id_key<32> | path_elements> | identity_path_index> | x<32> | epoch<32> | rln_identifier<32> ] -## output_buffer holds the proof data and should be parsed as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> ] -## integers wrapped in <> indicate value sizes in bytes -## path_elements and identity_path_index serialize a merkle proof and are vectors of elements of 32 and 1 bytes respectively -## the return bool value indicates the success or failure of the operation + CResultWitnessInputPtrVecU8* = object + ok*: ptr FFI_RLNWitnessInput + err*: Vec_uint8 -proc verify*( - ctx: ptr RLN, proof_buffer: ptr Buffer, proof_is_valid_ptr: ptr bool -): bool {.importc: "verify_rln_proof".} + CResultPartialWitnessInputPtrVecU8* = object + ok*: ptr FFI_RLNPartialWitnessInput + err*: Vec_uint8 -## rln-v2 -## proof_buffer has to be serialized as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> | signal_len<8> | signal ] -## rln-v1 -## ## proof_buffer has to be serialized as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal ] -## the return bool value indicates the success or failure of the call to the verify function -## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure + CResultVecCFrVecU8* = object + ok*: Vec_CFr + err*: Vec_uint8 -proc verify_with_roots*( - ctx: ptr RLN, - proof_buffer: ptr Buffer, - roots_buffer: ptr Buffer, - proof_is_valid_ptr: ptr bool, -): bool {.importc: "verify_with_roots".} + CResultVecU8VecU8* = object + ok*: Vec_uint8 + err*: Vec_uint8 -## rln-v2 -## proof_buffer has to be serialized as [ proof<128> | root<32> | external_nullifier<32> | share_x<32> | share_y<32> | nullifier<32> | signal_len<8> | signal ] -## rln-v1 -## proof_buffer has to be serialized as [ proof<128> | root<32> | epoch<32> | share_x<32> | share_y<32> | nullifier<32> | rln_identifier<32> | signal_len<8> | signal ] -## roots_buffer contains the concatenation of 32 bytes long serializations in little endian of root values -## the return bool value indicates the success or failure of the call to the verify function -## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure +const + FieldElementSize* = 32 + ZksnarkProofSize* = 128 + ## Single-message-id serialized RLNProof size: outer version + proof + ## + inner RLNProofValues (inner version + 5 field elements). + RlnProofWireSize* = 1 + ZksnarkProofSize + 1 + 5 * FieldElementSize -proc zk_prove*( - ctx: ptr RLN, input_buffer: ptr Buffer, output_buffer: ptr Buffer -): bool {.importc: "prove".} +# FFI declarations — source of truth: vendor/zerokit/rln/src/ffi/{ffi_rln,ffi_utils}.rs -## Computes the zkSNARK proof and stores it in output_buffer for input values stored in input_buffer -## rln-v2 -## input_buffer is serialized as input_data as [ identity_secret<32> | user_message_limit<32> | message_id<32> | path_elements> | identity_path_index> | x<32> | external_nullifier<32> ] -## rln-v1 -## input_buffer is serialized as input_data as [ id_key<32> | path_elements> | identity_path_index> | x<32> | epoch<32> | rln_identifier<32> ] -## output_buffer holds the proof data and should be parsed as [ proof<128> ] -## path_elements and indentity_path elements serialize a merkle proof for id_key and are vectors of elements of 32 and 1 bytes, respectively (not. Vec<>). -## x is the x coordinate of the Shamir's secret share for which the proof is computed -## epoch is the input epoch (equivalently, the nullifier) -## the return bool value indicates the success or failure of the operation +# --- RLN instance lifecycle (stateless variants) -------------------------- -proc zk_verify*( - ctx: ptr RLN, proof_buffer: ptr Buffer, proof_is_valid_ptr: ptr bool -): bool {.importc: "verify".} +proc ffi_rln_new*(): CResultRLNPtrVecU8 {.importc: "ffi_rln_new", cdecl.} -## Verifies the zkSNARK proof passed in proof_buffer -## input_buffer is serialized as input_data as [ proof<128> ] -## the verification of the zk proof is available in proof_is_valid_ptr, where a value of true indicates success and false a failure -## the return bool value indicates the success or failure of the operation +proc ffi_rln_new_with_params*( + zkey_data: ptr Vec_uint8, graph_data: ptr Vec_uint8 +): CResultRLNPtrVecU8 {.importc: "ffi_rln_new_with_params", cdecl.} -#-------------------------------- Common procedures ------------------------------------------- -# stateful version -proc new_circuit*( - tree_depth: uint, input_buffer: ptr Buffer, ctx: ptr (ptr RLN) -): bool {.importc: "new".} +proc ffi_rln_free*(rln: ptr RLN) {.importc: "ffi_rln_free", cdecl.} -## creates an instance of rln object as defined by the zerokit RLN lib -## input_buffer contains a serialization of the path where the circuit resources can be found (.r1cs, .wasm, .zkey and optionally the verification_key.json) -## ctx holds the final created rln object -## the return bool value indicates the success or failure of the operation +# --- Keygen --------------------------------------------------------------- -# stateless version -proc new_circuit*(ctx: ptr (ptr RLN)): bool {.importc: "new".} +proc ffi_extended_key_gen*(): Vec_CFr {.importc: "ffi_extended_key_gen", cdecl.} -proc new_circuit_from_data*( - zkey_buffer: ptr Buffer, graph_buffer: ptr Buffer, ctx: ptr (ptr RLN) -): bool {.importc: "new_with_params".} +proc ffi_seeded_extended_key_gen*( + seed: ptr Vec_uint8 +): Vec_CFr {.importc: "ffi_seeded_extended_key_gen", cdecl.} -## creates an instance of rln object as defined by the zerokit RLN lib by passing the required inputs as byte arrays -## zkey_buffer contains the bytes read from the .zkey proving key -## graph_buffer contains the bytes read from the graph data file -## ctx holds the final created rln object -## the return bool value indicates the success or failure of the operation +# --- Witness construction ------------------------------------------------- -#-------------------------------- Hashing utils ------------------------------------------- +proc ffi_rln_witness_input_new*( + identity_secret: ptr CFr, + user_message_limit: ptr CFr, + message_id: ptr CFr, + path_elements: ptr Vec_CFr, + identity_path_index: ptr Vec_uint8, + x: ptr CFr, + external_nullifier: ptr CFr, +): CResultWitnessInputPtrVecU8 {.importc: "ffi_rln_witness_input_new", cdecl.} -proc sha256*( - input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool -): bool {.importc: "hash".} +proc ffi_rln_witness_input_free*( + witness: ptr FFI_RLNWitnessInput +) {.importc: "ffi_rln_witness_input_free", cdecl.} -## it hashes (sha256) the plain text supplied in inputs_buffer and then maps it to a field element -## this proc is used to map arbitrary signals to field element for the sake of proof generation -## inputs_buffer holds the hash input as a byte seq -## the hash output is generated and populated inside output_buffer -## the output_buffer contains 32 bytes hash output +proc ffi_rln_partial_witness_input_new*( + identity_secret: ptr CFr, + user_message_limit: ptr CFr, + path_elements: ptr Vec_CFr, + identity_path_index: ptr Vec_uint8, +): CResultPartialWitnessInputPtrVecU8 {. + importc: "ffi_rln_partial_witness_input_new", cdecl +.} -proc poseidon*( - input_buffer: ptr Buffer, output_buffer: ptr Buffer, is_little_endian: bool -): bool {.importc: "poseidon_hash".} +proc ffi_rln_partial_witness_input_free*( + witness: ptr FFI_RLNPartialWitnessInput +) {.importc: "ffi_rln_partial_witness_input_free", cdecl.} -## it hashes (poseidon) the plain text supplied in inputs_buffer -## this proc is used to compute the identity secret hash, and external nullifier -## inputs_buffer holds the hash input as a byte seq -## the hash output is generated and populated inside output_buffer -## the output_buffer contains 32 bytes hash output +# --- Proof generation ----------------------------------------------------- +# safer-ffi's repr_c::Box lands on the Nim side as `ptr ptr T`. Call sites +# pass `addr handle` where `handle` is `ptr T`. + +proc ffi_generate_rln_proof*( + rln: ptr ptr RLN, witness: ptr ptr FFI_RLNWitnessInput +): CResultProofPtrVecU8 {.importc: "ffi_generate_rln_proof", cdecl.} + +proc ffi_generate_partial_zk_proof*( + rln: ptr ptr RLN, partial_witness: ptr ptr FFI_RLNPartialWitnessInput +): CResultPartialProofPtrVecU8 {.importc: "ffi_generate_partial_zk_proof", cdecl.} + +proc ffi_finish_rln_proof*( + rln: ptr ptr RLN, + partial_proof: ptr ptr FFI_RLNPartialProof, + witness: ptr ptr FFI_RLNWitnessInput, +): CResultProofPtrVecU8 {.importc: "ffi_finish_rln_proof", cdecl.} + +# --- Verification --------------------------------------------------------- + +proc ffi_verify_with_roots*( + rln: ptr ptr RLN, proof: ptr ptr FFI_RLNProof, roots: ptr Vec_CFr, x: ptr CFr +): CBoolResult {.importc: "ffi_verify_with_roots", cdecl.} + +# --- Proof serialization -------------------------------------------------- + +proc ffi_rln_proof_to_bytes_le*( + proof: ptr ptr FFI_RLNProof +): CResultVecU8VecU8 {.importc: "ffi_rln_proof_to_bytes_le", cdecl.} + +proc ffi_bytes_le_to_rln_proof*( + bytes: ptr Vec_uint8 +): CResultProofPtrVecU8 {.importc: "ffi_bytes_le_to_rln_proof", cdecl.} + +# v2.0.2: construct an RLNProof directly from its field elements (single +# message-id variant), avoiding the manual 290-byte wire layout. +proc ffi_rln_proof_new*( + groth16Bytes: ptr Vec_uint8, + root: ptr CFr, + externalNullifier: ptr CFr, + x: ptr CFr, + y: ptr CFr, + nullifier: ptr CFr, +): CResultProofPtrVecU8 {.importc: "ffi_rln_proof_new", cdecl.} + +proc ffi_rln_proof_free*(p: ptr FFI_RLNProof) {.importc: "ffi_rln_proof_free", cdecl.} + +proc ffi_rln_partial_proof_to_bytes_le*( + partial_proof: ptr ptr FFI_RLNPartialProof +): CResultVecU8VecU8 {.importc: "ffi_rln_partial_proof_to_bytes_le", cdecl.} + +proc ffi_bytes_le_to_rln_partial_proof*( + bytes: ptr Vec_uint8 +): CResultPartialProofPtrVecU8 {.importc: "ffi_bytes_le_to_rln_partial_proof", cdecl.} + +proc ffi_rln_partial_proof_free*( + p: ptr FFI_RLNPartialProof +) {.importc: "ffi_rln_partial_proof_free", cdecl.} + +# --- Proof values (extract root / x / y / nullifier from a proof) --------- + +proc ffi_rln_proof_get_values*( + proof: ptr ptr FFI_RLNProof +): ptr FFI_RLNProofValues {.importc: "ffi_rln_proof_get_values", cdecl.} + +proc ffi_rln_proof_values_get_root*( + pv: ptr ptr FFI_RLNProofValues +): ptr CFr {.importc: "ffi_rln_proof_values_get_root", cdecl.} + +proc ffi_rln_proof_values_get_x*( + pv: ptr ptr FFI_RLNProofValues +): ptr CFr {.importc: "ffi_rln_proof_values_get_x", cdecl.} + +proc ffi_rln_proof_values_get_external_nullifier*( + pv: ptr ptr FFI_RLNProofValues +): ptr CFr {.importc: "ffi_rln_proof_values_get_external_nullifier", cdecl.} + +proc ffi_rln_proof_values_get_y*( + pv: ptr ptr FFI_RLNProofValues +): CResultCFrPtrVecU8 {.importc: "ffi_rln_proof_values_get_y", cdecl.} + +proc ffi_rln_proof_values_get_nullifier*( + pv: ptr ptr FFI_RLNProofValues +): CResultCFrPtrVecU8 {.importc: "ffi_rln_proof_values_get_nullifier", cdecl.} + +proc ffi_rln_proof_values_free*( + pv: ptr FFI_RLNProofValues +) {.importc: "ffi_rln_proof_values_free", cdecl.} + +# --- Slashing ------------------------------------------------------------- + +proc ffi_compute_id_secret*( + share1_x: ptr CFr, share1_y: ptr CFr, share2_x: ptr CFr, share2_y: ptr CFr +): CResultCFrPtrVecU8 {.importc: "ffi_compute_id_secret", cdecl.} + +# --- Primitives: CFr ------------------------------------------------------ + +proc ffi_cfr_zero*(): ptr CFr {.importc: "ffi_cfr_zero", cdecl.} + +proc ffi_cfr_to_bytes_le*( + cfr: ptr CFr +): Vec_uint8 {.importc: "ffi_cfr_to_bytes_le", cdecl.} + +proc ffi_bytes_le_to_cfr*( + bytes: ptr Vec_uint8 +): CResultCFrPtrVecU8 {.importc: "ffi_bytes_le_to_cfr", cdecl.} + +proc ffi_cfr_free*(cfr: ptr CFr) {.importc: "ffi_cfr_free", cdecl.} + +# --- Primitives: Vec_CFr -------------------------------------------------- + +proc ffi_vec_cfr_new*(capacity: CSize): Vec_CFr {.importc: "ffi_vec_cfr_new", cdecl.} + +proc ffi_vec_cfr_push*( + v: ptr Vec_CFr, cfr: ptr CFr +) {.importc: "ffi_vec_cfr_push", cdecl.} + +proc ffi_vec_cfr_len*(v: ptr Vec_CFr): CSize {.importc: "ffi_vec_cfr_len", cdecl.} + +proc ffi_vec_cfr_get*( + v: ptr Vec_CFr, i: CSize +): ptr CFr {.importc: "ffi_vec_cfr_get", cdecl.} + +proc ffi_vec_cfr_free*(v: Vec_CFr) {.importc: "ffi_vec_cfr_free", cdecl.} + +# --- Primitives: Vec_uint8 ------------------------------------------------ + +proc ffi_vec_u8_free*(v: Vec_uint8) {.importc: "ffi_vec_u8_free", cdecl.} + +proc ffi_c_string_free*(s: Vec_uint8) {.importc: "ffi_c_string_free", cdecl.} + +# --- Hash helpers --------------------------------------------------------- + +proc ffi_hash_to_field_le*( + input: ptr Vec_uint8 +): ptr CFr {.importc: "ffi_hash_to_field_le", cdecl.} + +proc ffi_poseidon_hash_pair*( + a: ptr CFr, b: ptr CFr +): ptr CFr {.importc: "ffi_poseidon_hash_pair", cdecl.} + +# --- Memory-hygiene helpers ------------------------------------------------- + +proc hasError*(data: Vec_uint8): bool = + not data.dataPtr.isNil + +proc asString*(data: Vec_uint8): string = + if data.dataPtr.isNil or data.len == 0: + return "" + result = newString(int(data.len)) + copyMem(addr result[0], data.dataPtr, int(data.len)) + +proc consumeError*(prefix: string, data: Vec_uint8): string = + ## Read an error string out of a Rust-owned Vec_uint8 AND free it. + let msg = asString(data) + if hasError(data): + ffi_c_string_free(data) + if prefix.len == 0: + msg + elif msg.len == 0: + prefix + else: + prefix & msg + +proc toVecUint8*(data: openArray[byte]): Vec_uint8 = + ## Wrap Nim-owned bytes as a Vec_uint8 view. NOTE: the resulting Vec_uint8 + ## must NOT be passed to ffi_vec_u8_free — Nim retains ownership. + if data.len == 0: + return Vec_uint8(dataPtr: nil, len: 0, cap: 0) + Vec_uint8( + dataPtr: cast[ptr uint8](unsafeAddr data[0]), + len: CSize(data.len), + cap: CSize(data.len), + ) + +proc vecToSeq*(data: Vec_uint8): seq[byte] = + result = newSeq[byte](int(data.len)) + if result.len > 0: + copyMem(addr result[0], data.dataPtr, result.len) + +proc seqToFixed32*(data: openArray[byte]): RlnRelayResult[array[32, byte]] = + if data.len != FieldElementSize: + return err("Expected 32 bytes, got " & $data.len) + var output: array[32, byte] + copyMem(addr output[0], unsafeAddr data[0], FieldElementSize) + ok(output) + +proc cfrToBytesLe*(cfr: ptr CFr): RlnRelayResult[array[32, byte]] = + let bytes = ffi_cfr_to_bytes_le(cfr) + defer: + ffi_vec_u8_free(bytes) + if int(bytes.len) != FieldElementSize: + return err("Invalid field byte length: " & $bytes.len) + seqToFixed32(vecToSeq(bytes)) + +proc bytesToCfrLe*(data: openArray[byte]): RlnRelayResult[ptr CFr] = + ## Allocate a ptr CFr from raw bytes. Caller MUST ffi_cfr_free(x). + var vec = toVecUint8(data) + let res = ffi_bytes_le_to_cfr(addr vec) + if not res.ok.isNil: + return ok(res.ok) + err(consumeError("Failed to convert bytes to field: ", res.err)) + +proc cfrResultToBytes*( + res: CResultCFrPtrVecU8, prefix: string +): RlnRelayResult[array[32, byte]] = + ## Consume a CResultCFrPtrVecU8: read bytes if ok, free the CFr, or + ## propagate the error (also freeing the error string). + if res.ok.isNil: + return err(consumeError(prefix, res.err)) + defer: + ffi_cfr_free(res.ok) + cfrToBytesLe(res.ok) + +proc hashToFieldLe*(data: openArray[byte]): RlnRelayResult[ptr CFr] = + ## Caller MUST ffi_cfr_free the returned ptr. + var vec = toVecUint8(data) + let cfr = ffi_hash_to_field_le(addr vec) + if cfr.isNil: + return err("Failed to hash to field") + ok(cfr) + +proc poseidonPairLe*(a, b: openArray[byte]): RlnRelayResult[array[32, byte]] = + ## Poseidon hash of exactly two 32-byte field elements (little-endian). + ## zerokit v2 FFI only exposes pair-input Poseidon; unary is not supported. + let aPtr = bytesToCfrLe(a).valueOr: + return err(error) + defer: + ffi_cfr_free(aPtr) + let bPtr = bytesToCfrLe(b).valueOr: + return err(error) + defer: + ffi_cfr_free(bPtr) + let cfr = ffi_poseidon_hash_pair(aPtr, bPtr) + if cfr.isNil: + return err("Poseidon hash failed") + defer: + ffi_cfr_free(cfr) + cfrToBytesLe(cfr) diff --git a/waku/waku_rln_relay/rln/wrappers.nim b/waku/waku_rln_relay/rln/wrappers.nim index d1dec2b38..4fc8c1542 100644 --- a/waku/waku_rln_relay/rln/wrappers.nim +++ b/waku/waku_rln_relay/rln/wrappers.nim @@ -1,158 +1,149 @@ -import std/json -import - chronicles, - options, - eth/keys, - stew/[arrayops, byteutils, endians2], - stint, - results, - std/[sequtils, strutils, tables] +import chronicles, eth/keys, stew/[arrayops, endians2], stint, results import ./rln_interface, ../conversion_utils, ../protocol_types, ../protocol_metrics import ../../waku_core, ../../waku_keystore +{.push raises: [], gcsafe.} + logScope: topics = "waku rln_relay ffi" -proc membershipKeyGen*(): RlnRelayResult[IdentityCredential] = - ## generates a IdentityCredential that can be used for the registration into the rln membership contract - ## Returns an error if the key generation fails +# Forward decl; body defined below. +proc generateExternalNullifier*( + epoch: Epoch, rlnIdentifier: RlnIdentifier +): RlnRelayResult[ExternalNullifier] - # keysBufferPtr will hold the generated identity tuple i.e., trapdoor, nullifier, secret hash and commitment - var - keysBuffer: Buffer - keysBufferPtr = addr(keysBuffer) - done = key_gen(keysBufferPtr, true) +proc toRootVec(validRoots: seq[MerkleNode]): RlnRelayResult[Vec_CFr] = + ## Caller MUST ffi_vec_cfr_free the returned Vec_CFr. + var roots = ffi_vec_cfr_new(csize_t(validRoots.len)) + for root in validRoots: + let cfr = bytesToCfrLe(root).valueOr: + ffi_vec_cfr_free(roots) + return err("failed call to bytesToCfrLe in toRootVec: " & error) + ffi_vec_cfr_push(addr roots, cfr) + ffi_cfr_free(cfr) + ok(roots) - # check whether the keys are generated successfully - if (done == false): - return err("error in key generation") +proc proofPtrToRateLimitProof( + proofPtr: ptr FFI_RLNProof, epoch: Epoch, rlnIdentifier: RlnIdentifier +): RlnRelayResult[RateLimitProof] = + var proofHandle = proofPtr + let proofBytesRes = ffi_rln_proof_to_bytes_le(addr proofHandle) + if hasError(proofBytesRes.err): + return err(consumeError("Failed to serialize proof: ", proofBytesRes.err)) + defer: + ffi_vec_u8_free(proofBytesRes.ok) - if (keysBuffer.len != 4 * 32): - return err("keysBuffer is of invalid length") + let serialized = vecToSeq(proofBytesRes.ok) + if serialized.len < RlnProofWireSize: + return err("Serialized proof too short: " & $serialized.len) - var generatedKeys = cast[ptr array[4 * 32, byte]](keysBufferPtr.`ptr`)[] - # the public and secret keys together are 64 bytes + let proofValues = ffi_rln_proof_get_values(addr proofHandle) + if proofValues.isNil(): + return err("Failed to extract proof values") + defer: + ffi_rln_proof_values_free(proofValues) - # TODO define a separate proc to decode the generated keys to the secret and public components - var - idTrapdoor: array[32, byte] - idNullifier: array[32, byte] - idSecretHash: array[32, byte] - idCommitment: array[32, byte] - for (i, x) in idTrapdoor.mpairs: - x = generatedKeys[i + 0 * 32] - for (i, x) in idNullifier.mpairs: - x = generatedKeys[i + 1 * 32] - for (i, x) in idSecretHash.mpairs: - x = generatedKeys[i + 2 * 32] - for (i, x) in idCommitment.mpairs: - x = generatedKeys[i + 3 * 32] + var output: RateLimitProof + output.epoch = epoch + output.rlnIdentifier = rlnIdentifier - var identityCredential = IdentityCredential( - idTrapdoor: @idTrapdoor, - idNullifier: @idNullifier, - idSecretHash: @idSecretHash, - idCommitment: @idCommitment, + # zkSNARK bytes: skip the leading version byte, take 128. + copyMem(addr output.proof[0], unsafeAddr serialized[1], ZksnarkProofSize) + + var pvHandle = proofValues + + let rootPtr = ffi_rln_proof_values_get_root(addr pvHandle) + if rootPtr.isNil(): + return err("Failed to read proof root") + defer: + ffi_cfr_free(rootPtr) + output.merkleRoot = cfrToBytesLe(rootPtr).valueOr: + return + err("failed call to cfrToBytesLe (root) in proofPtrToRateLimitProof: " & error) + + let xPtr = ffi_rln_proof_values_get_x(addr pvHandle) + if xPtr.isNil(): + return err("Failed to read proof x") + defer: + ffi_cfr_free(xPtr) + output.shareX = cfrToBytesLe(xPtr).valueOr: + return + err("failed call to cfrToBytesLe (shareX) in proofPtrToRateLimitProof: " & error) + + let yRes = ffi_rln_proof_values_get_y(addr pvHandle) + output.shareY = cfrResultToBytes(yRes, "Failed to read proof y: ").valueOr: + return err(error) + + let nullifierRes = ffi_rln_proof_values_get_nullifier(addr pvHandle) + output.nullifier = cfrResultToBytes(nullifierRes, "Failed to read proof nullifier: ").valueOr: + return err(error) + + let extNullPtr = ffi_rln_proof_values_get_external_nullifier(addr pvHandle) + if extNullPtr.isNil(): + return err("Failed to read proof external nullifier") + defer: + ffi_cfr_free(extNullPtr) + output.externalNullifier = cfrToBytesLe(extNullPtr).valueOr: + return err( + "failed call to cfrToBytesLe (externalNullifier) in proofPtrToRateLimitProof: " & + error + ) + + ok(output) + +proc parseCredentialVec(vec: var Vec_CFr): RlnRelayResult[IdentityCredential] = + ## Vec_CFr order: idTrapdoor, idNullifier, idSecretHash, idCommitment. + if int(ffi_vec_cfr_len(addr vec)) != 4: + return err("Unexpected credential element count") + + template readField(idx: int): seq[byte] = + let f = ffi_vec_cfr_get(addr vec, csize_t(idx)) + if f.isNil(): + return err("Missing credential field from zerokit") + let bytes = cfrToBytesLe(f).valueOr: + return err("failed call to cfrToBytesLe in parseCredentialVec: " & error) + @bytes + + let idTrapdoor = readField(0) + let idNullifier = readField(1) + let idSecretHash = readField(2) + let idCommitment = readField(3) + + return ok( + IdentityCredential( + idTrapdoor: idTrapdoor, + idNullifier: idNullifier, + idSecretHash: idSecretHash, + idCommitment: idCommitment, + ) ) - return ok(identityCredential) - -type RlnTreeConfig = ref object of RootObj - cache_capacity: int - mode: string - compression: bool - flush_every_ms: int - -type RlnConfig = ref object of RootObj - resources_folder: string - tree_config: RlnTreeConfig - -proc `%`(c: RlnConfig): JsonNode = - ## wrapper around the generic JObject constructor. - ## We don't need to have a separate proc for the tree_config field - let tree_config = - %{ - "cache_capacity": %c.tree_config.cache_capacity, - "mode": %c.tree_config.mode, - "compression": %c.tree_config.compression, - "flush_every_ms": %c.tree_config.flush_every_ms, - } - return %[("resources_folder", %c.resources_folder), ("tree_config", %tree_config)] +proc membershipKeyGen*(): RlnRelayResult[IdentityCredential] = + var vec = ffi_extended_key_gen() + defer: + ffi_vec_cfr_free(vec) + parseCredentialVec(vec) proc createRLNInstanceLocal(): RLNResult = - ## generates an instance of RLN - ## An RLN instance supports both zkSNARKs logics and Merkle tree data structure and operations - ## Returns an error if the instance creation fails - - let rln_config = RlnConfig( - resources_folder: "tree_height_/", - tree_config: RlnTreeConfig( - cache_capacity: 15_000, - mode: "high_throughput", - compression: false, - flush_every_ms: 500, - ), - ) - - var serialized_rln_config = $(%rln_config) - - var - rlnInstance: ptr RLN - merkleDepth: csize_t = uint(20) - configBuffer = - serialized_rln_config.toOpenArrayByte(0, serialized_rln_config.high).toBuffer() - - # create an instance of RLN - let res = new_circuit(merkleDepth, addr configBuffer, addr rlnInstance) - # check whether the circuit parameters are generated successfully - if (res == false): - info "error in parameters generation" - return err("error in parameters generation") - return ok(rlnInstance) + ## Creates a stateless RLN instance (no local Merkle tree). + let res = ffi_rln_new() + if res.ok.isNil(): + let msg = consumeError("error in parameters generation: ", res.err) + info "error in parameters generation", err = msg + return err(msg) + ok(res.ok) proc createRLNInstance*(): RLNResult = - ## Wraps the rln instance creation for metrics - ## Returns an error if the instance creation fails + ## Wraps createRLNInstanceLocal with metrics timing. var res: RLNResult waku_rln_instance_creation_duration_seconds.nanosecondTime: res = createRLNInstanceLocal() return res -proc sha256*(data: openArray[byte]): RlnRelayResult[MerkleNode] = - ## a thin layer on top of the Nim wrapper of the sha256 hasher - var lenPrefData = encodeLengthPrefix(data) - var - hashInputBuffer = lenPrefData.toBuffer() - outputBuffer: Buffer # will holds the hash output - - trace "sha256 hash input buffer length", bufflen = hashInputBuffer.len - let hashSuccess = sha256(addr hashInputBuffer, addr outputBuffer, true) - - # check whether the hash call is done successfully - if not hashSuccess: - return err("error in sha256 hash") - - let output = cast[ptr MerkleNode](outputBuffer.`ptr`)[] - - return ok(output) - -proc poseidon*(data: seq[seq[byte]]): RlnRelayResult[array[32, byte]] = - ## a thin layer on top of the Nim wrapper of the poseidon hasher - var inputBytes = serialize(data) - var - hashInputBuffer = inputBytes.toBuffer() - outputBuffer: Buffer # will holds the hash output - - let hashSuccess = poseidon(addr hashInputBuffer, addr outputBuffer, true) - - # check whether the hash call is done successfully - if not hashSuccess: - return err("error in poseidon hash") - - let output = cast[ptr array[32, byte]](outputBuffer.`ptr`)[] - - return ok(output) +proc poseidon*(left, right: seq[byte]): RlnRelayResult[array[32, byte]] = + ## Poseidon hash of exactly 2 inputs; zerokit v2 FFI only exposes the pair variant. + poseidonPairLe(left, right) proc toLeaf*(rateCommitment: RateCommitment): RlnRelayResult[seq[byte]] = let idCommitment = rateCommitment.idCommitment @@ -165,7 +156,7 @@ proc toLeaf*(rateCommitment: RateCommitment): RlnRelayResult[seq[byte]] = return err( "could not convert the user message limit to bytes: " & getCurrentExceptionMsg() ) - let leaf = poseidon(@[@idCommitment, @userMessageLimit]).valueOr: + let leaf = poseidon(@idCommitment, @userMessageLimit).valueOr: return err("could not convert the rate commitment to a leaf") var retLeaf = newSeq[byte](leaf.len) for i in 0 ..< leaf.len: @@ -180,9 +171,31 @@ proc toLeaves*(rateCommitments: seq[RateCommitment]): RlnRelayResult[seq[seq[byt leaves.add(leaf) return ok(leaves) +proc generateExternalNullifier*( + epoch: Epoch, rlnIdentifier: RlnIdentifier +): RlnRelayResult[ExternalNullifier] = + ## externalNullifier = Poseidon(H(epoch), H(rlnIdentifier)); H = ffi_hash_to_field_le. + let epochFr = hashToFieldLe(@epoch).valueOr: + return err("Failed to hash epoch to field: " & error) + defer: + ffi_cfr_free(epochFr) + let rlnIdFr = hashToFieldLe(@rlnIdentifier).valueOr: + return err("Failed to hash rlnIdentifier to field: " & error) + defer: + ffi_cfr_free(rlnIdFr) + let cfr = ffi_poseidon_hash_pair(epochFr, rlnIdFr) + if cfr.isNil(): + return err("Failed to compute external nullifier") + defer: + ffi_cfr_free(cfr) + cfrToBytesLe(cfr).mapErr( + proc(e: string): string = + "Failed to serialize external nullifier: " & e + ) + proc extractMetadata*(proof: RateLimitProof): RlnRelayResult[ProofMetadata] = - let externalNullifier = poseidon(@[@(proof.epoch), @(proof.rlnIdentifier)]).valueOr: - return err("could not construct the external nullifier") + let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr: + return err("Failed to compute external nullifier: " & error) return ok( ProofMetadata( nullifier: proof.nullifier, @@ -191,3 +204,178 @@ proc extractMetadata*(proof: RateLimitProof): RlnRelayResult[ProofMetadata] = externalNullifier: externalNullifier, ) ) + +proc buildPathElementsVec( + pathElements: seq[byte], depth: int +): RlnRelayResult[Vec_CFr] = + ## Caller MUST ffi_vec_cfr_free the returned Vec_CFr. + var vec = ffi_vec_cfr_new(csize_t(depth)) + for i in 0 ..< depth: + let start = i * FieldElementSize + let element = bytesToCfrLe( + pathElements.toOpenArray(start, start + FieldElementSize - 1) + ).valueOr: + ffi_vec_cfr_free(vec) + return err( + "failed call to bytesToCfrLe (path element) in buildPathElementsVec: " & error + ) + ffi_vec_cfr_push(addr vec, element) + ffi_cfr_free(element) + ok(vec) + +proc buildWitnessInput( + witness: RLNWitnessInput +): RlnRelayResult[ptr FFI_RLNWitnessInput] = + ## ffi_rln_witness_input_new copies all inputs, so the intermediate CFrs/vecs + ## are freed here. Caller MUST ffi_rln_witness_input_free the returned handle. + let depth = witness.identity_path_index.len + if witness.path_elements.len != depth * FieldElementSize: + return err( + "Invalid Merkle path: expected " & $(depth * FieldElementSize) & " bytes for " & + $depth & " levels, got " & $witness.path_elements.len + ) + + var pathElementsVec = buildPathElementsVec(witness.path_elements, depth).valueOr: + return err("failed call to buildPathElementsVec in buildWitnessInput: " & error) + defer: + ffi_vec_cfr_free(pathElementsVec) + + var pathIndexVec = toVecUint8(witness.identity_path_index) + + let identitySecret = bytesToCfrLe(witness.identity_secret).valueOr: + return err( + "failed call to bytesToCfrLe (identity_secret) in buildWitnessInput: " & error + ) + defer: + ffi_cfr_free(identitySecret) + let userLimit = bytesToCfrLe(witness.user_message_limit).valueOr: + return err( + "failed call to bytesToCfrLe (user_message_limit) in buildWitnessInput: " & error + ) + defer: + ffi_cfr_free(userLimit) + let messageIdFr = bytesToCfrLe(witness.message_id).valueOr: + return + err("failed call to bytesToCfrLe (message_id) in buildWitnessInput: " & error) + defer: + ffi_cfr_free(messageIdFr) + let xFr = bytesToCfrLe(witness.x).valueOr: + return err("failed call to bytesToCfrLe (x) in buildWitnessInput: " & error) + defer: + ffi_cfr_free(xFr) + let externalNullifierFr = bytesToCfrLe(witness.external_nullifier).valueOr: + return err( + "failed call to bytesToCfrLe (external_nullifier) in buildWitnessInput: " & error + ) + defer: + ffi_cfr_free(externalNullifierFr) + + let witnessRes = ffi_rln_witness_input_new( + identitySecret, + userLimit, + messageIdFr, + addr pathElementsVec, + addr pathIndexVec, + xFr, + externalNullifierFr, + ) + if witnessRes.ok.isNil(): + return err( + consumeError("Failed to create witness in buildWitnessInput: ", witnessRes.err) + ) + return ok(witnessRes.ok) + +proc generateRlnProofWithWitness*( + rlnInstance: ptr RLN, + witness: RLNWitnessInput, + epoch: Epoch, + rlnIdentifier: RlnIdentifier, +): RlnRelayResult[RateLimitProof] = + let witnessHandle = buildWitnessInput(witness).valueOr: + return + err("failed call to buildWitnessInput in generateRlnProofWithWitness: " & error) + defer: + ffi_rln_witness_input_free(witnessHandle) + + var ctx = rlnInstance + var wh = witnessHandle + let proofRes = ffi_generate_rln_proof(addr ctx, addr wh) + if proofRes.ok.isNil(): + return err(consumeError("Failed to generate RLN proof: ", proofRes.err)) + defer: + ffi_rln_proof_free(proofRes.ok) + + return proofPtrToRateLimitProof(proofRes.ok, epoch, rlnIdentifier) + +proc buildRlnProof( + proof: RateLimitProof, externalNullifier: ExternalNullifier +): RlnRelayResult[ptr FFI_RLNProof] = + ## ffi_rln_proof_new copies all inputs, so the intermediate CFrs are freed + ## here. Caller MUST ffi_rln_proof_free the returned handle. + var groth16Vec = toVecUint8(proof.proof) + let rootFr = bytesToCfrLe(proof.merkleRoot).valueOr: + return err("failed call to bytesToCfrLe (root) in buildRlnProof: " & error) + defer: + ffi_cfr_free(rootFr) + let extNullFr = bytesToCfrLe(externalNullifier).valueOr: + return + err("failed call to bytesToCfrLe (externalNullifier) in buildRlnProof: " & error) + defer: + ffi_cfr_free(extNullFr) + let shareXFr = bytesToCfrLe(proof.shareX).valueOr: + return err("failed call to bytesToCfrLe (shareX) in buildRlnProof: " & error) + defer: + ffi_cfr_free(shareXFr) + let shareYFr = bytesToCfrLe(proof.shareY).valueOr: + return err("failed call to bytesToCfrLe (shareY) in buildRlnProof: " & error) + defer: + ffi_cfr_free(shareYFr) + let nullifierFr = bytesToCfrLe(proof.nullifier).valueOr: + return err("failed call to bytesToCfrLe (nullifier) in buildRlnProof: " & error) + defer: + ffi_cfr_free(nullifierFr) + + let proofRes = ffi_rln_proof_new( + addr groth16Vec, rootFr, extNullFr, shareXFr, shareYFr, nullifierFr + ) + if proofRes.ok.isNil(): + return + err(consumeError("Failed to build RLN proof in buildRlnProof: ", proofRes.err)) + return ok(proofRes.ok) + +proc verifyRlnProof*( + rlnInstance: ptr RLN, + proof: RateLimitProof, + signal: openArray[byte], + validRoots: seq[MerkleNode], +): RlnRelayResult[bool] = + if validRoots.len == 0: + return err("verifyRlnProof requires at least one valid root (stateless mode)") + + # externalNullifier isn't a protobuf wire field, so a received proof has it + # zeroed; recompute from epoch + rlnIdentifier. + let externalNullifier = generateExternalNullifier(proof.epoch, proof.rlnIdentifier).valueOr: + return err("failed call to generateExternalNullifier in verifyRlnProof: " & error) + + let proofHandlePtr = buildRlnProof(proof, externalNullifier).valueOr: + return err("failed call to buildRlnProof in verifyRlnProof: " & error) + defer: + ffi_rln_proof_free(proofHandlePtr) + + let xFr = hashToFieldLe(signal).valueOr: + return err("failed call to hashToFieldLe (signal) in verifyRlnProof: " & error) + defer: + ffi_cfr_free(xFr) + + var roots = toRootVec(validRoots).valueOr: + return err("failed call to toRootVec in verifyRlnProof: " & error) + defer: + ffi_vec_cfr_free(roots) + + var ctx = rlnInstance + var proofHandle = proofHandlePtr + let verifyRes = ffi_verify_with_roots(addr ctx, addr proofHandle, addr roots, xFr) + # zerokit FFI quirk: err is non-nil for all failures; free it and return the bool. + if hasError(verifyRes.err): + ffi_c_string_free(verifyRes.err) + return ok(verifyRes.ok) diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index 817bb8720..7c36300b2 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -13,7 +13,9 @@ import libp2p/protocols/pubsub/rpc/messages, libp2p/protocols/pubsub/pubsub, results, - stew/[byteutils, arrayops] + stew/[byteutils, arrayops], + brokers/broker_context + import ./group_manager, ./rln, @@ -24,10 +26,13 @@ import ./nonce_manager import - ../common/error_handling, - ../waku_relay, # for WakuRelayHandler - ../waku_core, - ../waku_keystore + waku/[ + common/error_handling, + waku_relay, # for WakuRelayHandler + waku_core, + requests/rln_requests, + waku_keystore, + ] logScope: topics = "waku rln_relay" @@ -64,7 +69,8 @@ type WakuRLNRelay* = ref object of RootObj onFatalErrorAction*: OnFatalErrorHandler nonceManager*: NonceManager epochMonitorFuture*: Future[void] - rootChangesFuture*: Future[void] + rootChangesFuture*: Future[Result[void, string]] + brokerCtx*: BrokerContext proc calcEpoch*(rlnPeer: WakuRLNRelay, t: float64): Epoch = ## gets time `t` as `flaot64` with subseconds resolution in the fractional part @@ -91,6 +97,7 @@ proc stop*(rlnPeer: WakuRLNRelay) {.async: (raises: [Exception]).} = # stop the group sync, and flush data to tree db info "stopping rln-relay" + RequestGenerateRlnProof.clearProvider(rlnPeer.brokerCtx) await rlnPeer.groupManager.stop() proc hasDuplicate*( @@ -178,12 +185,9 @@ proc validateMessage*( ## `timeOption` indicates Unix epoch time (fractional part holds sub-seconds) ## if `timeOption` is supplied, then the current epoch is calculated based on that - let decodeRes = RateLimitProof.init(msg.proof) - if decodeRes.isErr(): + let proof = RateLimitProof.init(msg.proof).valueOr: return MessageValidationResult.Invalid - let proof = decodeRes.get() - # track message count for metrics waku_rln_messages_total.inc() @@ -198,14 +202,18 @@ proc validateMessage*( if timeDiff > rlnPeer.rlnMaxTimestampGap: warn "invalid message: timestamp difference exceeds threshold", - timeDiff = timeDiff, maxTimestampGap = rlnPeer.rlnMaxTimestampGap + timeDiff = timeDiff, + maxTimestampGap = rlnPeer.rlnMaxTimestampGap, + contentTopic = msg.contentTopic waku_rln_invalid_messages_total.inc(labelValues = ["invalid_timestamp"]) return MessageValidationResult.Invalid let computedEpoch = rlnPeer.calcEpoch(messageTime) if proof.epoch != computedEpoch: warn "invalid message: timestamp mismatches epoch", - proofEpoch = fromEpoch(proof.epoch), computedEpoch = fromEpoch(computedEpoch) + proofEpoch = fromEpoch(proof.epoch), + computedEpoch = fromEpoch(computedEpoch), + contentTopic = msg.contentTopic waku_rln_invalid_messages_total.inc(labelValues = ["timestamp_mismatch"]) return MessageValidationResult.Invalid @@ -213,7 +221,8 @@ proc validateMessage*( if not rootValidationRes: warn "invalid message: provided root does not belong to acceptable window of roots", provided = proof.merkleRoot.inHex(), - validRoots = rlnPeer.groupManager.validRoots.mapIt(it.inHex()) + validRoots = rlnPeer.groupManager.validRoots.mapIt(it.inHex()), + contentTopic = msg.contentTopic waku_rln_invalid_messages_total.inc(labelValues = ["invalid_root"]) return MessageValidationResult.Invalid @@ -228,33 +237,36 @@ proc validateMessage*( let proofVerificationRes = rlnPeer.groupManager.verifyProof(msg.toRLNSignal(), proof) - if proofVerificationRes.isErr(): + proofVerificationRes.isOkOr: waku_rln_errors_total.inc(labelValues = ["proof_verification"]) - warn "invalid message: proof verification failed", payloadLen = msg.payload.len + warn "invalid message: proof verification failed", + payloadLen = msg.payload.len, contentTopic = msg.contentTopic return MessageValidationResult.Invalid if not proofVerificationRes.value(): # invalid proof - warn "invalid message: invalid proof", payloadLen = msg.payload.len + warn "invalid message: invalid proof", + payloadLen = msg.payload.len, contentTopic = msg.contentTopic waku_rln_invalid_messages_total.inc(labelValues = ["invalid_proof"]) return MessageValidationResult.Invalid # check if double messaging has happened - let proofMetadataRes = proof.extractMetadata() - if proofMetadataRes.isErr(): + let proofMetadata = proof.extractMetadata().valueOr: waku_rln_errors_total.inc(labelValues = ["proof_metadata_extraction"]) return MessageValidationResult.Invalid let msgEpoch = proof.epoch - let hasDup = rlnPeer.hasDuplicate(msgEpoch, proofMetadataRes.get()) + let hasDup = rlnPeer.hasDuplicate(msgEpoch, proofMetadata) if hasDup.isErr(): waku_rln_errors_total.inc(labelValues = ["duplicate_check"]) elif hasDup.value == true: - trace "invalid message: message is spam", payloadLen = msg.payload.len + trace "invalid message: message is spam", + payloadLen = msg.payload.len, contentTopic = msg.contentTopic waku_rln_spam_messages_total.inc() return MessageValidationResult.Spam - trace "message is valid", payloadLen = msg.payload.len + trace "message is valid", + payloadLen = msg.payload.len, contentTopic = msg.contentTopic # Metric increment moved to validator to include shard label return MessageValidationResult.Valid @@ -266,28 +278,24 @@ proc validateMessageAndUpdateLog*( let isValidMessage = rlnPeer.validateMessage(msg) - let decodeRes = RateLimitProof.init(msg.proof) - if decodeRes.isErr(): + let msgProof = RateLimitProof.init(msg.proof).valueOr: return MessageValidationResult.Invalid - let msgProof = decodeRes.get() - let proofMetadataRes = msgProof.extractMetadata() - - if proofMetadataRes.isErr(): + let proofMetadata = msgProof.extractMetadata().valueOr: return MessageValidationResult.Invalid # insert the message to the log (never errors) only if the # message is valid. if isValidMessage == MessageValidationResult.Valid: - discard rlnPeer.updateLog(msgProof.epoch, proofMetadataRes.get()) + discard rlnPeer.updateLog(msgProof.epoch, proofMetadata) return isValidMessage -proc appendRLNProof*( - rlnPeer: WakuRLNRelay, msg: var WakuMessage, senderEpochTime: float64 -): RlnRelayResult[void] = - ## returns true if it can create and append a `RateLimitProof` to the supplied `msg` - ## returns false otherwise +proc createRlnProof( + rlnPeer: WakuRLNRelay, msg: WakuMessage, senderEpochTime: float64 +): RlnRelayResult[seq[byte]] = + ## returns a new `RateLimitProof` for the supplied `msg` + ## returns an error if it cannot create the proof ## `senderEpochTime` indicates the number of seconds passed since Unix epoch. The fractional part holds sub-seconds. ## The `epoch` field of `RateLimitProof` is derived from the provided `senderEpochTime` (using `calcEpoch()`) @@ -299,7 +307,14 @@ proc appendRLNProof*( let proof = rlnPeer.groupManager.generateProof(input, epoch, nonce).valueOr: return err("could not generate rln-v2 proof: " & $error) - msg.proof = proof.encode().buffer + return ok(proof.encode().buffer) + +proc appendRLNProof*( + rlnPeer: WakuRLNRelay, msg: var WakuMessage, senderEpochTime: float64 +): RlnRelayResult[void] = + msg.proof = rlnPeer.createRlnProof(msg, senderEpochTime).valueOr: + return err($error) + return ok() proc clearNullifierLog*(rlnPeer: WakuRlnRelay) = @@ -333,19 +348,15 @@ proc generateRlnValidator*( trace "rln-relay topic validator is called" wakuRlnRelay.clearNullifierLog() - let decodeRes = RateLimitProof.init(message.proof) - - if decodeRes.isErr(): - trace "generateRlnValidator reject", error = decodeRes.error + let msgProof = RateLimitProof.init(message.proof).valueOr: + trace "generateRlnValidator reject", error = error return pubsub.ValidationResult.Reject - let msgProof = decodeRes.get() - # validate the message and update log let validationRes = wakuRlnRelay.validateMessageAndUpdateLog(message) let - proof = toHex(msgProof.proof) + proof = byteutils.toHex(msgProof.proof) epoch = fromEpoch(msgProof.epoch) root = inHex(msgProof.merkleRoot) shareX = inHex(msgProof.shareX) @@ -441,6 +452,7 @@ proc mount( rlnMaxEpochGap: max(uint64(MaxClockGapSeconds / float64(conf.epochSizeSec)), 1), rlnMaxTimestampGap: uint64(MaxClockGapSeconds), onFatalErrorAction: conf.onFatalErrorAction, + brokerCtx: globalBrokerContext(), ) # track root changes on smart contract merkle tree @@ -450,9 +462,22 @@ proc mount( # Start epoch monitoring in the background wakuRlnRelay.epochMonitorFuture = monitorEpochs(wakuRlnRelay) + + RequestGenerateRlnProof.setProvider( + wakuRlnRelay.brokerCtx, + proc( + msg: WakuMessage, senderEpochTime: float64 + ): Future[Result[RequestGenerateRlnProof, string]] {.async.} = + let proof = createRlnProof(wakuRlnRelay, msg, senderEpochTime).valueOr: + return err("Could not create RLN proof: " & $error) + + return ok(RequestGenerateRlnProof(proof: proof)), + ).isOkOr: + return err("Proof generator provider cannot be set: " & $error) + return ok(wakuRlnRelay) -proc isReady*(rlnPeer: WakuRLNRelay): Future[bool] {.async: (raises: [Exception]).} = +proc isReady*(rlnPeer: WakuRLNRelay): Future[bool] {.async.} = ## returns true if the rln-relay protocol is ready to relay messages ## returns false otherwise diff --git a/waku/waku_store/client.nim b/waku/waku_store/client.nim index 308d7f98e..b49662811 100644 --- a/waku/waku_store/client.nim +++ b/waku/waku_store/client.nim @@ -1,6 +1,12 @@ {.push raises: [].} -import std/[options, tables], results, chronicles, chronos, metrics, bearssl/rand +import + std/[options, tables, sequtils, algorithm, random], + results, + chronicles, + chronos, + metrics, + bearssl/rand import ../node/peer_manager, ../utils/requests, ./protocol_metrics, ./common, ./rpc_codec @@ -10,6 +16,8 @@ logScope: const DefaultPageSize*: uint = 20 # A recommended default number of waku messages per page +const MaxQueryRetries = 5 # Maximum number of store peers to try before giving up + type WakuStoreClient* = ref object peerManager: PeerManager rng: ref rand.HmacDrbgContext @@ -25,7 +33,9 @@ proc sendStoreRequest( ): Future[StoreQueryResult] {.async, gcsafe.} = var req = request + self.peerManager.addActiveStoreRequest(connection.peerId) defer: + self.peerManager.removeActiveStoreRequest(connection.peerId) await connection.closeWithEof() if req.requestId == "": @@ -79,18 +89,34 @@ proc query*( proc queryToAny*( self: WakuStoreClient, request: StoreQueryRequest, peerId = none(PeerId) ): Future[StoreQueryResult] {.async.} = - ## This proc is similar to the query one but in this case - ## we don't specify a particular peer and instead we get it from peer manager + ## we don't specify a particular peer and instead we get it from peer manager. + ## It will retry with different store peers if the dial fails. if request.paginationCursor.isSome() and request.paginationCursor.get() == EmptyCursor: return err(StoreError(kind: ErrorCode.BAD_REQUEST, cause: "invalid cursor")) - let peer = self.peerManager.selectPeer(WakuStoreCodec).valueOr: + # Get all available store peers + var peers = self.peerManager.switch.peerStore.getPeersByProtocol(WakuStoreCodec) + if peers.len == 0: return err(StoreError(kind: BAD_RESPONSE, cause: "no service store peer connected")) - let connection = (await self.peerManager.dialPeer(peer, WakuStoreCodec)).valueOr: - waku_store_errors.inc(labelValues = [DialFailure]) + # Shuffle to distribute load across store peers and limit retries + shuffle(peers) + let peersToTry = peers[0 ..< min(peers.len, MaxQueryRetries)] - return err(StoreError(kind: ErrorCode.PEER_DIAL_FAILURE, address: $peer)) + var lastError: StoreError + for peer in peersToTry: + let connection = (await self.peerManager.dialPeer(peer, WakuStoreCodec)).valueOr: + waku_store_errors.inc(labelValues = [DialFailure]) + warn "failed to dial store peer, trying next" + lastError = StoreError(kind: ErrorCode.PEER_DIAL_FAILURE, address: $peer) + continue - return await self.sendStoreRequest(request, connection) + let response = (await self.sendStoreRequest(request, connection)).valueOr: + warn "store query failed, trying next peer", peerId = peer.peerId, error = $error + lastError = error + continue + + return ok(response) + + return err(lastError) diff --git a/waku/waku_store/common.nim b/waku/waku_store/common.nim index d11c803f9..70446be4b 100644 --- a/waku/waku_store/common.nim +++ b/waku/waku_store/common.nim @@ -15,7 +15,7 @@ const type WakuStoreResult*[T] = Result[T, string] -## Public API types +## API types type StoreQueryRequest* = object diff --git a/waku/waku_store/protocol.nim b/waku/waku_store/protocol.nim index 395936625..17b7fb214 100644 --- a/waku/waku_store/protocol.nim +++ b/waku/waku_store/protocol.nim @@ -93,7 +93,9 @@ proc initProtocolHandler(self: WakuStore) = var resBuf: StoreResp var queryDuration: float + self.peerManager.addActiveStoreRequest(conn.peerId) defer: + self.peerManager.removeActiveStoreRequest(conn.peerId) await conn.closeWithEof() self.requestRateLimiter.checkUsageLimit(WakuStoreCodec, conn): @@ -132,8 +134,8 @@ proc initProtocolHandler(self: WakuStore) = let writeRes = catch: await conn.writeLp(resBuf.resp) - if writeRes.isErr(): - error "Connection write error", error = writeRes.error.msg + writeRes.isOkOr: + error "Connection write error", error = error.msg return if successfulQuery: diff --git a/waku/waku_store/resume.nim b/waku/waku_store/resume.nim index 208ba0aa6..b7864da94 100644 --- a/waku/waku_store/resume.nim +++ b/waku/waku_store/resume.nim @@ -92,8 +92,8 @@ proc initTransferHandler( let catchable = catch: await wakuStoreClient.query(req, peer) - if catchable.isErr(): - return err("store client error: " & catchable.error.msg) + catchable.isOkOr: + return err("store client error: " & error.msg) let res = catchable.get() let response = res.valueOr: @@ -105,8 +105,8 @@ proc initTransferHandler( let handleRes = catch: await wakuArchive.handleMessage(kv.pubsubTopic.get(), kv.message.get()) - if handleRes.isErr(): - error "message transfer failed", error = handleRes.error.msg + handleRes.isOkOr: + error "message transfer failed", error = error.msg continue if req.paginationCursor.isNone(): diff --git a/waku/waku_store/self_req_handler.nim b/waku/waku_store/self_req_handler.nim index 116946da5..315961307 100644 --- a/waku/waku_store/self_req_handler.nim +++ b/waku/waku_store/self_req_handler.nim @@ -25,11 +25,8 @@ proc handleSelfStoreRequest*( let handlerResult = catch: await self.requestHandler(req) - let resResult = - if handlerResult.isErr(): - return err("exception in handleSelfStoreRequest: " & handlerResult.error.msg) - else: - handlerResult.get() + let resResult = handlerResult.valueOr: + return err("exception in handleSelfStoreRequest: " & error.msg) let res = resResult.valueOr: return err("error in handleSelfStoreRequest: " & $error) diff --git a/waku/waku_store_legacy.nim b/waku/waku_store_legacy.nim deleted file mode 100644 index 9dac194c7..000000000 --- a/waku/waku_store_legacy.nim +++ /dev/null @@ -1,3 +0,0 @@ -import ./waku_store_legacy/common, ./waku_store_legacy/protocol - -export common, protocol diff --git a/waku/waku_store_legacy/README.md b/waku/waku_store_legacy/README.md deleted file mode 100644 index f2068734f..000000000 --- a/waku/waku_store_legacy/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Waku Store protocol - -The store protocol implements historical message support. See https://rfc.vac.dev/spec/13/ for more information. diff --git a/waku/waku_store_legacy/client.nim b/waku/waku_store_legacy/client.nim deleted file mode 100644 index d3301cfa4..000000000 --- a/waku/waku_store_legacy/client.nim +++ /dev/null @@ -1,248 +0,0 @@ -{.push raises: [].} - -import std/options, results, chronicles, chronos, metrics, bearssl/rand -import - ../node/peer_manager, - ../utils/requests, - ./protocol_metrics, - ./common, - ./rpc, - ./rpc_codec - -when defined(waku_exp_store_resume): - import std/[sequtils, times] - import ../waku_archive - import ../waku_core/message/digest - -logScope: - topics = "waku legacy store client" - -const DefaultPageSize*: uint = 20 - # A recommended default number of waku messages per page - -type WakuStoreClient* = ref object - peerManager: PeerManager - rng: ref rand.HmacDrbgContext - - # TODO: Move outside of the client - when defined(waku_exp_store_resume): - store: ArchiveDriver - -proc new*( - T: type WakuStoreClient, peerManager: PeerManager, rng: ref rand.HmacDrbgContext -): T = - WakuStoreClient(peerManager: peerManager, rng: rng) - -proc sendHistoryQueryRPC( - w: WakuStoreClient, req: HistoryQuery, peer: RemotePeerInfo -): Future[HistoryResult] {.async, gcsafe.} = - let connOpt = await w.peerManager.dialPeer(peer, WakuLegacyStoreCodec) - if connOpt.isNone(): - waku_legacy_store_errors.inc(labelValues = [dialFailure]) - return err(HistoryError(kind: HistoryErrorKind.PEER_DIAL_FAILURE, address: $peer)) - - let connection = connOpt.get() - - defer: - await connection.closeWithEof() - - let requestId = - if req.requestId != "": - req.requestId - else: - generateRequestId(w.rng) - - let reqRpc = HistoryRPC(requestId: requestId, query: some(req.toRPC())) - await connection.writeLP(reqRpc.encode().buffer) - - #TODO: I see a challenge here, if storeNode uses a different MaxRPCSize this read will fail. - # Need to find a workaround for this. - let buf = await connection.readLp(DefaultMaxRpcSize.int) - let respDecodeRes = HistoryRPC.decode(buf) - if respDecodeRes.isErr(): - waku_legacy_store_errors.inc(labelValues = [decodeRpcFailure]) - return - err(HistoryError(kind: HistoryErrorKind.BAD_RESPONSE, cause: decodeRpcFailure)) - - let respRpc = respDecodeRes.get() - - # Disabled ,for now, since the default response is a possible case (no messages, pagesize = 0, error = NONE(0)) - # TODO: Rework the RPC protocol to differentiate the default value from an empty value (e.g., status = 200 (OK)) - # and rework the protobuf parsing to return Option[T] when empty values are received - if respRpc.response.isNone(): - waku_legacy_store_errors.inc(labelValues = [emptyRpcResponseFailure]) - return err( - HistoryError(kind: HistoryErrorKind.BAD_RESPONSE, cause: emptyRpcResponseFailure) - ) - - let resp = respRpc.response.get() - - return resp.toAPI() - -proc query*( - w: WakuStoreClient, req: HistoryQuery, peer: RemotePeerInfo -): Future[HistoryResult] {.async, gcsafe.} = - return await w.sendHistoryQueryRPC(req, peer) - -# TODO: Move outside of the client -when defined(waku_exp_store_resume): - ## Resume store - - const StoreResumeTimeWindowOffset: Timestamp = getNanosecondTime(20) - ## Adjust the time window with an offset of 20 seconds - - proc new*( - T: type WakuStoreClient, - peerManager: PeerManager, - rng: ref rand.HmacDrbgContext, - store: ArchiveDriver, - ): T = - WakuStoreClient(peerManager: peerManager, rng: rng, store: store) - - proc queryAll( - w: WakuStoreClient, query: HistoryQuery, peer: RemotePeerInfo - ): Future[WakuStoreResult[seq[WakuMessage]]] {.async, gcsafe.} = - ## A thin wrapper for query. Sends the query to the given peer. when the query has a valid pagingInfo, - ## it retrieves the historical messages in pages. - ## Returns all the fetched messages, if error occurs, returns an error string - - # Make a copy of the query - var req = query - - var messageList: seq[WakuMessage] = @[] - - while true: - let queryRes = await w.query(req, peer) - if queryRes.isErr(): - return err($queryRes.error) - - let response = queryRes.get() - - messageList.add(response.messages) - - # Check whether it is the last page - if response.cursor.isNone(): - break - - # Update paging cursor - req.cursor = response.cursor - - return ok(messageList) - - proc queryLoop( - w: WakuStoreClient, req: HistoryQuery, peers: seq[RemotePeerInfo] - ): Future[WakuStoreResult[seq[WakuMessage]]] {.async, gcsafe.} = - ## Loops through the peers candidate list in order and sends the query to each - ## - ## Once all responses have been received, the retrieved messages are consolidated into one deduplicated list. - ## if no messages have been retrieved, the returned future will resolve into a result holding an empty seq. - let queryFuturesList = peers.mapIt(w.queryAll(req, it)) - - await allFutures(queryFuturesList) - - let messagesList = queryFuturesList - .map( - proc(fut: Future[WakuStoreResult[seq[WakuMessage]]]): seq[WakuMessage] = - try: - # fut.read() can raise a CatchableError - # These futures have been awaited before using allFutures(). Call completed() just as a sanity check. - if not fut.completed() or fut.read().isErr(): - return @[] - - fut.read().value - except CatchableError: - return @[] - ) - .concat() - .deduplicate() - - return ok(messagesList) - - proc put( - store: ArchiveDriver, pubsubTopic: PubsubTopic, message: WakuMessage - ): Result[void, string] = - let - digest = waku_archive.computeDigest(message) - messageHash = computeMessageHash(pubsubTopic, message) - receivedTime = - if message.timestamp > 0: - message.timestamp - else: - getNanosecondTime(getTime().toUnixFloat()) - - store.put(pubsubTopic, message, digest, messageHash, receivedTime) - - proc resume*( - w: WakuStoreClient, - peerList = none(seq[RemotePeerInfo]), - pageSize = DefaultPageSize, - pubsubTopic = DefaultPubsubTopic, - ): Future[WakuStoreResult[uint64]] {.async, gcsafe.} = - ## resume proc retrieves the history of waku messages published on the default waku pubsub topic since the last time the waku store node has been online - ## messages are stored in the store node's messages field and in the message db - ## the offline time window is measured as the difference between the current time and the timestamp of the most recent persisted waku message - ## an offset of 20 second is added to the time window to count for nodes asynchrony - ## peerList indicates the list of peers to query from. - ## The history is fetched from all available peers in this list and then consolidated into one deduplicated list. - ## Such candidates should be found through a discovery method (to be developed). - ## if no peerList is passed, one of the peers in the underlying peer manager unit of the store protocol is picked randomly to fetch the history from. - ## The history gets fetched successfully if the dialed peer has been online during the queried time window. - ## the resume proc returns the number of retrieved messages if no error occurs, otherwise returns the error string - - # If store has not been provided, don't even try - if w.store.isNil(): - return err("store not provided (nil)") - - # NOTE: Original implementation is based on the message's sender timestamp. At the moment - # of writing, the sqlite store implementation returns the last message's receiver - # timestamp. - # lastSeenTime = lastSeenItem.get().msg.timestamp - let - lastSeenTime = w.store.getNewestMessageTimestamp().get(Timestamp(0)) - now = getNanosecondTime(getTime().toUnixFloat()) - - info "resuming with offline time window", - lastSeenTime = lastSeenTime, currentTime = now - - let - queryEndTime = now + StoreResumeTimeWindowOffset - queryStartTime = max(lastSeenTime - StoreResumeTimeWindowOffset, 0) - - let req = HistoryQuery( - pubsubTopic: some(pubsubTopic), - startTime: some(queryStartTime), - endTime: some(queryEndTime), - pageSize: uint64(pageSize), - direction: default(), - ) - - var res: WakuStoreResult[seq[WakuMessage]] - if peerList.isSome(): - info "trying the candidate list to fetch the history" - res = await w.queryLoop(req, peerList.get()) - else: - info "no candidate list is provided, selecting a random peer" - # if no peerList is set then query from one of the peers stored in the peer manager - let peerOpt = w.peerManager.selectPeer(WakuLegacyStoreCodec) - if peerOpt.isNone(): - warn "no suitable remote peers" - waku_legacy_store_errors.inc(labelValues = [peerNotFoundFailure]) - return err("no suitable remote peers") - - info "a peer is selected from peer manager" - res = await w.queryAll(req, peerOpt.get()) - - if res.isErr(): - info "failed to resume the history" - return err("failed to resume the history") - - # Save the retrieved messages in the store - var added: uint = 0 - for msg in res.get(): - let putStoreRes = w.store.put(pubsubTopic, msg) - if putStoreRes.isErr(): - continue - - added.inc() - - return ok(added) diff --git a/waku/waku_store_legacy/common.nim b/waku/waku_store_legacy/common.nim deleted file mode 100644 index c1958f201..000000000 --- a/waku/waku_store_legacy/common.nim +++ /dev/null @@ -1,108 +0,0 @@ -{.push raises: [].} - -import std/[options, sequtils], results, stew/byteutils, nimcrypto/sha2 -import ../waku_core, ../common/paging - -from ../waku_core/codecs import WakuLegacyStoreCodec -export WakuLegacyStoreCodec - -const - DefaultPageSize*: uint64 = 20 - - MaxPageSize*: uint64 = 100 - -type WakuStoreResult*[T] = Result[T, string] - -## Waku message digest - -type MessageDigest* = MDigest[256] - -proc computeDigest*(msg: WakuMessage): MessageDigest = - var ctx: sha256 - ctx.init() - defer: - ctx.clear() - - ctx.update(msg.contentTopic.toBytes()) - ctx.update(msg.payload) - - # Computes the hash - return ctx.finish() - -## Public API types - -type - HistoryCursor* = object - pubsubTopic*: PubsubTopic - senderTime*: Timestamp - storeTime*: Timestamp - digest*: MessageDigest - - HistoryQuery* = object - pubsubTopic*: Option[PubsubTopic] - contentTopics*: seq[ContentTopic] - cursor*: Option[HistoryCursor] - startTime*: Option[Timestamp] - endTime*: Option[Timestamp] - pageSize*: uint64 - direction*: PagingDirection - requestId*: string - - HistoryResponse* = object - messages*: seq[WakuMessage] - cursor*: Option[HistoryCursor] - - HistoryErrorKind* {.pure.} = enum - UNKNOWN = uint32(000) - BAD_RESPONSE = uint32(300) - BAD_REQUEST = uint32(400) - TOO_MANY_REQUESTS = uint32(429) - SERVICE_UNAVAILABLE = uint32(503) - PEER_DIAL_FAILURE = uint32(504) - - HistoryError* = object - case kind*: HistoryErrorKind - of PEER_DIAL_FAILURE: - address*: string - of BAD_RESPONSE, BAD_REQUEST: - cause*: string - else: - discard - - HistoryResult* = Result[HistoryResponse, HistoryError] - -proc parse*(T: type HistoryErrorKind, kind: uint32): T = - case kind - of 000, 200, 300, 400, 429, 503: - HistoryErrorKind(kind) - else: - HistoryErrorKind.UNKNOWN - -proc `$`*(err: HistoryError): string = - case err.kind - of HistoryErrorKind.PEER_DIAL_FAILURE: - "PEER_DIAL_FAILURE: " & err.address - of HistoryErrorKind.BAD_RESPONSE: - "BAD_RESPONSE: " & err.cause - of HistoryErrorKind.BAD_REQUEST: - "BAD_REQUEST: " & err.cause - of HistoryErrorKind.TOO_MANY_REQUESTS: - "TOO_MANY_REQUESTS" - of HistoryErrorKind.SERVICE_UNAVAILABLE: - "SERVICE_UNAVAILABLE" - of HistoryErrorKind.UNKNOWN: - "UNKNOWN" - -proc checkHistCursor*(self: HistoryCursor): Result[void, HistoryError] = - if self.pubsubTopic.len == 0: - return err(HistoryError(kind: BAD_REQUEST, cause: "empty pubsubTopic")) - if self.senderTime == 0: - return err(HistoryError(kind: BAD_REQUEST, cause: "invalid senderTime")) - if self.storeTime == 0: - return err(HistoryError(kind: BAD_REQUEST, cause: "invalid storeTime")) - if self.digest.data.all( - proc(x: byte): bool = - x == 0 - ): - return err(HistoryError(kind: BAD_REQUEST, cause: "empty digest")) - return ok() diff --git a/waku/waku_store_legacy/protocol.nim b/waku/waku_store_legacy/protocol.nim deleted file mode 100644 index 058bcbe78..000000000 --- a/waku/waku_store_legacy/protocol.nim +++ /dev/null @@ -1,191 +0,0 @@ -## Waku Store protocol for historical messaging support. -## See spec for more details: -## https://github.com/vacp2p/specs/blob/master/specs/waku/v2/waku-store.md -{.push raises: [].} - -import - std/[options, times], - results, - chronicles, - chronos, - bearssl/rand, - libp2p/crypto/crypto, - libp2p/protocols/protocol, - libp2p/protobuf/minprotobuf, - libp2p/stream/connection, - metrics -import - ../waku_core, - ../node/peer_manager, - ./common, - ./rpc, - ./rpc_codec, - ./protocol_metrics, - ../common/rate_limit/request_limiter - -logScope: - topics = "waku legacy store" - -type HistoryQueryHandler* = - proc(req: HistoryQuery): Future[HistoryResult] {.async, gcsafe.} - -type WakuStore* = ref object of LPProtocol - peerManager: PeerManager - rng: ref rand.HmacDrbgContext - queryHandler*: HistoryQueryHandler - requestRateLimiter*: RequestRateLimiter - -## Protocol - -type StoreResp = tuple[resp: seq[byte], requestId: string] - -proc handleLegacyQueryRequest( - self: WakuStore, requestor: PeerId, raw_request: seq[byte] -): Future[StoreResp] {.async.} = - let decodeRes = HistoryRPC.decode(raw_request) - if decodeRes.isErr(): - error "failed to decode rpc", peerId = requestor, error = $decodeRes.error - waku_legacy_store_errors.inc(labelValues = [decodeRpcFailure]) - return (newSeq[byte](), "failed to decode rpc") - - let reqRpc = decodeRes.value - - if reqRpc.query.isNone(): - error "empty query rpc", peerId = requestor, requestId = reqRpc.requestId - waku_legacy_store_errors.inc(labelValues = [emptyRpcQueryFailure]) - return (newSeq[byte](), "empty query rpc") - - let requestId = reqRpc.requestId - var request = reqRpc.query.get().toAPI() - request.requestId = requestId - - info "received history query", - peerId = requestor, requestId = requestId, query = request - waku_legacy_store_queries.inc() - - var responseRes: HistoryResult - try: - responseRes = await self.queryHandler(request) - except Exception: - error "history query failed", - peerId = requestor, requestId = requestId, error = getCurrentExceptionMsg() - - let error = HistoryError(kind: HistoryErrorKind.UNKNOWN).toRPC() - let response = HistoryResponseRPC(error: error) - return ( - HistoryRPC(requestId: requestId, response: some(response)).encode().buffer, - requestId, - ) - - if responseRes.isErr(): - error "history query failed", - peerId = requestor, requestId = requestId, error = responseRes.error - - let response = responseRes.toRPC() - return ( - HistoryRPC(requestId: requestId, response: some(response)).encode().buffer, - requestId, - ) - - let response = responseRes.toRPC() - - info "sending history response", - peerId = requestor, requestId = requestId, messages = response.messages.len - - return ( - HistoryRPC(requestId: requestId, response: some(response)).encode().buffer, - requestId, - ) - -proc initProtocolHandler(ws: WakuStore) = - let rejectResponseBuf = HistoryRPC( - ## We will not copy and decode RPC buffer from stream only for requestId - ## in reject case as it is comparably too expensive and opens possible - ## attack surface - requestId: "N/A", - response: some( - HistoryResponseRPC( - error: HistoryError(kind: HistoryErrorKind.TOO_MANY_REQUESTS).toRPC() - ) - ), - ).encode().buffer - - proc handler(conn: Connection, proto: string) {.async: (raises: [CancelledError]).} = - var successfulQuery = false ## only consider the correct queries in metrics - var resBuf: StoreResp - var queryDuration: float - - defer: - await conn.closeWithEof() - - ws.requestRateLimiter.checkUsageLimit(WakuLegacyStoreCodec, conn): - let readRes = catch: - await conn.readLp(DefaultMaxRpcSize.int) - - let reqBuf = readRes.valueOr: - error "Connection read error", error = error.msg - return - - waku_service_network_bytes.inc( - amount = reqBuf.len().int64, labelValues = [WakuLegacyStoreCodec, "in"] - ) - - let queryStartTime = getTime().toUnixFloat() - try: - resBuf = await ws.handleLegacyQueryRequest(conn.peerId, reqBuf) - except CatchableError: - error "legacy store query handler failed", - remote_peer_id = conn.peerId, error = getCurrentExceptionMsg() - return - - queryDuration = getTime().toUnixFloat() - queryStartTime - waku_legacy_store_time_seconds.set(queryDuration, ["query-db-time"]) - successfulQuery = true - do: - info "Legacy store query request rejected due rate limit exceeded", - peerId = conn.peerId, limit = $ws.requestRateLimiter.setting - resBuf = (rejectResponseBuf, "rejected") - - let writeRespStartTime = getTime().toUnixFloat() - let writeRes = catch: - await conn.writeLp(resBuf.resp) - - if writeRes.isErr(): - error "Connection write error", error = writeRes.error.msg - return - - if successfulQuery: - let writeDuration = getTime().toUnixFloat() - writeRespStartTime - waku_legacy_store_time_seconds.set(writeDuration, ["send-store-resp-time"]) - info "after sending response", - requestId = resBuf.requestId, - queryDurationSecs = queryDuration, - writeStreamDurationSecs = writeDuration - - waku_service_network_bytes.inc( - amount = resBuf.resp.len().int64, labelValues = [WakuLegacyStoreCodec, "out"] - ) - - ws.handler = handler - ws.codec = WakuLegacyStoreCodec - -proc new*( - T: type WakuStore, - peerManager: PeerManager, - rng: ref rand.HmacDrbgContext, - queryHandler: HistoryQueryHandler, - rateLimitSetting: Option[RateLimitSetting] = none[RateLimitSetting](), -): T = - # Raise a defect if history query handler is nil - if queryHandler.isNil(): - raise newException(NilAccessDefect, "history query handler is nil") - - let ws = WakuStore( - rng: rng, - peerManager: peerManager, - queryHandler: queryHandler, - requestRateLimiter: newRequestRateLimiter(rateLimitSetting), - ) - ws.initProtocolHandler() - setServiceLimitMetric(WakuLegacyStoreCodec, rateLimitSetting) - ws diff --git a/waku/waku_store_legacy/protocol_metrics.nim b/waku/waku_store_legacy/protocol_metrics.nim deleted file mode 100644 index 45a848998..000000000 --- a/waku/waku_store_legacy/protocol_metrics.nim +++ /dev/null @@ -1,21 +0,0 @@ -{.push raises: [].} - -import metrics - -declarePublicCounter waku_legacy_store_errors, - "number of legacy store protocol errors", ["type"] -declarePublicCounter waku_legacy_store_queries, - "number of legacy store queries received" - -## "query-db-time" phase considers the time when node performs the query to the database. -## "send-store-resp-time" phase is the time when node writes the store response to the store-client. -declarePublicGauge waku_legacy_store_time_seconds, - "Time in seconds spent by each store phase", labels = ["phase"] - -# Error types (metric label values) -const - dialFailure* = "dial_failure_legacy" - decodeRpcFailure* = "decode_rpc_failure_legacy" - peerNotFoundFailure* = "peer_not_found_failure_legacy" - emptyRpcQueryFailure* = "empty_rpc_query_failure_legacy" - emptyRpcResponseFailure* = "empty_rpc_response_failure_legacy" diff --git a/waku/waku_store_legacy/rpc.nim b/waku/waku_store_legacy/rpc.nim deleted file mode 100644 index bce3e60cd..000000000 --- a/waku/waku_store_legacy/rpc.nim +++ /dev/null @@ -1,223 +0,0 @@ -{.push raises: [].} - -import std/[options, sequtils], results -import ../waku_core, ../common/paging, ./common - -## Wire protocol - -const HistoryQueryDirectionDefaultValue = default(type HistoryQuery.direction) - -type PagingIndexRPC* = object - ## This type contains the description of an Index used in the pagination of WakuMessages - pubsubTopic*: PubsubTopic - senderTime*: Timestamp # the time at which the message is generated - receiverTime*: Timestamp - digest*: MessageDigest # calculated over payload and content topic - -proc `==`*(x, y: PagingIndexRPC): bool = - ## receiverTime plays no role in index equality - (x.senderTime == y.senderTime) and (x.digest == y.digest) and - (x.pubsubTopic == y.pubsubTopic) - -proc compute*( - T: type PagingIndexRPC, - msg: WakuMessage, - receivedTime: Timestamp, - pubsubTopic: PubsubTopic, -): T = - ## Takes a WakuMessage with received timestamp and returns its Index. - let - digest = computeDigest(msg) - senderTime = msg.timestamp - - PagingIndexRPC( - pubsubTopic: pubsubTopic, - senderTime: senderTime, - receiverTime: receivedTime, - digest: digest, - ) - -type PagingInfoRPC* = object - ## This type holds the information needed for the pagination - pageSize*: Option[uint64] - cursor*: Option[PagingIndexRPC] - direction*: Option[PagingDirection] - -type - HistoryContentFilterRPC* = object - contentTopic*: ContentTopic - - HistoryQueryRPC* = object - contentFilters*: seq[HistoryContentFilterRPC] - pubsubTopic*: Option[PubsubTopic] - pagingInfo*: Option[PagingInfoRPC] - startTime*: Option[int64] - endTime*: Option[int64] - - HistoryResponseErrorRPC* {.pure.} = enum - ## HistoryResponseErrorRPC contains error message to inform the querying node about - ## the state of its request - NONE = uint32(0) - INVALID_CURSOR = uint32(1) - TOO_MANY_REQUESTS = uint32(429) - SERVICE_UNAVAILABLE = uint32(503) - - HistoryResponseRPC* = object - messages*: seq[WakuMessage] - pagingInfo*: Option[PagingInfoRPC] - error*: HistoryResponseErrorRPC - - HistoryRPC* = object - requestId*: string - query*: Option[HistoryQueryRPC] - response*: Option[HistoryResponseRPC] - -proc parse*(T: type HistoryResponseErrorRPC, kind: uint32): T = - case kind - of 0, 1, 429, 503: - cast[HistoryResponseErrorRPC](kind) - else: - # TODO: Improve error variants/move to satus codes - HistoryResponseErrorRPC.INVALID_CURSOR - -## Wire protocol type mappings - -proc toRPC*(cursor: HistoryCursor): PagingIndexRPC {.gcsafe.} = - PagingIndexRPC( - pubsubTopic: cursor.pubsubTopic, - senderTime: cursor.senderTime, - receiverTime: cursor.storeTime, - digest: cursor.digest, - ) - -proc toAPI*(rpc: PagingIndexRPC): HistoryCursor = - HistoryCursor( - pubsubTopic: rpc.pubsubTopic, - senderTime: rpc.senderTime, - storeTime: rpc.receiverTime, - digest: rpc.digest, - ) - -proc toRPC*(query: HistoryQuery): HistoryQueryRPC = - var rpc = HistoryQueryRPC() - - rpc.contentFilters = - query.contentTopics.mapIt(HistoryContentFilterRPC(contentTopic: it)) - - rpc.pubsubTopic = query.pubsubTopic - - rpc.pagingInfo = block: - if query.cursor.isNone() and query.pageSize == default(type query.pageSize) and - query.direction == HistoryQueryDirectionDefaultValue: - none(PagingInfoRPC) - else: - let - pageSize = some(query.pageSize) - cursor = query.cursor.map(toRPC) - direction = some(query.direction) - - some(PagingInfoRPC(pageSize: pageSize, cursor: cursor, direction: direction)) - - rpc.startTime = query.startTime - rpc.endTime = query.endTime - - rpc - -proc toAPI*(rpc: HistoryQueryRPC): HistoryQuery = - let - pubsubTopic = rpc.pubsubTopic - - contentTopics = rpc.contentFilters.mapIt(it.contentTopic) - - cursor = - if rpc.pagingInfo.isNone() or rpc.pagingInfo.get().cursor.isNone(): - none(HistoryCursor) - else: - rpc.pagingInfo.get().cursor.map(toAPI) - - startTime = rpc.startTime - - endTime = rpc.endTime - - pageSize = - if rpc.pagingInfo.isNone() or rpc.pagingInfo.get().pageSize.isNone(): - 0'u64 - else: - rpc.pagingInfo.get().pageSize.get() - - direction = - if rpc.pagingInfo.isNone() or rpc.pagingInfo.get().direction.isNone(): - HistoryQueryDirectionDefaultValue - else: - rpc.pagingInfo.get().direction.get() - - HistoryQuery( - pubsubTopic: pubsubTopic, - contentTopics: contentTopics, - cursor: cursor, - startTime: startTime, - endTime: endTime, - pageSize: pageSize, - direction: direction, - ) - -proc toRPC*(err: HistoryError): HistoryResponseErrorRPC = - # TODO: Better error mappings/move to error codes - case err.kind - of HistoryErrorKind.BAD_REQUEST: - # TODO: Respond aksi with the reason - HistoryResponseErrorRPC.INVALID_CURSOR - of HistoryErrorKind.TOO_MANY_REQUESTS: - HistoryResponseErrorRPC.TOO_MANY_REQUESTS - of HistoryErrorKind.SERVICE_UNAVAILABLE: - HistoryResponseErrorRPC.SERVICE_UNAVAILABLE - else: - HistoryResponseErrorRPC.INVALID_CURSOR - -proc toAPI*(err: HistoryResponseErrorRPC): HistoryError = - # TODO: Better error mappings/move to error codes - case err - of HistoryResponseErrorRPC.INVALID_CURSOR: - HistoryError(kind: HistoryErrorKind.BAD_REQUEST, cause: "invalid cursor") - of HistoryResponseErrorRPC.TOO_MANY_REQUESTS: - HistoryError(kind: HistoryErrorKind.TOO_MANY_REQUESTS) - of HistoryResponseErrorRPC.SERVICE_UNAVAILABLE: - HistoryError(kind: HistoryErrorKind.SERVICE_UNAVAILABLE) - else: - HistoryError(kind: HistoryErrorKind.UNKNOWN) - -proc toRPC*(res: HistoryResult): HistoryResponseRPC = - if res.isErr(): - let error = res.error.toRPC() - - HistoryResponseRPC(error: error) - else: - let resp = res.get() - - let - messages = resp.messages - - pagingInfo = block: - if resp.cursor.isNone(): - none(PagingInfoRPC) - else: - some(PagingInfoRPC(cursor: resp.cursor.map(toRPC))) - - error = HistoryResponseErrorRPC.NONE - - HistoryResponseRPC(messages: messages, pagingInfo: pagingInfo, error: error) - -proc toAPI*(rpc: HistoryResponseRPC): HistoryResult = - if rpc.error != HistoryResponseErrorRPC.NONE: - err(rpc.error.toAPI()) - else: - let - messages = rpc.messages - - cursor = - if rpc.pagingInfo.isNone(): - none(HistoryCursor) - else: - rpc.pagingInfo.get().cursor.map(toAPI) - - ok(HistoryResponse(messages: messages, cursor: cursor)) diff --git a/waku/waku_store_legacy/rpc_codec.nim b/waku/waku_store_legacy/rpc_codec.nim deleted file mode 100644 index f9c518e83..000000000 --- a/waku/waku_store_legacy/rpc_codec.nim +++ /dev/null @@ -1,255 +0,0 @@ -{.push raises: [].} - -import std/options, nimcrypto/hash -import ../common/[protobuf, paging], ../waku_core, ./common, ./rpc - -const DefaultMaxRpcSize* = -1 - -## Pagination - -proc encode*(index: PagingIndexRPC): ProtoBuffer = - ## Encode an Index object into a ProtoBuffer - ## returns the resultant ProtoBuffer - var pb = initProtoBuffer() - - pb.write3(1, index.digest.data) - pb.write3(2, zint64(index.receiverTime)) - pb.write3(3, zint64(index.senderTime)) - pb.write3(4, index.pubsubTopic) - pb.finish3() - - pb - -proc decode*(T: type PagingIndexRPC, buffer: seq[byte]): ProtobufResult[T] = - ## creates and returns an Index object out of buffer - var rpc = PagingIndexRPC() - let pb = initProtoBuffer(buffer) - - var data: seq[byte] - if not ?pb.getField(1, data): - return err(ProtobufError.missingRequiredField("digest")) - else: - var digest = MessageDigest() - for count, b in data: - digest.data[count] = b - - rpc.digest = digest - - var receiverTime: zint64 - if not ?pb.getField(2, receiverTime): - return err(ProtobufError.missingRequiredField("receiver_time")) - else: - rpc.receiverTime = int64(receiverTime) - - var senderTime: zint64 - if not ?pb.getField(3, senderTime): - return err(ProtobufError.missingRequiredField("sender_time")) - else: - rpc.senderTime = int64(senderTime) - - var pubsubTopic: string - if not ?pb.getField(4, pubsubTopic): - return err(ProtobufError.missingRequiredField("pubsub_topic")) - else: - rpc.pubsubTopic = pubsubTopic - - ok(rpc) - -proc encode*(rpc: PagingInfoRPC): ProtoBuffer = - ## Encodes a PagingInfo object into a ProtoBuffer - ## returns the resultant ProtoBuffer - var pb = initProtoBuffer() - - pb.write3(1, rpc.pageSize) - pb.write3(2, rpc.cursor.map(encode)) - pb.write3( - 3, - rpc.direction.map( - proc(d: PagingDirection): uint32 = - uint32(ord(d)) - ), - ) - pb.finish3() - - pb - -proc decode*(T: type PagingInfoRPC, buffer: seq[byte]): ProtobufResult[T] = - ## creates and returns a PagingInfo object out of buffer - var rpc = PagingInfoRPC() - let pb = initProtoBuffer(buffer) - - var pageSize: uint64 - if not ?pb.getField(1, pageSize): - rpc.pageSize = none(uint64) - else: - rpc.pageSize = some(pageSize) - - var cursorBuffer: seq[byte] - if not ?pb.getField(2, cursorBuffer): - rpc.cursor = none(PagingIndexRPC) - else: - let cursor = ?PagingIndexRPC.decode(cursorBuffer) - rpc.cursor = some(cursor) - - var direction: uint32 - if not ?pb.getField(3, direction): - rpc.direction = none(PagingDirection) - else: - rpc.direction = some(PagingDirection(direction)) - - ok(rpc) - -## Wire protocol - -proc encode*(rpc: HistoryContentFilterRPC): ProtoBuffer = - var pb = initProtoBuffer() - - pb.write3(1, rpc.contentTopic) - pb.finish3() - - pb - -proc decode*(T: type HistoryContentFilterRPC, buffer: seq[byte]): ProtobufResult[T] = - let pb = initProtoBuffer(buffer) - - var contentTopic: ContentTopic - if not ?pb.getField(1, contentTopic): - return err(ProtobufError.missingRequiredField("content_topic")) - ok(HistoryContentFilterRPC(contentTopic: contentTopic)) - -proc encode*(rpc: HistoryQueryRPC): ProtoBuffer = - var pb = initProtoBuffer() - pb.write3(2, rpc.pubsubTopic) - - for filter in rpc.contentFilters: - pb.write3(3, filter.encode()) - - pb.write3(4, rpc.pagingInfo.map(encode)) - pb.write3( - 5, - rpc.startTime.map( - proc(time: int64): zint64 = - zint64(time) - ), - ) - pb.write3( - 6, - rpc.endTime.map( - proc(time: int64): zint64 = - zint64(time) - ), - ) - pb.finish3() - - pb - -proc decode*(T: type HistoryQueryRPC, buffer: seq[byte]): ProtobufResult[T] = - var rpc = HistoryQueryRPC() - let pb = initProtoBuffer(buffer) - - var pubsubTopic: string - if not ?pb.getField(2, pubsubTopic): - rpc.pubsubTopic = none(string) - else: - rpc.pubsubTopic = some(pubsubTopic) - - var buffs: seq[seq[byte]] - if not ?pb.getRepeatedField(3, buffs): - rpc.contentFilters = @[] - else: - for pb in buffs: - let filter = ?HistoryContentFilterRPC.decode(pb) - rpc.contentFilters.add(filter) - - var pagingInfoBuffer: seq[byte] - if not ?pb.getField(4, pagingInfoBuffer): - rpc.pagingInfo = none(PagingInfoRPC) - else: - let pagingInfo = ?PagingInfoRPC.decode(pagingInfoBuffer) - rpc.pagingInfo = some(pagingInfo) - - var startTime: zint64 - if not ?pb.getField(5, startTime): - rpc.startTime = none(int64) - else: - rpc.startTime = some(int64(startTime)) - - var endTime: zint64 - if not ?pb.getField(6, endTime): - rpc.endTime = none(int64) - else: - rpc.endTime = some(int64(endTime)) - - ok(rpc) - -proc encode*(response: HistoryResponseRPC): ProtoBuffer = - var pb = initProtoBuffer() - - for rpc in response.messages: - pb.write3(2, rpc.encode()) - - pb.write3(3, response.pagingInfo.map(encode)) - pb.write3(4, uint32(ord(response.error))) - pb.finish3() - - pb - -proc decode*(T: type HistoryResponseRPC, buffer: seq[byte]): ProtobufResult[T] = - var rpc = HistoryResponseRPC() - let pb = initProtoBuffer(buffer) - - var messages: seq[seq[byte]] - if ?pb.getRepeatedField(2, messages): - for pb in messages: - let message = ?WakuMessage.decode(pb) - rpc.messages.add(message) - else: - rpc.messages = @[] - - var pagingInfoBuffer: seq[byte] - if ?pb.getField(3, pagingInfoBuffer): - let pagingInfo = ?PagingInfoRPC.decode(pagingInfoBuffer) - rpc.pagingInfo = some(pagingInfo) - else: - rpc.pagingInfo = none(PagingInfoRPC) - - var error: uint32 - if not ?pb.getField(4, error): - return err(ProtobufError.missingRequiredField("error")) - else: - rpc.error = HistoryResponseErrorRPC.parse(error) - - ok(rpc) - -proc encode*(rpc: HistoryRPC): ProtoBuffer = - var pb = initProtoBuffer() - - pb.write3(1, rpc.requestId) - pb.write3(2, rpc.query.map(encode)) - pb.write3(3, rpc.response.map(encode)) - pb.finish3() - - pb - -proc decode*(T: type HistoryRPC, buffer: seq[byte]): ProtobufResult[T] = - var rpc = HistoryRPC() - let pb = initProtoBuffer(buffer) - - if not ?pb.getField(1, rpc.requestId): - return err(ProtobufError.missingRequiredField("request_id")) - - var queryBuffer: seq[byte] - if not ?pb.getField(2, queryBuffer): - rpc.query = none(HistoryQueryRPC) - else: - let query = ?HistoryQueryRPC.decode(queryBuffer) - rpc.query = some(query) - - var responseBuffer: seq[byte] - if not ?pb.getField(3, responseBuffer): - rpc.response = none(HistoryResponseRPC) - else: - let response = ?HistoryResponseRPC.decode(responseBuffer) - rpc.response = some(response) - - ok(rpc) diff --git a/waku/waku_store_legacy/self_req_handler.nim b/waku/waku_store_legacy/self_req_handler.nim deleted file mode 100644 index e465d9e5b..000000000 --- a/waku/waku_store_legacy/self_req_handler.nim +++ /dev/null @@ -1,31 +0,0 @@ -## -## This file is aimed to attend the requests that come directly -## from the 'self' node. It is expected to attend the store requests that -## come from REST-store endpoint when those requests don't indicate -## any store-peer address. -## -## Notice that the REST-store requests normally assume that the REST -## server is acting as a store-client. In this module, we allow that -## such REST-store node can act as store-server as well by retrieving -## its own stored messages. The typical use case for that is when -## using `nwaku-compose`, which spawn a Waku node connected to a local -## database, and the user is interested in retrieving the messages -## stored by that local store node. -## - -import results, chronos -import ./protocol, ./common - -proc handleSelfStoreRequest*( - self: WakuStore, histQuery: HistoryQuery -): Future[WakuStoreResult[HistoryResponse]] {.async.} = - ## Handles the store requests made by the node to itself. - ## Normally used in REST-store requests - - try: - let resp: HistoryResponse = (await self.queryHandler(histQuery)).valueOr: - return err("error in handleSelfStoreRequest: " & $error) - - return WakuStoreResult[HistoryResponse].ok(resp) - except Exception: - return err("exception in handleSelfStoreRequest: " & getCurrentExceptionMsg()) diff --git a/waku/waku_store_sync/reconciliation.nim b/waku/waku_store_sync/reconciliation.nim index 8b196a3e9..b18251fff 100644 --- a/waku/waku_store_sync/reconciliation.nim +++ b/waku/waku_store_sync/reconciliation.nim @@ -79,7 +79,8 @@ proc messageIngress*( let id = SyncID(time: msg.timestamp, hash: msgHash) self.storage.insert(id, pubsubTopic, msg.contentTopic).isOkOr: - error "failed to insert new message", msg_hash = $id.hash.toHex(), error = $error + error "failed to insert new message", + msg_hash = byteutils.toHex(id.hash), error = $error proc messageIngress*( self: SyncReconciliation, @@ -87,7 +88,7 @@ proc messageIngress*( pubsubTopic: PubsubTopic, msg: WakuMessage, ) = - trace "message ingress", msg_hash = msgHash.toHex(), msg = msg + trace "message ingress", msg_hash = byteutils.toHex(msgHash), msg = msg if msg.ephemeral: return @@ -95,7 +96,8 @@ proc messageIngress*( let id = SyncID(time: msg.timestamp, hash: msgHash) self.storage.insert(id, pubsubTopic, msg.contentTopic).isOkOr: - error "failed to insert new message", msg_hash = $id.hash.toHex(), error = $error + error "failed to insert new message", + msg_hash = byteutils.toHex(id.hash), error = $error proc messageIngress*( self: SyncReconciliation, @@ -104,7 +106,8 @@ proc messageIngress*( contentTopic: ContentTopic, ) = self.storage.insert(id, pubsubTopic, contentTopic).isOkOr: - error "failed to insert new message", msg_hash = $id.hash.toHex(), error = $error + error "failed to insert new message", + msg_hash = byteutils.toHex(id.hash), error = $error proc preProcessPayload( self: SyncReconciliation, payload: RangesData @@ -142,7 +145,7 @@ proc preProcessPayload( # convert to skip range before processing for i in 0 ..< payload.ranges.len: let rangeType = payload.ranges[i][1] - if rangeType != RangeType.Skip: + if rangeType == RangeType.Skip: continue let upperBound = payload.ranges[i][0].b.time @@ -230,10 +233,9 @@ proc processRequest( let writeRes = catch: await conn.writeLP(rawPayload) - if writeRes.isErr(): + writeRes.isOkOr: await conn.close() - return - err("remote " & $conn.peerId & " connection write error: " & writeRes.error.msg) + return err("remote " & $conn.peerId & " connection write error: " & error.msg) trace "sync payload sent", local = self.peerManager.switch.peerInfo.peerId, @@ -286,11 +288,9 @@ proc initiate( let writeRes = catch: await connection.writeLP(sendPayload) - if writeRes.isErr(): + writeRes.isOkOr: await connection.close() - return err( - "remote " & $connection.peerId & " connection write error: " & writeRes.error.msg - ) + return err("remote " & $connection.peerId & " connection write error: " & error.msg) trace "sync payload sent", local = self.peerManager.switch.peerInfo.peerId, @@ -468,7 +468,7 @@ proc idsReceiverLoop(self: SyncReconciliation) {.async.} = self.messageIngress(id, pubsub, content) -proc start*(self: SyncReconciliation) = +method start*(self: SyncReconciliation) {.async: (raises: [CancelledError]).} = if self.started: return @@ -484,13 +484,16 @@ proc start*(self: SyncReconciliation) = info "Store Sync Reconciliation protocol started" -proc stop*(self: SyncReconciliation) = - if self.syncInterval > ZeroDuration: - self.periodicSyncFut.cancelSoon() +method stop*(self: SyncReconciliation) {.async: (raises: []).} = + defer: + self.started = false if self.syncInterval > ZeroDuration: - self.periodicPruneFut.cancelSoon() + await self.periodicSyncFut.cancelAndWait() - self.idsReceiverFut.cancelSoon() + if self.syncInterval > ZeroDuration: + await self.periodicPruneFut.cancelAndWait() + + await self.idsReceiverFut.cancelAndWait() info "Store Sync Reconciliation protocol stopped" diff --git a/waku/waku_store_sync/transfer.nim b/waku/waku_store_sync/transfer.nim index 5e3e376d1..5d20afb18 100644 --- a/waku/waku_store_sync/transfer.nim +++ b/waku/waku_store_sync/transfer.nim @@ -58,9 +58,8 @@ proc sendMessage( let writeRes = catch: await conn.writeLP(rawPayload) - if writeRes.isErr(): - return - err("remote " & $conn.peerId & " connection write error: " & writeRes.error.msg) + writeRes.isOkOr: + return err("remote [" & $conn.peerId & "] connection write error: " & error.msg) total_transfer_messages_exchanged.inc(labelValues = [Sending]) @@ -218,7 +217,7 @@ proc new*( return transfer -proc start*(self: SyncTransfer) = +method start*(self: SyncTransfer) {.async: (raises: [CancelledError]).} = if self.started: return @@ -229,10 +228,11 @@ proc start*(self: SyncTransfer) = info "Store Sync Transfer protocol started" -proc stop*(self: SyncTransfer) = - self.started = false +method stop*(self: SyncTransfer) {.async: (raises: []).} = + defer: + self.started = false - self.localWantsRxFut.cancelSoon() - self.remoteNeedsRxFut.cancelSoon() + await self.localWantsRxFut.cancelAndWait() + await self.remoteNeedsRxFut.cancelAndWait() info "Store Sync Transfer protocol stopped"