From 54f4ad8fa2ed452df670e25213a7fa34e5cc5432 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Tue, 2 Dec 2025 11:00:26 -0300 Subject: [PATCH 01/11] fix: fix .github waku-org/ --> logos-messaging/ (#3653) * fix: fix .github waku-org/ --> logos-messaging/ * bump CI tests timeout 45 --> 90 minutes * fix .gitmodules waku-org --> logos-messaging --- .github/ISSUE_TEMPLATE/prepare_beta_release.md | 14 +++++++------- .github/ISSUE_TEMPLATE/prepare_full_release.md | 16 ++++++++-------- .github/workflows/ci.yml | 14 +++++++------- .github/workflows/pre-release.yml | 10 +++++----- .gitmodules | 2 +- 5 files changed, 28 insertions(+), 28 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/prepare_beta_release.md b/.github/ISSUE_TEMPLATE/prepare_beta_release.md index 270f6a8e6..9afaefbd1 100644 --- a/.github/ISSUE_TEMPLATE/prepare_beta_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_beta_release.md @@ -10,7 +10,7 @@ assignees: '' ### Items to complete @@ -34,10 +34,10 @@ All items below are to be completed by the owner of the given release. - [ ] **Proceed with release** - [ ] Assign a final release tag (`v0.X.0-beta`) to the same commit that contains the validated release-candidate tag (e.g. `v0.X.0-beta-rc.N`) and submit a PR from the release branch to `master`. - - [ ] Update [nwaku-compose](https://github.com/waku-org/nwaku-compose) and [waku-simulator](https://github.com/waku-org/waku-simulator) according to the new release. - - [ ] Bump nwaku dependency in [waku-rust-bindings](https://github.com/waku-org/waku-rust-bindings) and make sure all examples and tests work. - - [ ] Bump nwaku dependency in [waku-go-bindings](https://github.com/waku-org/waku-go-bindings) and make sure all tests work. - - [ ] Create GitHub release (https://github.com/waku-org/nwaku/releases). + - [ ] Update [nwaku-compose](https://github.com/logos-messaging/nwaku-compose) and [waku-simulator](https://github.com/logos-messaging/waku-simulator) according to the new release. + - [ ] Bump nwaku dependency in [waku-rust-bindings](https://github.com/logos-messaging/waku-rust-bindings) and make sure all examples and tests work. + - [ ] Bump nwaku dependency in [waku-go-bindings](https://github.com/logos-messaging/waku-go-bindings) and make sure all tests work. + - [ ] Create GitHub release (https://github.com/logos-messaging/nwaku/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. - [ ] **Promote release to fleets** @@ -47,8 +47,8 @@ All items below are to be completed by the owner of the given release. ### Links -- [Release process](https://github.com/waku-org/nwaku/blob/master/docs/contributors/release-process.md) -- [Release notes](https://github.com/waku-org/nwaku/blob/master/CHANGELOG.md) +- [Release process](https://github.com/logos-messaging/nwaku/blob/master/docs/contributors/release-process.md) +- [Release notes](https://github.com/logos-messaging/nwaku/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/) diff --git a/.github/ISSUE_TEMPLATE/prepare_full_release.md b/.github/ISSUE_TEMPLATE/prepare_full_release.md index 18c668d16..314146f60 100644 --- a/.github/ISSUE_TEMPLATE/prepare_full_release.md +++ b/.github/ISSUE_TEMPLATE/prepare_full_release.md @@ -10,7 +10,7 @@ assignees: '' ### Items to complete @@ -54,11 +54,11 @@ All items below are to be completed by the owner of the given release. - [ ] **Proceed with release** - - [ ] Assign a final release tag (`v0.X.0`) to the same commit that contains the validated release-candidate tag (e.g. `v0.X.0`). - - [ ] Update [nwaku-compose](https://github.com/waku-org/nwaku-compose) and [waku-simulator](https://github.com/waku-org/waku-simulator) according to the new release. - - [ ] Bump nwaku dependency in [waku-rust-bindings](https://github.com/waku-org/waku-rust-bindings) and make sure all examples and tests work. - - [ ] Bump nwaku dependency in [waku-go-bindings](https://github.com/waku-org/waku-go-bindings) and make sure all tests work. - - [ ] Create GitHub release (https://github.com/waku-org/nwaku/releases). + - [ ] Assign a final release tag (`v0.X.0`) to the same commit that contains the validated release-candidate tag (e.g. `v0.X.0`). + - [ ] Update [nwaku-compose](https://github.com/logos-messaging/nwaku-compose) and [waku-simulator](https://github.com/logos-messaging/waku-simulator) according to the new release. + - [ ] Bump nwaku dependency in [waku-rust-bindings](https://github.com/logos-messaging/waku-rust-bindings) and make sure all examples and tests work. + - [ ] Bump nwaku dependency in [waku-go-bindings](https://github.com/logos-messaging/waku-go-bindings) and make sure all tests work. + - [ ] Create GitHub release (https://github.com/logos-messaging/nwaku/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. - [ ] **Promote release to fleets** @@ -67,8 +67,8 @@ All items below are to be completed by the owner of the given release. ### Links -- [Release process](https://github.com/waku-org/nwaku/blob/master/docs/contributors/release-process.md) -- [Release notes](https://github.com/waku-org/nwaku/blob/master/CHANGELOG.md) +- [Release process](https://github.com/logos-messaging/nwaku/blob/master/docs/contributors/release-process.md) +- [Release notes](https://github.com/logos-messaging/nwaku/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/) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5cf64b66a..12c1abd6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: - name: Build binaries run: make V=1 QUICK_AND_DIRTY_COMPILER=1 all tools - + build-windows: needs: changes if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' }} @@ -94,7 +94,7 @@ jobs: matrix: os: [ubuntu-22.04, macos-15] runs-on: ${{ matrix.os }} - timeout-minutes: 45 + timeout-minutes: 90 name: test-${{ matrix.os }} steps: @@ -121,7 +121,7 @@ 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 @@ -132,12 +132,12 @@ jobs: 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: logos-messaging/nwaku/.github/workflows/container-image.yml@master 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/waku-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_0.0.1 with: node_nwaku: ${{ needs.build-docker-image.outputs.image }} @@ -145,14 +145,14 @@ jobs: js-waku-node: needs: build-docker-image - uses: waku-org/js-waku/.github/workflows/test-node.yml@master + uses: logos-messaging/js-waku/.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/js-waku/.github/workflows/test-node.yml@master with: nim_wakunode_image: ${{ needs.build-docker-image.outputs.image }} test_type: node-optional diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index fe108e616..380ec755f 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 @@ -91,14 +91,14 @@ jobs: build-docker-image: needs: tag-name - uses: waku-org/nwaku/.github/workflows/container-image.yml@master + uses: logos-messaging/nwaku/.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/js-waku/.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/js-waku/.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/nwaku/issues/\1)@g' > release_notes.md sed -i "s/^## .*/Generated at $(date)/" release_notes.md diff --git a/.gitmodules b/.gitmodules index b7e52550a..93a3a006f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -181,6 +181,6 @@ 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 From 8c30a8e1bb7469e6184d1ac6289676aec27b719d Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Wed, 3 Dec 2025 11:55:34 +0100 Subject: [PATCH 02/11] Rest store api constraints default page size to 20 and max to 100 (#3602) Co-authored-by: Vishwanath Martur <64204611+vishwamartur@users.noreply.github.com> --- docs/api/rest-api.md | 3 +++ docs/operators/how-to/configure-rest-api.md | 3 ++- waku/rest_api/endpoint/store/client.nim | 2 +- waku/rest_api/endpoint/store/handlers.nim | 8 ++++++++ 4 files changed, 14 insertions(+), 2 deletions(-) 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/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/waku/rest_api/endpoint/store/client.nim b/waku/rest_api/endpoint/store/client.nim index 80939ee25..71ba7610d 100644 --- a/waku/rest_api/endpoint/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/rest_api/endpoint/store/handlers.nim b/waku/rest_api/endpoint/store/handlers.nim index 79724b9d7..7d37191fb 100644 --- a/waku/rest_api/endpoint/store/handlers.nim +++ b/waku/rest_api/endpoint/store/handlers.nim @@ -129,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, From a8590a0a7dd53776bc2fef87149fdb084b58d317 Mon Sep 17 00:00:00 2001 From: Tanya S <120410716+stubbsta@users.noreply.github.com> Date: Thu, 4 Dec 2025 10:26:18 +0200 Subject: [PATCH 03/11] chore: Add gasprice overflow check (#3636) * Check for gasPrice overflow * use trace for logging and update comments * Update log level for gas price logs --- .../group_manager/on_chain/group_manager.nim | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 db68b2289..e8af61682 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 @@ -229,7 +229,18 @@ method register*( var gasPrice: int g.retryWrapper(gasPrice, "Failed to get gas price"): - int(await ethRpc.provider.eth_gasPrice()) * 2 + let fetchedGasPrice = uint64(await ethRpc.provider.eth_gasPrice()) + ## Multiply by 2 to speed up the transaction + ## Check for overflow when casting to int + if fetchedGasPrice > uint64(high(int) div 2): + warn "Gas price overflow detected, capping at maximum int value", + fetchedGasPrice = fetchedGasPrice, maxInt = high(int) + high(int) + else: + let calculatedGasPrice = int(fetchedGasPrice) * 2 + debug "Gas price calculated", + fetchedGasPrice = fetchedGasPrice, gasPrice = calculatedGasPrice + calculatedGasPrice let idCommitmentHex = identityCredential.idCommitment.inHex() info "identityCredential idCommitmentHex", idCommitment = idCommitmentHex let idCommitment = identityCredential.idCommitment.toUInt256() From 2cf4fe559a0a6a4511cc9da2b69c7935ebc7862f Mon Sep 17 00:00:00 2001 From: Tanya S <120410716+stubbsta@users.noreply.github.com> Date: Mon, 8 Dec 2025 08:29:48 +0200 Subject: [PATCH 04/11] Chore: bump waku-rlnv2-contract-repo commit (#3651) * Bump commit for vendor wakurlnv2contract * Update RLN registration proc for contract updates * add option to runAnvil for state dump or load with optional contract deployment on setup * Code clean up * Upodate rln relay tests to use cached anvil state * Minor updates to utils and new test for anvil state dump * stopAnvil needs to wait for graceful shutdown * configure runAnvil to use load state in other tests * reduce ci timeout * Allow for RunAnvil load state file to be compressed * Fix linting * Change return type of sendMintCall to Futre[void] * Update naming of ci path for interop tests --- .github/workflows/ci.yml | 4 +- tests/node/test_wakunode_legacy_lightpush.nim | 4 +- tests/node/test_wakunode_lightpush.nim | 4 +- ...ployed-contracts-mint-and-approved.json.gz | Bin 0 -> 118346 bytes .../test_rln_contract_deployment.nim | 29 ++ .../test_rln_group_manager_onchain.nim | 4 +- tests/waku_rln_relay/test_waku_rln_relay.nim | 4 +- .../test_wakunode_rln_relay.nim | 4 +- tests/waku_rln_relay/utils_onchain.nim | 249 ++++++++++++++---- tests/wakunode_rest/test_rest_health.nim | 4 +- vendor/waku-rlnv2-contract | 2 +- .../group_manager/on_chain/group_manager.nim | 38 +-- 12 files changed, 259 insertions(+), 87 deletions(-) create mode 100644 tests/waku_rln_relay/anvil_state/state-deployed-contracts-mint-and-approved.json.gz create mode 100644 tests/waku_rln_relay/test_rln_contract_deployment.nim diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12c1abd6d..e3186a007 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,7 +94,7 @@ jobs: matrix: os: [ubuntu-22.04, macos-15] runs-on: ${{ matrix.os }} - timeout-minutes: 90 + timeout-minutes: 45 name: test-${{ matrix.os }} steps: @@ -137,7 +137,7 @@ jobs: nwaku-nwaku-interop-tests: needs: build-docker-image - uses: logos-messaging/waku-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_0.0.1 + uses: logos-messaging/logos-messaging-interop-tests/.github/workflows/nim_waku_PR.yml@SMOKE_TEST_0.0.1 with: node_nwaku: ${{ needs.build-docker-image.outputs.image }} diff --git a/tests/node/test_wakunode_legacy_lightpush.nim b/tests/node/test_wakunode_legacy_lightpush.nim index a51ba60b9..80e623ce4 100644 --- a/tests/node/test_wakunode_legacy_lightpush.nim +++ b/tests/node/test_wakunode_legacy_lightpush.nim @@ -135,8 +135,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)) diff --git a/tests/node/test_wakunode_lightpush.nim b/tests/node/test_wakunode_lightpush.nim index 12bfdddd8..29f72b2cc 100644 --- a/tests/node/test_wakunode_lightpush.nim +++ b/tests/node/test_wakunode_lightpush.nim @@ -135,8 +135,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)) 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 0000000000000000000000000000000000000000..ceb081c77788d7b5a3a933b2d0510303247694a1 GIT binary patch literal 118346 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 z z!PQ=H9UMRgPp^bg#6+nM$#uYewvURinIg7E=RpjI?O)h4Z}0_C+|PHQ+NG=76{ip{ILSg)coeov+bz?ZkOg^7Y< z1EUvFDxQ?hOAs2``nc@2UU4s(V!N2izLx4-lsa7zljyic$+vhqRt58H`cl2H536oK z^g{G^(mQfRjyjs{8Nvmp>2f)Ho1zKP(O6s=i@%cX-t%rt1+)YERuYPvDqjA7;0x5v zsQ;Mvf4dKOD*E%^?}Pf0UuYn>P?P94^xe%>SMYpa!qXoui^BY2e^qI!`nrUB>KdYI zBi1IGPS30N=U4S^WUFWJ8AvnoLH+*Q*GRhk{yZHx)|rnNHbYkw@*jQuI+vp3nDWf3 zJhRe>pE*@otqYaJK+{``ii+VZE>h$2LK{vKKDT{PzG8nb@N+O=QT zY_ejX8HV`z32rNfJb%*+o$s}xGJLc+nyeVggt(gIRYyIDCOeL#&oxndwz@(uUAH}7 zPnD$#Rg8t%02A?Vsp1Z|bzLlo=k}mfkfP=lsh|-vsxx6Po?FlDNdIH#BFD_C-CAO5 zQ^8wnO1rNa<2X(saMzK|l0gCcfr!9Ccc{I;A_NT24yq6fX|a|1vWK-p2e%B?m}Kc8 zhaOaPj`>J^Vw56y_$b-KF=FUguA&&jPGJ{Ra)<3!;rbb9a$pcgt&qk+#!r?!-mxm% zrHp5ND~tN+rgSbrIlyCpOMo2ewTfsC!J0fk4!bAyZdl!x_%x92%|jWEw`Y-iwO~J< zhE_jbnUBr#PCxnU_LINX_v2sub<;a|U;ktLwdO^{jWA?<@eBlAtoQ_Xed_I4)TX3? zib{EoVv#@)PhmXK>29A6N}sXTUq|*wc-d!UrE5A@U2GWyW4ieJEuz(;QqQy)uMaVX zw-oL@R*Y?fmBmu1t1*6-Q57AK{sDfmeMa?E%cHjGY!si*K026*;uYC(?@sW3O^jcFwtzsPz>! z%PALS79p0-Y}uH-82F9wEf$vGG$$Izon#yncA{Y~*2HqSh%Q?Gy?MXi6xS;ZOEoKI zV=QXvJX!+w;vx7C{{U#=JvG=r$vd;dT5=GGrlbXJ;h-)55av`niKjas^E|e%yCsBx zlAy?$2QQVgWB=qz+P3rtr?Iloe2ekcCazcAm>hTCD?!4-d7q&6_F{D_Jl8#Iu(=C?np-=z$N&9|SIJ%z zWp7H9*!QSyF{YAu_O8re@C=UjB?9`vqhecxsUqeX`;?EtJw@eq`d83=BypT|qHCtBUYf zXI-%hV|li@CvOUir~E*-Q+2T|E;5Hv#CA4=dT~#k=Vg_?IG6|f&8An^xgY?TnL=0J zY7v_}Vb7H!EmoDzdh$lWh0Ice{I*|R(v6@r&c3>dLKb(mQctsD{zc⁡~sqblGgo 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 TCAs$Wd2js-fauz)YDEbEVqEpL literal 0 HcmV?d00001 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..aac900911 100644 --- a/tests/waku_rln_relay/test_rln_group_manager_onchain.nim +++ b/tests/waku_rln_relay/test_rln_group_manager_onchain.nim @@ -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) diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index 0bbb448e1..ea3a5ca62 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -27,8 +27,8 @@ 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) diff --git a/tests/waku_rln_relay/test_wakunode_rln_relay.nim b/tests/waku_rln_relay/test_wakunode_rln_relay.nim index 7308ae257..1850b5277 100644 --- a/tests/waku_rln_relay/test_wakunode_rln_relay.nim +++ b/tests/waku_rln_relay/test_wakunode_rln_relay.nim @@ -30,8 +30,8 @@ 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) diff --git a/tests/waku_rln_relay/utils_onchain.nim b/tests/waku_rln_relay/utils_onchain.nim index 06e4fcdcf..d8bb13a62 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() @@ -106,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: @@ -142,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 @@ -157,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 @@ -487,20 +478,64 @@ 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) # 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 = [ + + var args = + @[ "--port", $port, "--gas-limit", @@ -509,9 +544,54 @@ proc runAnvil*(port: int = 8540, chainId: string = "1234"): Process = "1000000000", "--chain-id", $chainId, - ], - options = {poUsePath, poStdErrToStdOut}, - ) + ] + + # 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 @@ -549,7 +629,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}") @@ -560,52 +647,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/wakunode_rest/test_rest_health.nim b/tests/wakunode_rest/test_rest_health.nim index dacfd801e..ed8269f55 100644 --- a/tests/wakunode_rest/test_rest_health.nim +++ b/tests/wakunode_rest/test_rest_health.nim @@ -41,8 +41,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) diff --git a/vendor/waku-rlnv2-contract b/vendor/waku-rlnv2-contract index 900d4f95e..8a338f354 160000 --- a/vendor/waku-rlnv2-contract +++ b/vendor/waku-rlnv2-contract @@ -1 +1 @@ -Subproject commit 900d4f95e0e618bdeb4c241f7a4b6347df6bb950 +Subproject commit 8a338f354481e8a3f3d64a72e38fad4c62e32dcd 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 e8af61682..bdb272c1f 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 @@ -242,7 +242,7 @@ method register*( fetchedGasPrice = fetchedGasPrice, gasPrice = calculatedGasPrice calculatedGasPrice 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", @@ -259,11 +259,10 @@ method register*( var tsReceipt: ReceiptObject g.retryWrapper(tsReceipt, "Failed to get the transaction receipt"): await ethRpc.getMinedTransactionReceipt(txHash) - info "registration transaction mined", txHash = txHash + 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") @@ -272,18 +271,27 @@ method register*( ValueError, "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(): + raise newException( + ValueError, "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]) From 7920368a36687cd5f12afa52d59866792d8457ca Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 8 Dec 2025 06:34:57 -0300 Subject: [PATCH 05/11] fix: remove ENR cache from peer exchange (#3652) * remove WakuPeerExchange.enrCache * add forEnrPeers to support fast PeerStore search * add getEnrsFromStore * fix peer exchange tests --- tests/node/test_wakunode_peer_exchange.nim | 18 ++-- tests/waku_peer_exchange/test_protocol.nim | 100 +++++++++------------ waku/node/peer_manager/waku_peer_store.nim | 14 +++ waku/node/waku_node.nim | 3 - waku/waku_peer_exchange/protocol.nim | 98 +++++++++----------- 5 files changed, 108 insertions(+), 125 deletions(-) diff --git a/tests/node/test_wakunode_peer_exchange.nim b/tests/node/test_wakunode_peer_exchange.nim index 9b0ea4c40..e6649c455 100644 --- a/tests/node/test_wakunode_peer_exchange.nim +++ b/tests/node/test_wakunode_peer_exchange.nim @@ -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/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/waku/node/peer_manager/waku_peer_store.nim b/waku/node/peer_manager/waku_peer_store.nim index 9cde53fe1..b7f2669e5 100644 --- a/waku/node/peer_manager/waku_peer_store.nim +++ b/waku/node/peer_manager/waku_peer_store.nim @@ -227,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_node.nim b/waku/node/waku_node.nim index 65b2093bb..07e36dd13 100644 --- a/waku/node/waku_node.nim +++ b/waku/node/waku_node.nim @@ -525,9 +525,6 @@ proc stop*(node: WakuNode) {.async.} = 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() diff --git a/waku/waku_peer_exchange/protocol.nim b/waku/waku_peer_exchange/protocol.nim index cf7ebc2a7..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]).} = @@ -174,7 +160,8 @@ proc initProtocolHandler(wpx: WakuPeerExchange) = error "Failed to respond with BAD_REQUEST:", error = $error return - let enrs = wpx.getEnrsFromCache(decBuf.request.numPeers) + let enrs = wpx.getEnrsFromStore(decBuf.request.numPeers) + info "peer exchange request received" trace "px enrs to respond", enrs = $enrs try: @@ -214,5 +201,4 @@ proc new*( ) wpx.initProtocolHandler() setServiceLimitMetric(WakuPeerExchangeCodec, rateLimitSetting) - asyncSpawn wpx.updatePxEnrCache() return wpx From 12952d070f10fba51afbbcfbfa1b782d0d2fed3a Mon Sep 17 00:00:00 2001 From: Sergei Tikhomirov Date: Tue, 9 Dec 2025 10:45:06 +0100 Subject: [PATCH 06/11] Add text file for coding LLMs with high-level nwaku info and style guide advice (#3624) * add CLAUDE.md first version * extract style guide advice * use AGENTS.md instead of CLAUDE.md for neutrality * chore: update AGENTS.md w.r.t. master developments * Apply suggestions from code review Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> * remove project tree from AGENTS.md; minor editx * Apply suggestions from code review Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> --------- Co-authored-by: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Co-authored-by: NagyZoltanPeter <113987313+NagyZoltanPeter@users.noreply.github.com> --- AGENTS.md | 509 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 509 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..4f735f240 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,509 @@ +# 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`. + + From 868d43164e9b5ad0c3a856e872448e9e80531e0c Mon Sep 17 00:00:00 2001 From: Darshan K <35736874+darshankabariya@users.noreply.github.com> Date: Wed, 10 Dec 2025 17:40:42 +0530 Subject: [PATCH 07/11] Release : patch release v0.37.1-beta (#3661) --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61e818afd..3c80a3b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,10 @@ -## v0.37.0 (2025-10-01) +## v0.37.1-beta (2025-12-10) + +### Bug Fixes + +- 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-beta (2025-10-01) ### Notes From 7d1c6abaacba3e05edb57fa177381602b71b9b98 Mon Sep 17 00:00:00 2001 From: Sergei Tikhomirov Date: Thu, 11 Dec 2025 10:51:47 +0100 Subject: [PATCH 08/11] chore: do not mount lightpush without relay (fixes #2808) (#3540) * chore: do not mount lightpush without relay (fixes #2808) - Change mountLightPush signature to return Result[void, string] - Return error when relay is not mounted - Update all call sites to handle Result return type - Add test verifying mounting fails without relay - Only advertise lightpush capability when relay is enabled * chore: don't mount legacy lightpush without relay --- apps/chat2/chat2.nim | 4 +- tests/node/test_wakunode_legacy_lightpush.nim | 23 ++++++- tests/node/test_wakunode_lightpush.nim | 23 ++++++- tests/node/test_wakunode_sharding.nim | 8 +-- tests/wakunode_rest/test_rest_lightpush.nim | 2 +- .../test_rest_lightpush_legacy.nim | 2 +- .../conf_builder/waku_conf_builder.nim | 2 +- waku/factory/node_factory.nim | 7 ++- waku/node/kernel_api/lightpush.nim | 61 ++++++++++--------- 9 files changed, 88 insertions(+), 44 deletions(-) diff --git a/apps/chat2/chat2.nim b/apps/chat2/chat2.nim index e2a46ca1b..71d8a4e6a 100644 --- a/apps/chat2/chat2.nim +++ b/apps/chat2/chat2.nim @@ -480,7 +480,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/tests/node/test_wakunode_legacy_lightpush.nim b/tests/node/test_wakunode_legacy_lightpush.nim index 80e623ce4..4aedd7d4b 100644 --- a/tests/node/test_wakunode_legacy_lightpush.nim +++ b/tests/node/test_wakunode_legacy_lightpush.nim @@ -13,6 +13,7 @@ import node/peer_manager, node/waku_node, node/kernel_api, + node/kernel_api/lightpush, waku_lightpush_legacy, waku_lightpush_legacy/common, waku_lightpush_legacy/protocol_metrics, @@ -56,7 +57,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() @@ -147,7 +148,7 @@ 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) @@ -213,7 +214,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 +250,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_lightpush.nim b/tests/node/test_wakunode_lightpush.nim index 29f72b2cc..7b4da6d4c 100644 --- a/tests/node/test_wakunode_lightpush.nim +++ b/tests/node/test_wakunode_lightpush.nim @@ -13,6 +13,7 @@ import node/peer_manager, node/waku_node, node/kernel_api, + node/kernel_api/lightpush, waku_lightpush, waku_rln_relay, ], @@ -55,7 +56,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() @@ -147,7 +148,7 @@ 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) @@ -213,7 +214,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 +252,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_sharding.nim b/tests/node/test_wakunode_sharding.nim index eefd8f06e..261077e36 100644 --- a/tests/node/test_wakunode_sharding.nim +++ b/tests/node/test_wakunode_sharding.nim @@ -282,7 +282,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 +405,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" @@ -563,7 +563,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" @@ -874,7 +874,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 diff --git a/tests/wakunode_rest/test_rest_lightpush.nim b/tests/wakunode_rest/test_rest_lightpush.nim index cc5c715b8..deba7de22 100644 --- a/tests/wakunode_rest/test_rest_lightpush.nim +++ b/tests/wakunode_rest/test_rest_lightpush.nim @@ -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 526a6c24e..4043eeed9 100644 --- a/tests/wakunode_rest/test_rest_lightpush_legacy.nim +++ b/tests/wakunode_rest/test_rest_lightpush_legacy.nim @@ -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/waku/factory/conf_builder/waku_conf_builder.nim b/waku/factory/conf_builder/waku_conf_builder.nim index 645869247..f3f942ecc 100644 --- a/waku/factory/conf_builder/waku_conf_builder.nim +++ b/waku/factory/conf_builder/waku_conf_builder.nim @@ -606,7 +606,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, diff --git a/waku/factory/node_factory.nim b/waku/factory/node_factory.nim index 34fc958fe..2cdfdb0d2 100644 --- a/waku/factory/node_factory.nim +++ b/waku/factory/node_factory.nim @@ -368,8 +368,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()) diff --git a/waku/node/kernel_api/lightpush.nim b/waku/node/kernel_api/lightpush.nim index 9451767ac..2a5f6acbb 100644 --- a/waku/node/kernel_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" @@ -146,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) @@ -174,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" From 9e2b3830e92419ab6fec3263f858bd872300b295 Mon Sep 17 00:00:00 2001 From: Ivan FB <128452529+Ivansete-status@users.noreply.github.com> Date: Mon, 15 Dec 2025 12:11:11 +0100 Subject: [PATCH 09/11] Distribute libwaku (#3612) * allow create libwaku pkg * fix Makefile create library extension libwaku * make sure libwaku is built as part of assets * Makefile: avoid rm libwaku before building it * properly format debian pkg in gh release workflow * waku.nimble set dylib extension correctly * properly pass lib name and ext to waku.nimble --- .github/workflows/release-assets.yml | 75 +++++++++++++++++++++++++--- Makefile | 30 +++++++---- waku.nimble | 30 +++++------ 3 files changed, 98 insertions(+), 37 deletions(-) diff --git a/.github/workflows/release-assets.yml b/.github/workflows/release-assets.yml index c6cfbd680..50e3c4c3d 100644 --- a/.github/workflows/release-assets.yml +++ b/.github/workflows/release-assets.yml @@ -41,25 +41,84 @@ 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 + + - 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}}" 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}} -d:postgres" CI=false libwaku + make -j${NPROC} NIMFLAGS="--parallelBuild:${NPROC} -d:disableMarchNative --os:${OS} --cpu:${{matrix.arch}} -d:postgres" CI=false STATIC=1 libwaku + + - 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: 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 diff --git a/Makefile b/Makefile index 2f15ccd71..44f1c6495 100644 --- a/Makefile +++ b/Makefile @@ -426,18 +426,27 @@ docker-liteprotocoltester-push: .PHONY: cbindings cwaku_example libwaku STATIC ?= 0 +BUILD_COMMAND ?= libwakuDynamic + +ifeq ($(detected_OS),Windows) + LIB_EXT_DYNAMIC = dll + LIB_EXT_STATIC = lib +else ifeq ($(detected_OS),Darwin) + LIB_EXT_DYNAMIC = dylib + LIB_EXT_STATIC = a +else ifeq ($(detected_OS),Linux) + LIB_EXT_DYNAMIC = so + LIB_EXT_STATIC = a +endif + +LIB_EXT := $(LIB_EXT_DYNAMIC) +ifeq ($(STATIC), 1) + LIB_EXT = $(LIB_EXT_STATIC) + BUILD_COMMAND = libwakuStatic +endif 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) - make -f scripts/libwaku_windows_setup.mk windows-setup - echo -e $(BUILD_MSG) "build/$@.dll" && $(ENV_SCRIPT) nim libwakuDynamic $(NIM_PARAMS) waku.nims -else - echo -e $(BUILD_MSG) "build/$@.so" && $(ENV_SCRIPT) nim libwakuDynamic $(NIM_PARAMS) waku.nims -endif + echo -e $(BUILD_MSG) "build/$@.$(LIB_EXT)" && $(ENV_SCRIPT) nim $(BUILD_COMMAND) $(NIM_PARAMS) waku.nims $@.$(LIB_EXT) ##################### ## Mobile Bindings ## @@ -549,4 +558,3 @@ 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 - diff --git a/waku.nimble b/waku.nimble index 79fdd9fd6..09ff48969 100644 --- a/waku.nimble +++ b/waku.nimble @@ -61,27 +61,21 @@ proc buildBinary(name: string, srcDir = "./", params = "", lang = "c") = exec "nim " & lang & " --out:build/" & name & " --mm:refc " & extra_params & " " & srcDir & name & ".nim" -proc buildLibrary(name: string, srcDir = "./", params = "", `type` = "static") = +proc buildLibrary(lib_name: string, srcDir = "./", params = "", `type` = "static") = 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(): + for i in 2 ..< (paramCount() - 1): 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:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:libwaku --skipParentCfg:on -d:discv5_protocol_id=d5waku " & + extra_params & " " & srcDir & "libwaku.nim" 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:size --noMain --mm:refc --header -d:metrics --nimMainPrefix:libwaku --skipParentCfg:off -d:discv5_protocol_id=d5waku " & + extra_params & " " & srcDir & "libwaku.nim" proc buildMobileAndroid(srcDir = ".", params = "") = let cpu = getEnv("CPU") @@ -206,12 +200,12 @@ let chroniclesParams = "--warning:UnusedImport:on " & "-d:chronicles_log_level=TRACE" task libwakuStatic, "Build the cbindings waku node library": - let name = "libwaku" - buildLibrary name, "library/", chroniclesParams, "static" + let lib_name = paramStr(paramCount()) + buildLibrary lib_name, "library/", chroniclesParams, "static" task libwakuDynamic, "Build the cbindings waku node library": - let name = "libwaku" - buildLibrary name, "library/", chroniclesParams, "dynamic" + let lib_name = paramStr(paramCount()) + buildLibrary lib_name, "library/", chroniclesParams, "dynamic" ### Mobile Android task libWakuAndroid, "Build the mobile bindings for Android": From 10dc3d3eb4b6a3d4313f7b2cc4a85a925e9ce039 Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 15 Dec 2025 09:15:33 -0300 Subject: [PATCH 10/11] chore: misc CI fixes (#3664) * add make update to CI workflow * add a nwaku -> logos-messaging-nim workflow rename * pin local container-image.yml workflow to a commit --- .github/workflows/ci.yml | 8 +++++++- .github/workflows/container-image.yml | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3186a007..b51f4621c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -76,6 +76,9 @@ jobs: .git/modules key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + - name: Make update + run: make update + - name: Build binaries run: make V=1 QUICK_AND_DIRTY_COMPILER=1 all tools @@ -114,6 +117,9 @@ jobs: .git/modules key: ${{ runner.os }}-vendor-modules-${{ steps.submodules.outputs.hash }} + - name: Make update + run: make update + - name: Run tests run: | postgres_enabled=0 @@ -132,7 +138,7 @@ jobs: build-docker-image: needs: changes if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' || needs.changes.outputs.docker == 'true' }} - uses: logos-messaging/nwaku/.github/workflows/container-image.yml@master + uses: logos-messaging/logos-messaging-nim/.github/workflows/container-image.yml@4139681df984de008069e86e8ce695f1518f1c0b secrets: inherit nwaku-nwaku-interop-tests: diff --git a/.github/workflows/container-image.yml b/.github/workflows/container-image.yml index cfa66d20a..2bc08be2f 100644 --- a/.github/workflows/container-image.yml +++ b/.github/workflows/container-image.yml @@ -41,7 +41,7 @@ jobs: env: QUAY_PASSWORD: ${{ secrets.QUAY_PASSWORD }} QUAY_USER: ${{ secrets.QUAY_USER }} - + - name: Checkout code if: ${{ steps.secrets.outcome == 'success' }} uses: actions/checkout@v4 @@ -65,6 +65,7 @@ jobs: id: build if: ${{ steps.secrets.outcome == 'success' }} run: | + make update make -j${NPROC} V=1 QUICK_AND_DIRTY_COMPILER=1 NIMFLAGS="-d:disableMarchNative -d:postgres -d:chronicles_colors:none" wakunode2 From 2477c4980f15df0efc2eedf27d7593e0dd2b1e1b Mon Sep 17 00:00:00 2001 From: Fabiana Cecin Date: Mon, 15 Dec 2025 10:33:39 -0300 Subject: [PATCH 11/11] chore: update ci container-image.yml ref to a commit in master (#3666) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b51f4621c..9c94577f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,7 +138,7 @@ jobs: build-docker-image: needs: changes if: ${{ needs.changes.outputs.v2 == 'true' || needs.changes.outputs.common == 'true' || needs.changes.outputs.docker == 'true' }} - uses: logos-messaging/logos-messaging-nim/.github/workflows/container-image.yml@4139681df984de008069e86e8ce695f1518f1c0b + uses: logos-messaging/logos-messaging-nim/.github/workflows/container-image.yml@10dc3d3eb4b6a3d4313f7b2cc4a85a925e9ce039 secrets: inherit nwaku-nwaku-interop-tests: